Skip to content

Commit

Permalink
ruleguard,dsl: add experimental support for const value filters
Browse files Browse the repository at this point in the history
Added Value expression field that can be used to check the submatch value.
Since `go/types` has some const-folding out-of-the-box, we get it too.

Right now only `int` value types are supported, but we could extend the
set of types later.

This is a proof-of-concept that can be useful in #96.

Signed-off-by: Iskander Sharipov <quasilyte@gmail.com>
  • Loading branch information
quasilyte committed Oct 31, 2020
1 parent f29b6e3 commit 1bc1577
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 1 deletion.
10 changes: 10 additions & 0 deletions dsl/fluent/dsl.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ type Var struct {
// Const reports whether expr matched by var is a constant value.
Const bool

// Value is a compile-time computable value of the expression.
Value ExprValue

// Addressable reports whether the corresponding expression is addressable.
// See https://golang.org/ref/spec#Address_operators.
Addressable bool
Expand All @@ -81,6 +84,13 @@ type Var struct {
Text MatchedText
}

// ExprValue describes a compile-time computable value of a matched expr.
type ExprValue struct{}

// Int returns compile-time computable int value of the expression.
// If value can't be computed, condition will fail.
func (ExprValue) Int() int { return intResult }

// ExprType describes a type of a matcher expr.
type ExprType struct {
// Size represents expression type size in bytes.
Expand Down
1 change: 1 addition & 0 deletions dsl/fluent/internal.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package fluent

var boolResult bool
var intResult int

var underlyingType ExprType
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// 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\t// PkgPath is a file package path.\n\t// Examples: \"io/ioutil\", \"strings\", \"github.com/quasilyte/go-ruleguard/dsl/fluent\".\n\tPkgPath 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")
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// Value is a compile-time computable value of the expression.\n\tValue ExprValue\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// ExprValue describes a compile-time computable value of a matched expr.\ntype ExprValue struct{}\n\n// Int returns compile-time computable int value of the expression.\n// If value can't be computed, condition will fail.\nfunc (ExprValue) Int() int { return intResult }\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\t// PkgPath is a file package path.\n\t// Examples: \"io/ioutil\", \"strings\", \"github.com/quasilyte/go-ruleguard/dsl/fluent\".\n\tPkgPath 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\nvar intResult int\n\nvar underlyingType ExprType\n\n")
25 changes: 25 additions & 0 deletions ruleguard/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,31 @@ func TestDebug(t *testing.T) {
` $x int: int(10)`,
},
},

`m.Match("$x + $_").Where(m["x"].Value.Int() >= 10)`: {
`sink = 20 + 1`: nil,

// OK: $x is const-folded.
`sink = (2 << 3) + 1`: nil,

// Not an int.
`sink = "20" + "x"`: {
`input.go:4: [rules.go:5] rejected by m["x"].Value.Int() >= 10`,
` $x untyped string: "20"`,
},

// Not a const value.
`sink = f().(int) + 0`: {
`input.go:4: [rules.go:5] rejected by m["x"].Value.Int() >= 10`,
` $x int: f().(int)`,
},

// Less than 10.
`sink = 4 + 1`: {
`input.go:4: [rules.go:5] rejected by m["x"].Value.Int() >= 10`,
` $x untyped int: 4`,
},
},
}

exprToRules := func(s string) *GoRuleSet {
Expand Down
14 changes: 14 additions & 0 deletions ruleguard/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,20 @@ func (p *rulesParser) walkFilter(dst *matchFilter, e ast.Expr, negate bool) erro
})
return nil
}
if operand.path == "Value.Int" && y != nil {
p.appendSubFilter(dst, e, operand.varName, func(params *nodeFilterParams) bool {
tv := params.ctx.Types.Types[params.n]
x := tv.Value
if x == nil {
return false // The value is unknown
}
if x.Kind() != constant.Int {
return false // It's not int
}
return expectedResult == constant.Compare(x, e.Op, y)
})
return nil
}
if operand.path == "Text" && y != nil {
p.appendSubFilter(dst, e, operand.varName, func(params *nodeFilterParams) bool {
s := params.nodeText(params.n)
Expand Down

0 comments on commit 1bc1577

Please sign in to comment.