woodpecker/vendor/github.com/quasilyte/go-ruleguard/ruleguard/engine.go
Lukas c28f7cb29f
Add golangci-lint (#502)
Initial part of #435
2021-11-14 21:01:54 +01:00

171 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
}