172 lines
4.2 KiB
Go
172 lines
4.2 KiB
Go
package ruleguard
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/types"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/quasilyte/go-ruleguard/ruleguard/quasigo"
|
|
"github.com/quasilyte/go-ruleguard/ruleguard/typematch"
|
|
)
|
|
|
|
type engine struct {
|
|
state *engineState
|
|
|
|
ruleSet *goRuleSet
|
|
}
|
|
|
|
func newEngine() *engine {
|
|
return &engine{
|
|
state: newEngineState(),
|
|
}
|
|
}
|
|
|
|
func (e *engine) Load(ctx *ParseContext, filename string, r io.Reader) error {
|
|
config := rulesParserConfig{
|
|
state: e.state,
|
|
ctx: ctx,
|
|
importer: newGoImporter(e.state, goImporterConfig{
|
|
fset: ctx.Fset,
|
|
debugImports: ctx.DebugImports,
|
|
debugPrint: ctx.DebugPrint,
|
|
}),
|
|
itab: typematch.NewImportsTab(stdlibPackages),
|
|
}
|
|
p := newRulesParser(config)
|
|
rset, err := p.ParseFile(filename, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if e.ruleSet == nil {
|
|
e.ruleSet = rset
|
|
} else {
|
|
combinedRuleSet, err := mergeRuleSets([]*goRuleSet{e.ruleSet, rset})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.ruleSet = combinedRuleSet
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *engine) Run(ctx *RunContext, f *ast.File) error {
|
|
if e.ruleSet == nil {
|
|
return errors.New("used Run() with an empty rule set; forgot to call Load() first?")
|
|
}
|
|
rset := cloneRuleSet(e.ruleSet)
|
|
return newRulesRunner(ctx, e.state, rset).run(f)
|
|
}
|
|
|
|
// engineState is a shared state inside the engine.
|
|
type engineState struct {
|
|
env *quasigo.Env
|
|
|
|
typeByFQNMu sync.RWMutex
|
|
typeByFQN map[string]types.Type
|
|
|
|
pkgCacheMu sync.RWMutex
|
|
// pkgCache contains all imported packages, from any importer.
|
|
pkgCache map[string]*types.Package
|
|
}
|
|
|
|
func newEngineState() *engineState {
|
|
env := quasigo.NewEnv()
|
|
state := &engineState{
|
|
env: env,
|
|
pkgCache: make(map[string]*types.Package),
|
|
typeByFQN: map[string]types.Type{},
|
|
}
|
|
for key, typ := range typeByName {
|
|
state.typeByFQN[key] = typ
|
|
}
|
|
initEnv(state, env)
|
|
return state
|
|
}
|
|
|
|
func (state *engineState) GetCachedPackage(pkgPath string) *types.Package {
|
|
state.pkgCacheMu.RLock()
|
|
pkg := state.pkgCache[pkgPath]
|
|
state.pkgCacheMu.RUnlock()
|
|
return pkg
|
|
}
|
|
|
|
func (state *engineState) AddCachedPackage(pkgPath string, pkg *types.Package) {
|
|
state.pkgCacheMu.Lock()
|
|
state.addCachedPackage(pkgPath, pkg)
|
|
state.pkgCacheMu.Unlock()
|
|
}
|
|
|
|
func (state *engineState) addCachedPackage(pkgPath string, pkg *types.Package) {
|
|
state.pkgCache[pkgPath] = pkg
|
|
|
|
// Also add all complete packages that are dependencies of the pkg.
|
|
// This way we cache more and avoid duplicated package loading
|
|
// which can lead to typechecking issues.
|
|
//
|
|
// Note that it does not increase our memory consumption
|
|
// as these packages are reachable via pkg, so they'll
|
|
// not be freed by GC anyway.
|
|
for _, imported := range pkg.Imports() {
|
|
if imported.Complete() {
|
|
state.addCachedPackage(imported.Path(), imported)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (state *engineState) FindType(importer *goImporter, currentPkg *types.Package, fqn string) (types.Type, error) {
|
|
// TODO(quasilyte): we can pre-populate the cache during the Load() phase.
|
|
// If we inspect the AST of a user function, all constant FQN can be preloaded.
|
|
// It could be a good thing as Load() is not expected to be executed in
|
|
// concurrent environment, so write-locking is not a big deal there.
|
|
|
|
state.typeByFQNMu.RLock()
|
|
cachedType, ok := state.typeByFQN[fqn]
|
|
state.typeByFQNMu.RUnlock()
|
|
if ok {
|
|
return cachedType, nil
|
|
}
|
|
|
|
// Code below is under a write critical section.
|
|
state.typeByFQNMu.Lock()
|
|
defer state.typeByFQNMu.Unlock()
|
|
|
|
typ, err := state.findTypeNoCache(importer, currentPkg, fqn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state.typeByFQN[fqn] = typ
|
|
return typ, nil
|
|
}
|
|
|
|
func (state *engineState) findTypeNoCache(importer *goImporter, currentPkg *types.Package, fqn string) (types.Type, error) {
|
|
pos := strings.LastIndexByte(fqn, '.')
|
|
if pos == -1 {
|
|
return nil, fmt.Errorf("%s is not a valid FQN", fqn)
|
|
}
|
|
pkgPath := fqn[:pos]
|
|
objectName := fqn[pos+1:]
|
|
var pkg *types.Package
|
|
if directDep := findDependency(currentPkg, pkgPath); directDep != nil {
|
|
pkg = directDep
|
|
} else {
|
|
loadedPkg, err := importer.Import(pkgPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkg = loadedPkg
|
|
}
|
|
obj := pkg.Scope().Lookup(objectName)
|
|
if obj == nil {
|
|
return nil, fmt.Errorf("%s is not found in %s", objectName, pkgPath)
|
|
}
|
|
typ := obj.Type()
|
|
state.typeByFQN[fqn] = typ
|
|
return typ, nil
|
|
}
|