diff --git a/analyzer/testdata/src/filtertest/f1.go b/analyzer/testdata/src/filtertest/f1.go index b537c807..c11fedb2 100644 --- a/analyzer/testdata/src/filtertest/f1.go +++ b/analyzer/testdata/src/filtertest/f1.go @@ -10,6 +10,11 @@ type implementsAll struct{} func (implementsAll) Read([]byte) (int, error) { return 0, nil } func (implementsAll) String() string { return "" } +func _() { + fileTest("with foo prefix") + fileTest("f1.go") // want `YES` +} + func detectType() { { type withNamedTime struct { diff --git a/analyzer/testdata/src/filtertest/foo_file1.go b/analyzer/testdata/src/filtertest/foo_file1.go new file mode 100644 index 00000000..79f962f1 --- /dev/null +++ b/analyzer/testdata/src/filtertest/foo_file1.go @@ -0,0 +1,5 @@ +package filtertest + +func _() { + fileTest("with foo prefix") // want `YES` +} diff --git a/analyzer/testdata/src/filtertest/foo_file2.go b/analyzer/testdata/src/filtertest/foo_file2.go new file mode 100644 index 00000000..79f962f1 --- /dev/null +++ b/analyzer/testdata/src/filtertest/foo_file2.go @@ -0,0 +1,5 @@ +package filtertest + +func _() { + fileTest("with foo prefix") // want `YES` +} diff --git a/analyzer/testdata/src/filtertest/rules.go b/analyzer/testdata/src/filtertest/rules.go index d503e553..1b443d06 100644 --- a/analyzer/testdata/src/filtertest/rules.go +++ b/analyzer/testdata/src/filtertest/rules.go @@ -98,4 +98,12 @@ func _(m fluent.Matcher) { m.Match(`importsTest(os.PathListSeparator, "path/filepath")`). Where(m.File().Imports("path/filepath")). Report(`YES`) + + m.Match(`fileTest("with foo prefix")`). + Where(m.File().Name.Matches(`^foo_`)). + Report(`YES`) + + m.Match(`fileTest("f1.go")`). + Where(m.File().Name.Matches(`^f1.go$`)). + Report(`YES`) } diff --git a/analyzer/testdata/src/filtertest/utils.go b/analyzer/testdata/src/filtertest/utils.go index 73c9ce6f..0ce6f5b6 100644 --- a/analyzer/testdata/src/filtertest/utils.go +++ b/analyzer/testdata/src/filtertest/utils.go @@ -5,6 +5,7 @@ func pureTest(args ...interface{}) {} func textTest(args ...interface{}) {} func parensFilterTest(args ...interface{}) {} func importsTest(args ...interface{}) {} +func fileTest(args ...interface{}) {} func random() int { return 42 diff --git a/dsl/fluent/dsl.go b/dsl/fluent/dsl.go index 597bee79..1581df4d 100644 --- a/dsl/fluent/dsl.go +++ b/dsl/fluent/dsl.go @@ -113,8 +113,17 @@ type MatchedText string // Matches reports whether the text matches the given regexp pattern. func (MatchedText) Matches(pattern string) bool { return boolResult } +// String represents an arbitrary string-typed data. +type String string + +// Matches reports whether a string matches the given regexp pattern. +func (String) Matches(pattern string) bool { return boolResult } + // File represents the current Go source file. -type File struct{} +type File struct { + // Name is a file base name. + Name String +} // Imports reports whether the current file imports the given path. func (File) Imports(path string) bool { return boolResult } diff --git a/dslgen/dsl_sources.go b/dslgen/dsl_sources.go index fbd667e4..181e1579 100644 --- a/dslgen/dsl_sources.go +++ b/dslgen/dsl_sources.go @@ -1,3 +1,3 @@ package dslgen -var Fluent = []byte("package fluent\n\n// Matcher is a main API group-level entry point.\n// It's used to define and configure the group rules.\n// It also represents a map of all rule-local variables.\ntype Matcher map[string]Var\n\n// Import loads given package path into a rule group imports table.\n//\n// That table is used during the rules compilation.\n//\n// The table has the following effect on the rules:\n//\t* For type expressions, it's used to resolve the\n//\t full package paths of qualified types, like `foo.Bar`.\n//\t If Import(`a/b/foo`) is called, `foo.Bar` will match\n//\t `a/b/foo.Bar` type during the pattern execution.\nfunc (m Matcher) Import(pkgPath string) {}\n\n// Match specifies a set of patterns that match a rule being defined.\n// Pattern matching succeeds if at least 1 pattern matches.\n//\n// If none of the given patterns matched, rule execution stops.\nfunc (m Matcher) Match(pattern string, alternatives ...string) Matcher {\n\treturn m\n}\n\n// Where applies additional constraint to a match.\n// If a given cond is not satisfied, a match is rejected and\n// rule execution stops.\nfunc (m Matcher) Where(cond bool) Matcher {\n\treturn m\n}\n\n// Report prints a message if associated rule match is successful.\n//\n// A message is a string that can contain interpolated expressions.\n// For every matched variable it's possible to interpolate\n// their printed representation into the message text with $.\n// An entire match can be addressed with $$.\nfunc (m Matcher) Report(message string) Matcher {\n\treturn m\n}\n\n// Suggest assigns a quickfix suggestion for the matched code.\nfunc (m Matcher) Suggest(suggestion string) Matcher {\n\treturn m\n}\n\n// At binds the reported node to a named submatch.\n// If no explicit location is given, the outermost node ($$) is used.\nfunc (m Matcher) At(v Var) Matcher {\n\treturn m\n}\n\n// File returns the current file context.\nfunc (m Matcher) File() File { return File{} }\n\n// Var is a pattern variable that describes a named submatch.\ntype Var struct {\n\t// Pure reports whether expr matched by var is side-effect-free.\n\tPure bool\n\n\t// Const reports whether expr matched by var is a constant value.\n\tConst bool\n\n\t// Addressable reports whether the corresponding expression is addressable.\n\t// See https://golang.org/ref/spec#Address_operators.\n\tAddressable bool\n\n\t// Type is a type of a matched expr.\n\t//\n\t// For function call expressions, a type is a function result type,\n\t// but for a function expression itself it's a *types.Signature.\n\t//\n\t// Suppose we have a `a.b()` expression:\n\t//\t`$x()` m[\"x\"].Type is `a.b` function type\n\t//\t`$x` m[\"x\"].Type is `a.b()` function call result type\n\tType ExprType\n\n\t// Text is a captured node text as in the source code.\n\tText MatchedText\n}\n\n// ExprType describes a type of a matcher expr.\ntype ExprType struct {\n\t// Size represents expression type size in bytes.\n\tSize int\n}\n\n// Underlying returns expression type underlying type.\n// See https://golang.org/pkg/go/types/#Type Underlying() method documentation.\n// Read https://golang.org/ref/spec#Types section to learn more about underlying types.\nfunc (ExprType) Underlying() ExprType { return underlyingType }\n\n// AssignableTo reports whether a type is assign-compatible with a given type.\n// See https://golang.org/pkg/go/types/#AssignableTo.\nfunc (ExprType) AssignableTo(typ string) bool { return boolResult }\n\n// ConvertibleTo reports whether a type is conversible to a given type.\n// See https://golang.org/pkg/go/types/#ConvertibleTo.\nfunc (ExprType) ConvertibleTo(typ string) bool { return boolResult }\n\n// Implements reports whether a type implements a given interface.\n// See https://golang.org/pkg/go/types/#Implements.\nfunc (ExprType) Implements(typ string) bool { return boolResult }\n\n// Is reports whether a type is identical to a given type.\nfunc (ExprType) Is(typ string) bool { return boolResult }\n\n// MatchedText represents a source text associated with a matched node.\ntype MatchedText string\n\n// Matches reports whether the text matches the given regexp pattern.\nfunc (MatchedText) Matches(pattern string) bool { return boolResult }\n\n// File represents the current Go source file.\ntype File struct{}\n\n// Imports reports whether the current file imports the given path.\nfunc (File) Imports(path string) bool { return boolResult }\n\n\n\nvar boolResult bool\n\nvar underlyingType ExprType\n\n") +var Fluent = []byte("package fluent\n\n// Matcher is a main API group-level entry point.\n// It's used to define and configure the group rules.\n// It also represents a map of all rule-local variables.\ntype Matcher map[string]Var\n\n// Import loads given package path into a rule group imports table.\n//\n// That table is used during the rules compilation.\n//\n// The table has the following effect on the rules:\n//\t* For type expressions, it's used to resolve the\n//\t full package paths of qualified types, like `foo.Bar`.\n//\t If Import(`a/b/foo`) is called, `foo.Bar` will match\n//\t `a/b/foo.Bar` type during the pattern execution.\nfunc (m Matcher) Import(pkgPath string) {}\n\n// Match specifies a set of patterns that match a rule being defined.\n// Pattern matching succeeds if at least 1 pattern matches.\n//\n// If none of the given patterns matched, rule execution stops.\nfunc (m Matcher) Match(pattern string, alternatives ...string) Matcher {\n\treturn m\n}\n\n// Where applies additional constraint to a match.\n// If a given cond is not satisfied, a match is rejected and\n// rule execution stops.\nfunc (m Matcher) Where(cond bool) Matcher {\n\treturn m\n}\n\n// Report prints a message if associated rule match is successful.\n//\n// A message is a string that can contain interpolated expressions.\n// For every matched variable it's possible to interpolate\n// their printed representation into the message text with $.\n// An entire match can be addressed with $$.\nfunc (m Matcher) Report(message string) Matcher {\n\treturn m\n}\n\n// Suggest assigns a quickfix suggestion for the matched code.\nfunc (m Matcher) Suggest(suggestion string) Matcher {\n\treturn m\n}\n\n// At binds the reported node to a named submatch.\n// If no explicit location is given, the outermost node ($$) is used.\nfunc (m Matcher) At(v Var) Matcher {\n\treturn m\n}\n\n// File returns the current file context.\nfunc (m Matcher) File() File { return File{} }\n\n// Var is a pattern variable that describes a named submatch.\ntype Var struct {\n\t// Pure reports whether expr matched by var is side-effect-free.\n\tPure bool\n\n\t// Const reports whether expr matched by var is a constant value.\n\tConst bool\n\n\t// Addressable reports whether the corresponding expression is addressable.\n\t// See https://golang.org/ref/spec#Address_operators.\n\tAddressable bool\n\n\t// Type is a type of a matched expr.\n\t//\n\t// For function call expressions, a type is a function result type,\n\t// but for a function expression itself it's a *types.Signature.\n\t//\n\t// Suppose we have a `a.b()` expression:\n\t//\t`$x()` m[\"x\"].Type is `a.b` function type\n\t//\t`$x` m[\"x\"].Type is `a.b()` function call result type\n\tType ExprType\n\n\t// Text is a captured node text as in the source code.\n\tText MatchedText\n}\n\n// ExprType describes a type of a matcher expr.\ntype ExprType struct {\n\t// Size represents expression type size in bytes.\n\tSize int\n}\n\n// Underlying returns expression type underlying type.\n// See https://golang.org/pkg/go/types/#Type Underlying() method documentation.\n// Read https://golang.org/ref/spec#Types section to learn more about underlying types.\nfunc (ExprType) Underlying() ExprType { return underlyingType }\n\n// AssignableTo reports whether a type is assign-compatible with a given type.\n// See https://golang.org/pkg/go/types/#AssignableTo.\nfunc (ExprType) AssignableTo(typ string) bool { return boolResult }\n\n// ConvertibleTo reports whether a type is conversible to a given type.\n// See https://golang.org/pkg/go/types/#ConvertibleTo.\nfunc (ExprType) ConvertibleTo(typ string) bool { return boolResult }\n\n// Implements reports whether a type implements a given interface.\n// See https://golang.org/pkg/go/types/#Implements.\nfunc (ExprType) Implements(typ string) bool { return boolResult }\n\n// Is reports whether a type is identical to a given type.\nfunc (ExprType) Is(typ string) bool { return boolResult }\n\n// MatchedText represents a source text associated with a matched node.\ntype MatchedText string\n\n// Matches reports whether the text matches the given regexp pattern.\nfunc (MatchedText) Matches(pattern string) bool { return boolResult }\n\n// String represents an arbitrary string-typed data.\ntype String string\n\n// Matches reports whether a string matches the given regexp pattern.\nfunc (String) Matches(pattern string) bool { return boolResult }\n\n// File represents the current Go source file.\ntype File struct {\n\t// Name is a file base name.\n\tName String\n}\n\n// Imports reports whether the current file imports the given path.\nfunc (File) Imports(path string) bool { return boolResult }\n\n\n\nvar boolResult bool\n\nvar underlyingType ExprType\n\n") diff --git a/ruleguard/gorule.go b/ruleguard/gorule.go index 5ab89c8f..07bbb5c7 100644 --- a/ruleguard/gorule.go +++ b/ruleguard/gorule.go @@ -23,8 +23,9 @@ type goRule struct { } type matchFilter struct { - fileImports []string - sub map[string]submatchFilter + fileImports []string + filenamePred func(string) bool + sub map[string]submatchFilter } type submatchFilter struct { diff --git a/ruleguard/parser.go b/ruleguard/parser.go index 6be21f09..0581824c 100644 --- a/ruleguard/parser.go +++ b/ruleguard/parser.go @@ -10,6 +10,7 @@ import ( "go/types" "io" "path" + "path/filepath" "regexp" "strconv" @@ -453,18 +454,6 @@ func (p *rulesParser) walkFilter(dst *matchFilter, e ast.Expr, negate bool) erro return p.walkFilter(dst, e.X, negate) } - // File-related filters. - fileOperand := p.toFileFilterOperand(e) - switch fileOperand.path { - case "Imports": - pkgPath, ok := p.toStringValue(fileOperand.args[0]) - if !ok { - return p.errorf(fileOperand.args[0], "expected a string literal argument") - } - dst.fileImports = append(dst.fileImports, pkgPath) - return nil - } - // TODO(quasilyte): refactor and extend. operand := p.toFilterOperand(e) args := operand.args @@ -473,6 +462,28 @@ func (p *rulesParser) walkFilter(dst *matchFilter, e ast.Expr, negate bool) erro switch operand.path { default: return p.errorf(e, "%s is not a valid filter expression", sprintNode(p.fset, e)) + case "File.Imports": + pkgPath, ok := p.toStringValue(args[0]) + if !ok { + return p.errorf(args[0], "expected a string literal argument") + } + dst.fileImports = append(dst.fileImports, pkgPath) + return nil + case "File.Name.Matches": + patternString, ok := p.toStringValue(args[0]) + if !ok { + return p.errorf(args[0], "expected a string literal argument") + } + re, err := regexp.Compile(patternString) + if err != nil { + return p.errorf(args[0], "parse regexp: %v", err) + } + wantMatched := !negate + dst.filenamePred = func(filename string) bool { + return wantMatched == re.MatchString(filepath.Base(filename)) + } + return nil + case "Pure": if negate { filter.pure = bool3false @@ -635,23 +646,6 @@ func (p *rulesParser) toStringValue(x ast.Node) (string, bool) { return "", false } -func (p *rulesParser) toFileFilterOperand(e ast.Expr) filterOperand { - var o filterOperand - - call, ok := e.(*ast.CallExpr) - if !ok { - return o - } - selector, ok := call.Fun.(*ast.SelectorExpr) - if !ok { - return o - } - - o.args = call.Args - o.path = selector.Sel.Name - return o -} - func (p *rulesParser) toFilterOperand(e ast.Expr) filterOperand { var o filterOperand @@ -676,6 +670,9 @@ func (p *rulesParser) toFilterOperand(e ast.Expr) filterOperand { } e = selector.X } + + o.path = path + indexing, ok := e.(*ast.IndexExpr) if !ok { return o @@ -684,14 +681,10 @@ func (p *rulesParser) toFilterOperand(e ast.Expr) filterOperand { if !ok { return o } - indexString, ok := p.toStringValue(indexing.Index) - if !ok { - return o - } - o.mapName = mapIdent.Name + indexString, _ := p.toStringValue(indexing.Index) o.varName = indexString - o.path = path + return o } diff --git a/ruleguard/runner.go b/ruleguard/runner.go index 6fcf53de..f0560290 100644 --- a/ruleguard/runner.go +++ b/ruleguard/runner.go @@ -97,6 +97,16 @@ func (rr *rulesRunner) handleMatch(rule goRule, m gogrep.MatchData) bool { } } + // TODO(quasilyte): do not run filename check for every match. + // Exclude rules for the file that will never match due to the + // file-scoped filters. Same goes for the fileImports filter + // and ideas proposed in #78. Most rules do not have file-scoped + // filters, so we don't loose much here, but we can optimize + // this file filters in the future. + if rule.filter.filenamePred != nil && !rule.filter.filenamePred(rr.filename) { + return false + } + for name, node := range m.Values { var expr ast.Expr switch node := node.(type) {