woodpecker/vendor/github.com/quasilyte/go-ruleguard/ruleguard/ir_loader.go
2021-11-16 21:07:53 +01:00

720 lines
20 KiB
Go

package ruleguard
import (
"bytes"
"fmt"
"go/ast"
"go/constant"
"go/parser"
"go/token"
"go/types"
"io/ioutil"
"regexp"
"github.com/quasilyte/go-ruleguard/internal/gogrep"
"github.com/quasilyte/go-ruleguard/nodetag"
"github.com/quasilyte/go-ruleguard/ruleguard/goutil"
"github.com/quasilyte/go-ruleguard/ruleguard/ir"
"github.com/quasilyte/go-ruleguard/ruleguard/quasigo"
"github.com/quasilyte/go-ruleguard/ruleguard/textmatch"
"github.com/quasilyte/go-ruleguard/ruleguard/typematch"
)
type irLoaderConfig struct {
ctx *LoadContext
state *engineState
importer *goImporter
itab *typematch.ImportsTab
pkg *types.Package
gogrepFset *token.FileSet
prefix string
importedPkg string
}
type irLoader struct {
state *engineState
ctx *LoadContext
itab *typematch.ImportsTab
pkg *types.Package
file *ir.File
gogrepFset *token.FileSet
filename string
res *goRuleSet
importer *goImporter
group *GoRuleGroup
prefix string // For imported packages, a prefix that is added to a rule group name
importedPkg string // Package path; only for imported packages
imported []*goRuleSet
}
func newIRLoader(config irLoaderConfig) *irLoader {
return &irLoader{
state: config.state,
ctx: config.ctx,
importer: config.importer,
itab: config.itab,
pkg: config.pkg,
prefix: config.prefix,
gogrepFset: config.gogrepFset,
}
}
func (l *irLoader) LoadFile(filename string, f *ir.File) (*goRuleSet, error) {
l.filename = filename
l.file = f
l.res = &goRuleSet{
universal: &scopedGoRuleSet{},
groups: make(map[string]*GoRuleGroup),
}
for _, imp := range f.BundleImports {
if l.importedPkg != "" {
return nil, l.errorf(imp.Line, nil, "imports from imported packages are not supported yet")
}
if err := l.loadBundle(imp); err != nil {
return nil, err
}
}
if err := l.compileFilterFuncs(filename, f); err != nil {
return nil, err
}
for i := range f.RuleGroups {
if err := l.loadRuleGroup(&f.RuleGroups[i]); err != nil {
return nil, err
}
}
if len(l.imported) != 0 {
toMerge := []*goRuleSet{l.res}
toMerge = append(toMerge, l.imported...)
merged, err := mergeRuleSets(toMerge)
if err != nil {
return nil, err
}
l.res = merged
}
return l.res, nil
}
func (l *irLoader) importErrorf(line int, wrapped error, format string, args ...interface{}) error {
return &ImportError{
msg: fmt.Sprintf("%s:%d: %s", l.filename, line, fmt.Sprintf(format, args...)),
err: wrapped,
}
}
func (l *irLoader) errorf(line int, wrapped error, format string, args ...interface{}) error {
if wrapped == nil {
return fmt.Errorf("%s:%d: %s", l.filename, line, fmt.Sprintf(format, args...))
}
return fmt.Errorf("%s:%d: %s: %w", l.filename, line, fmt.Sprintf(format, args...), wrapped)
}
func (l *irLoader) loadBundle(bundle ir.BundleImport) error {
files, err := findBundleFiles(bundle.PkgPath)
if err != nil {
return l.errorf(bundle.Line, err, "can't find imported bundle files")
}
for _, filename := range files {
rset, err := l.loadExternFile(bundle.Prefix, bundle.PkgPath, filename)
if err != nil {
return l.errorf(bundle.Line, err, "error during bundle file loading")
}
l.imported = append(l.imported, rset)
}
return nil
}
func (l *irLoader) loadExternFile(prefix, pkgPath, filename string) (*goRuleSet, error) {
src, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
irfile, pkg, err := convertAST(l.ctx, l.importer, filename, src)
if err != nil {
return nil, err
}
config := irLoaderConfig{
state: l.state,
ctx: l.ctx,
importer: l.importer,
prefix: prefix,
pkg: pkg,
importedPkg: pkgPath,
itab: l.itab,
gogrepFset: l.gogrepFset,
}
rset, err := newIRLoader(config).LoadFile(filename, irfile)
if err != nil {
return nil, fmt.Errorf("%s: %w", l.importedPkg, err)
}
return rset, nil
}
func (l *irLoader) compileFilterFuncs(filename string, irfile *ir.File) error {
if len(irfile.CustomDecls) == 0 {
return nil
}
var buf bytes.Buffer
buf.WriteString("package gorules\n")
buf.WriteString("import \"github.com/quasilyte/go-ruleguard/dsl\"\n")
buf.WriteString("import \"github.com/quasilyte/go-ruleguard/dsl/types\"\n")
buf.WriteString("type _ = dsl.Matcher\n")
buf.WriteString("type _ = types.Type\n")
for _, src := range irfile.CustomDecls {
buf.WriteString(src)
buf.WriteString("\n")
}
fset := token.NewFileSet()
f, err := goutil.LoadGoFile(goutil.LoadConfig{
Fset: fset,
Filename: filename,
Data: &buf,
Importer: l.importer,
})
if err != nil {
// If this ever happens, user will get unexpected error
// lines for it; but we should trust that 99.9% errors
// should be catched at irconv phase so we get a valid Go
// source here as well?
return fmt.Errorf("parse custom decls: %w", err)
}
for _, decl := range f.Syntax.Decls {
decl, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
ctx := &quasigo.CompileContext{
Env: l.state.env,
Types: f.Types,
Fset: fset,
}
compiled, err := quasigo.Compile(ctx, decl)
if err != nil {
return err
}
if l.ctx.DebugFilter == decl.Name.String() {
l.ctx.DebugPrint(quasigo.Disasm(l.state.env, compiled))
}
ctx.Env.AddFunc(f.Pkg.Path(), decl.Name.String(), compiled)
}
return nil
}
func (l *irLoader) loadRuleGroup(group *ir.RuleGroup) error {
l.group = &GoRuleGroup{
Line: group.Line,
Filename: l.filename,
Name: group.Name,
DocSummary: group.DocSummary,
DocBefore: group.DocBefore,
DocAfter: group.DocAfter,
DocNote: group.DocNote,
DocTags: group.DocTags,
}
if l.prefix != "" {
l.group.Name = l.prefix + "/" + l.group.Name
}
if l.ctx.GroupFilter != nil && !l.ctx.GroupFilter(l.group.Name) {
return nil // Skip this group
}
if _, ok := l.res.groups[l.group.Name]; ok {
panic(fmt.Sprintf("duplicated function %s after the typecheck", l.group.Name)) // Should never happen
}
l.res.groups[l.group.Name] = l.group
l.itab.EnterScope()
defer l.itab.LeaveScope()
for _, imported := range group.Imports {
l.itab.Load(imported.Name, imported.Path)
}
for _, rule := range group.Rules {
if err := l.loadRule(rule); err != nil {
return err
}
}
return nil
}
func (l *irLoader) loadRule(rule ir.Rule) error {
proto := goRule{
line: rule.Line,
group: l.group,
suggestion: rule.SuggestTemplate,
msg: rule.ReportTemplate,
location: rule.LocationVar,
}
info := filterInfo{
Vars: make(map[string]struct{}),
}
if rule.WhereExpr.IsValid() {
filter, err := l.newFilter(rule.WhereExpr, &info)
if err != nil {
return err
}
proto.filter = filter
}
for _, pat := range rule.SyntaxPatterns {
if err := l.loadSyntaxRule(proto, info, rule, pat.Value, pat.Line); err != nil {
return err
}
}
for _, pat := range rule.CommentPatterns {
if err := l.loadCommentRule(proto, rule, pat.Value, pat.Line); err != nil {
return err
}
}
return nil
}
func (l *irLoader) loadCommentRule(resultProto goRule, rule ir.Rule, src string, line int) error {
dst := l.res.universal
pat, err := regexp.Compile(src)
if err != nil {
return l.errorf(rule.Line, err, "compile regexp")
}
resultBase := resultProto
resultBase.line = line
result := goCommentRule{
base: resultProto,
pat: pat,
captureGroups: regexpHasCaptureGroups(src),
}
dst.commentRules = append(dst.commentRules, result)
return nil
}
func (l *irLoader) loadSyntaxRule(resultProto goRule, filterInfo filterInfo, rule ir.Rule, src string, line int) error {
result := resultProto
result.line = line
pat, info, err := gogrep.Compile(l.gogrepFset, src, false)
if err != nil {
return l.errorf(rule.Line, err, "parse match pattern")
}
result.pat = pat
for filterVar := range filterInfo.Vars {
if filterVar == "$$" {
continue // OK: a predefined var for the "entire match"
}
_, ok := info.Vars[filterVar]
if !ok {
return l.errorf(rule.Line, nil, "filter refers to a non-existing var %s", filterVar)
}
}
dst := l.res.universal
var dstTags []nodetag.Value
switch tag := pat.NodeTag(); tag {
case nodetag.Unknown:
return l.errorf(rule.Line, nil, "can't infer a tag of %s", src)
case nodetag.Node:
return l.errorf(rule.Line, nil, "%s pattern is too general", src)
case nodetag.StmtList:
dstTags = []nodetag.Value{
nodetag.BlockStmt,
nodetag.CaseClause,
nodetag.CommClause,
}
case nodetag.ExprList:
dstTags = []nodetag.Value{
nodetag.CallExpr,
nodetag.CompositeLit,
nodetag.ReturnStmt,
}
default:
dstTags = []nodetag.Value{tag}
}
for _, tag := range dstTags {
dst.rulesByTag[tag] = append(dst.rulesByTag[tag], result)
}
dst.categorizedNum++
return nil
}
func (l *irLoader) unwrapTypeExpr(filter ir.FilterExpr) (types.Type, error) {
typeString := l.unwrapStringExpr(filter)
if typeString == "" {
return nil, l.errorf(filter.Line, nil, "expected a non-empty type string")
}
typ, err := typeFromString(typeString)
if err != nil {
return nil, l.errorf(filter.Line, err, "parse type expr")
}
if typ == nil {
return nil, l.errorf(filter.Line, nil, "can't convert %s into a type constraint yet", typeString)
}
return typ, nil
}
func (l *irLoader) unwrapInterfaceExpr(filter ir.FilterExpr) (*types.Interface, error) {
typeString := l.unwrapStringExpr(filter)
if typeString == "" {
return nil, l.errorf(filter.Line, nil, "expected a non-empty type name string")
}
typ, err := l.state.FindType(l.importer, l.pkg, typeString)
if err == nil {
iface, ok := typ.Underlying().(*types.Interface)
if !ok {
return nil, l.errorf(filter.Line, nil, "%s is not an interface type", typeString)
}
return iface, nil
}
n, err := parser.ParseExpr(typeString)
if err != nil {
return nil, l.errorf(filter.Line, err, "parse %s type expr", typeString)
}
qn, ok := n.(*ast.SelectorExpr)
if !ok {
return nil, l.errorf(filter.Line, nil, "can't resolve %s type; try a fully-qualified name", typeString)
}
pkgName, ok := qn.X.(*ast.Ident)
if !ok {
return nil, l.errorf(filter.Line, nil, "invalid package name")
}
pkgPath, ok := l.itab.Lookup(pkgName.Name)
if !ok {
return nil, l.errorf(filter.Line, nil, "package %s is not imported", pkgName.Name)
}
pkg, err := l.importer.Import(pkgPath)
if err != nil {
return nil, l.importErrorf(filter.Line, err, "can't load %s", pkgPath)
}
obj := pkg.Scope().Lookup(qn.Sel.Name)
if obj == nil {
return nil, l.errorf(filter.Line, nil, "%s is not found in %s", qn.Sel.Name, pkgPath)
}
iface, ok := obj.Type().Underlying().(*types.Interface)
if !ok {
return nil, l.errorf(filter.Line, nil, "%s is not an interface type", qn.Sel.Name)
}
return iface, nil
}
func (l *irLoader) unwrapRegexpExpr(filter ir.FilterExpr) (textmatch.Pattern, error) {
patternString := l.unwrapStringExpr(filter)
if patternString == "" {
return nil, l.errorf(filter.Line, nil, "expected a non-empty regexp pattern argument")
}
re, err := textmatch.Compile(patternString)
if err != nil {
return nil, l.errorf(filter.Line, err, "compile regexp")
}
return re, nil
}
func (l *irLoader) unwrapNodeTagExpr(filter ir.FilterExpr) (nodetag.Value, error) {
typeString := l.unwrapStringExpr(filter)
if typeString == "" {
return nodetag.Unknown, l.errorf(filter.Line, nil, "expected a non-empty string argument")
}
tag := nodetag.FromString(typeString)
if tag == nodetag.Unknown {
return tag, l.errorf(filter.Line, nil, "%s is not a valid go/ast type name", typeString)
}
return tag, nil
}
func (l *irLoader) unwrapStringExpr(filter ir.FilterExpr) string {
if filter.Op == ir.FilterStringOp {
return filter.Value.(string)
}
return ""
}
func (l *irLoader) newFilter(filter ir.FilterExpr, info *filterInfo) (matchFilter, error) {
if filter.HasVar() {
info.Vars[filter.Value.(string)] = struct{}{}
}
if filter.IsBinaryExpr() {
return l.newBinaryExprFilter(filter, info)
}
result := matchFilter{src: filter.Src}
switch filter.Op {
case ir.FilterNotOp:
x, err := l.newFilter(filter.Args[0], info)
if err != nil {
return result, err
}
result.fn = makeNotFilter(result.src, x)
case ir.FilterVarTextMatchesOp:
re, err := l.unwrapRegexpExpr(filter.Args[0])
if err != nil {
return result, err
}
result.fn = makeTextMatchesFilter(result.src, filter.Value.(string), re)
case ir.FilterVarObjectIsOp:
typeString := l.unwrapStringExpr(filter.Args[0])
if typeString == "" {
return result, l.errorf(filter.Line, nil, "expected a non-empty string argument")
}
switch typeString {
case "Func", "Var", "Const", "TypeName", "Label", "PkgName", "Builtin", "Nil":
// OK.
default:
return result, l.errorf(filter.Line, nil, "%s is not a valid go/types object name", typeString)
}
result.fn = makeObjectIsFilter(result.src, filter.Value.(string), typeString)
case ir.FilterRootNodeParentIsOp:
tag, err := l.unwrapNodeTagExpr(filter.Args[0])
if err != nil {
return result, err
}
result.fn = makeRootParentNodeIsFilter(result.src, tag)
case ir.FilterVarNodeIsOp:
tag, err := l.unwrapNodeTagExpr(filter.Args[0])
if err != nil {
return result, err
}
result.fn = makeNodeIsFilter(result.src, filter.Value.(string), tag)
case ir.FilterVarTypeIsOp, ir.FilterVarTypeUnderlyingIsOp:
typeString := l.unwrapStringExpr(filter.Args[0])
if typeString == "" {
return result, l.errorf(filter.Line, nil, "expected a non-empty string argument")
}
ctx := typematch.Context{Itab: l.itab}
pat, err := typematch.Parse(&ctx, typeString)
if err != nil {
return result, l.errorf(filter.Line, err, "parse type expr")
}
underlying := filter.Op == ir.FilterVarTypeUnderlyingIsOp
result.fn = makeTypeIsFilter(result.src, filter.Value.(string), underlying, pat)
case ir.FilterVarTypeConvertibleToOp:
dstType, err := l.unwrapTypeExpr(filter.Args[0])
if err != nil {
return result, err
}
result.fn = makeTypeConvertibleToFilter(result.src, filter.Value.(string), dstType)
case ir.FilterVarTypeAssignableToOp:
dstType, err := l.unwrapTypeExpr(filter.Args[0])
if err != nil {
return result, err
}
result.fn = makeTypeAssignableToFilter(result.src, filter.Value.(string), dstType)
case ir.FilterVarTypeImplementsOp:
iface, err := l.unwrapInterfaceExpr(filter.Args[0])
if err != nil {
return result, err
}
result.fn = makeTypeImplementsFilter(result.src, filter.Value.(string), iface)
case ir.FilterVarPureOp:
result.fn = makePureFilter(result.src, filter.Value.(string))
case ir.FilterVarConstOp:
result.fn = makeConstFilter(result.src, filter.Value.(string))
case ir.FilterVarConstSliceOp:
result.fn = makeConstSliceFilter(result.src, filter.Value.(string))
case ir.FilterVarAddressableOp:
result.fn = makeAddressableFilter(result.src, filter.Value.(string))
case ir.FilterFileImportsOp:
result.fn = makeFileImportsFilter(result.src, filter.Value.(string))
case ir.FilterDeadcodeOp:
result.fn = makeDeadcodeFilter(result.src)
case ir.FilterGoVersionEqOp:
version, err := ParseGoVersion(filter.Value.(string))
if err != nil {
return result, l.errorf(filter.Line, err, "parse Go version")
}
result.fn = makeGoVersionFilter(result.src, token.EQL, version)
case ir.FilterGoVersionLessThanOp:
version, err := ParseGoVersion(filter.Value.(string))
if err != nil {
return result, l.errorf(filter.Line, err, "parse Go version")
}
result.fn = makeGoVersionFilter(result.src, token.LSS, version)
case ir.FilterGoVersionGreaterThanOp:
version, err := ParseGoVersion(filter.Value.(string))
if err != nil {
return result, l.errorf(filter.Line, err, "parse Go version")
}
result.fn = makeGoVersionFilter(result.src, token.GTR, version)
case ir.FilterGoVersionLessEqThanOp:
version, err := ParseGoVersion(filter.Value.(string))
if err != nil {
return result, l.errorf(filter.Line, err, "parse Go version")
}
result.fn = makeGoVersionFilter(result.src, token.LEQ, version)
case ir.FilterGoVersionGreaterEqThanOp:
version, err := ParseGoVersion(filter.Value.(string))
if err != nil {
return result, l.errorf(filter.Line, err, "parse Go version")
}
result.fn = makeGoVersionFilter(result.src, token.GEQ, version)
case ir.FilterFilePkgPathMatchesOp:
re, err := regexp.Compile(filter.Value.(string))
if err != nil {
return result, l.errorf(filter.Line, err, "compile regexp")
}
result.fn = makeFilePkgPathMatchesFilter(result.src, re)
case ir.FilterFileNameMatchesOp:
re, err := regexp.Compile(filter.Value.(string))
if err != nil {
return result, l.errorf(filter.Line, err, "compile regexp")
}
result.fn = makeFileNameMatchesFilter(result.src, re)
case ir.FilterVarFilterOp:
funcName := filter.Args[0].Value.(string)
userFn := l.state.env.GetFunc(l.file.PkgPath, funcName)
if userFn == nil {
return result, l.errorf(filter.Line, nil, "can't find a compiled version of %s", funcName)
}
result.fn = makeCustomVarFilter(result.src, filter.Value.(string), userFn)
}
if result.fn == nil {
return result, l.errorf(filter.Line, nil, "unsupported expr: %s (%s)", result.src, filter.Op)
}
return result, nil
}
func (l *irLoader) newBinaryExprFilter(filter ir.FilterExpr, info *filterInfo) (matchFilter, error) {
if filter.Op == ir.FilterAndOp || filter.Op == ir.FilterOrOp {
result := matchFilter{src: filter.Src}
lhs, err := l.newFilter(filter.Args[0], info)
if err != nil {
return result, err
}
rhs, err := l.newFilter(filter.Args[1], info)
if err != nil {
return result, err
}
if filter.Op == ir.FilterAndOp {
result.fn = makeAndFilter(lhs, rhs)
} else {
result.fn = makeOrFilter(lhs, rhs)
}
return result, nil
}
// If constexpr is on the LHS, move it to the right, so the code below
// can imply constants being on the RHS all the time.
if filter.Args[0].IsBasicLit() && !filter.Args[1].IsBasicLit() {
// Just a precaution: if we ever have a float values here,
// we may not want to rearrange anything.
switch filter.Args[0].Value.(type) {
case string, int64:
switch filter.Op {
case ir.FilterEqOp, ir.FilterNeqOp:
// Simple commutative ops. Just swap the args.
newFilter := filter
newFilter.Args = []ir.FilterExpr{filter.Args[1], filter.Args[0]}
return l.newBinaryExprFilter(newFilter, info)
}
}
}
result := matchFilter{src: filter.Src}
var tok token.Token
switch filter.Op {
case ir.FilterEqOp:
tok = token.EQL
case ir.FilterNeqOp:
tok = token.NEQ
case ir.FilterGtOp:
tok = token.GTR
case ir.FilterGtEqOp:
tok = token.GEQ
case ir.FilterLtOp:
tok = token.LSS
case ir.FilterLtEqOp:
tok = token.LEQ
default:
return result, l.errorf(filter.Line, nil, "unsupported operator in binary expr: %s", result.src)
}
lhs := filter.Args[0]
rhs := filter.Args[1]
var rhsValue constant.Value
switch rhs.Op {
case ir.FilterStringOp:
rhsValue = constant.MakeString(rhs.Value.(string))
case ir.FilterIntOp:
rhsValue = constant.MakeInt64(rhs.Value.(int64))
}
switch lhs.Op {
case ir.FilterVarLineOp:
if rhsValue != nil {
result.fn = makeLineConstFilter(result.src, lhs.Value.(string), tok, rhsValue)
} else if rhs.Op == lhs.Op {
result.fn = makeLineFilter(result.src, lhs.Value.(string), tok, rhs.Value.(string))
}
case ir.FilterVarTypeSizeOp:
if rhsValue != nil {
result.fn = makeTypeSizeConstFilter(result.src, lhs.Value.(string), tok, rhsValue)
}
case ir.FilterVarValueIntOp:
if rhsValue != nil {
result.fn = makeValueIntConstFilter(result.src, lhs.Value.(string), tok, rhsValue)
} else if rhs.Op == lhs.Op {
result.fn = makeValueIntFilter(result.src, lhs.Value.(string), tok, rhs.Value.(string))
}
case ir.FilterVarTextOp:
if rhsValue != nil {
result.fn = makeTextConstFilter(result.src, lhs.Value.(string), tok, rhsValue)
} else if rhs.Op == lhs.Op {
result.fn = makeTextFilter(result.src, lhs.Value.(string), tok, rhs.Value.(string))
}
}
if result.fn == nil {
return result, l.errorf(filter.Line, nil, "unsupported binary expr: %s", result.src)
}
return result, nil
}
type filterInfo struct {
Vars map[string]struct{}
}