Skip to content

Commit

Permalink
ruleguard,dsl: add File.Name filter support
Browse files Browse the repository at this point in the history
File.Name is a file *base* name property.

We may add File.FullName (or Path) property later if anyone needs it.

Signed-off-by: Iskander Sharipov <[email protected]>
  • Loading branch information
quasilyte committed Oct 27, 2020
1 parent 4d26bb9 commit 39805ac
Show file tree
Hide file tree
Showing 10 changed files with 79 additions and 45 deletions.
5 changes: 5 additions & 0 deletions analyzer/testdata/src/filtertest/f1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions analyzer/testdata/src/filtertest/foo_file1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package filtertest

func _() {
fileTest("with foo prefix") // want `YES`
}
5 changes: 5 additions & 0 deletions analyzer/testdata/src/filtertest/foo_file2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package filtertest

func _() {
fileTest("with foo prefix") // want `YES`
}
8 changes: 8 additions & 0 deletions analyzer/testdata/src/filtertest/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
}
1 change: 1 addition & 0 deletions analyzer/testdata/src/filtertest/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion dsl/fluent/dsl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
2 changes: 1 addition & 1 deletion dslgen/dsl_sources.go
Original file line number Diff line number Diff line change
@@ -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 $<name>.\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 $<name>.\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")
5 changes: 3 additions & 2 deletions ruleguard/gorule.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
72 changes: 31 additions & 41 deletions ruleguard/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"go/types"
"io"
"path"
"path/filepath"
"regexp"
"strconv"

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -676,21 +670,17 @@ func (p *rulesParser) toFilterOperand(e ast.Expr) filterOperand {
}
e = selector.X
}

indexing, ok := e.(*ast.IndexExpr)
if !ok {
return o
}
mapIdent, ok := indexing.X.(*ast.Ident)
if !ok {
return o
}
indexString, ok := p.toStringValue(indexing.Index)
if !ok {
return o
if ok {
mapIdent, ok := indexing.X.(*ast.Ident)
o.mapName = mapIdent.Name
if ok {
indexString, _ := p.toStringValue(indexing.Index)
o.varName = indexString
}
}

o.mapName = mapIdent.Name
o.varName = indexString
o.path = path
return o
}
Expand Down
10 changes: 10 additions & 0 deletions ruleguard/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 39805ac

Please sign in to comment.