Skip to content

Commit

Permalink
ruleguard: filter parsing improvements
Browse files Browse the repository at this point in the history
Refactored Where() filter parsing code.

Implemented || operator while at it.

Fixes #115 !(A && B) conditions now work properly
Fixes #26  A || B    conditions are now implemented
  • Loading branch information
quasilyte committed Dec 20, 2020
1 parent 1364919 commit b787354
Show file tree
Hide file tree
Showing 7 changed files with 494 additions and 251 deletions.
13 changes: 13 additions & 0 deletions analyzer/testdata/src/regression/issue115.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package regression

func testIssue115() {
intFunc := func() int { return 19 }
stringFunc := func() string { return "19" }

println(13)
println(43 + 5)

println("foo") // want `\Q"foo" is not a constexpr int`
println(intFunc()) // want `\QintFunc() is not a constexpr int`
println(stringFunc()) // want `\QstringFunc() is not a constexpr int`
}
6 changes: 6 additions & 0 deletions analyzer/testdata/src/regression/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ func issue72(m fluent.Matcher) {
`fmt.Sprintf("%s<%s>", $name, $email)`).
Report("use net/mail Address.String() instead of fmt.Sprintf()")
}

func issue115(m fluent.Matcher) {
m.Match(`println($x)`).
Where(!(m["x"].Const && m["x"].Type.Is("int"))).
Report("$x is not a constexpr int")
}
53 changes: 51 additions & 2 deletions ruleguard/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,11 @@ func TestDebug(t *testing.T) {
},
},

// TODO(quasilyte): don't lose "!" in the debug output.
`m.Match("$x + $_").Where(!m["x"].Type.Is("int"))`: {
`sink = "a" + "b"`: nil,

`sink = int(10) + 20`: {
`input.go:4: [rules.go:5] rejected by m["x"].Type.Is("int")`,
`input.go:4: [rules.go:5] rejected by !m["x"].Type.Is("int")`,
` $x int: int(10)`,
},
},
Expand Down Expand Up @@ -91,6 +90,56 @@ func TestDebug(t *testing.T) {
` $x interface{}: f((10))`,
},
},

// When debugging OR, the last alternative will be reported as the failure reason,
// although it should be obvious that all operands are falsy.
// We don't return the entire OR expression as a reason to avoid the output cluttering.
`m.Match("_ = $x").Where(m["x"].Type.Is("int") || m["x"].Type.Is("string"))`: {
`_ = ""`: nil,
`_ = 10`: nil,

`_ = []int{}`: {
`input.go:4: [rules.go:5] rejected by m["x"].Type.Is("string")`,
` $x []int: []int{}`,
},

`_ = int32(0)`: {
`input.go:4: [rules.go:5] rejected by m["x"].Type.Is("string")`,
` $x int32: int32(0)`,
},
},

// Using 3 operands for || and different ()-groupings.
`m.Match("_ = $x").Where(m["x"].Type.Is("int") || m["x"].Type.Is("string") || m["x"].Text == "f()")`: {
`_ = ""`: nil,
`_ = 10`: nil,
`_ = f()`: nil,

`_ = []string{"x"}`: {
`input.go:4: [rules.go:5] rejected by m["x"].Text == "f()"`,
` $x []string: []string{"x"}`,
},
},
`m.Match("_ = $x").Where(m["x"].Type.Is("int") || (m["x"].Type.Is("string") || m["x"].Text == "f()"))`: {
`_ = ""`: nil,
`_ = 10`: nil,
`_ = f()`: nil,

`_ = []string{"x"}`: {
`input.go:4: [rules.go:5] rejected by m["x"].Text == "f()"`,
` $x []string: []string{"x"}`,
},
},
`m.Match("_ = $x").Where((m["x"].Type.Is("int") || m["x"].Type.Is("string")) || m["x"].Text == "f()")`: {
`_ = ""`: nil,
`_ = 10`: nil,
`_ = f()`: nil,

`_ = []string{"x"}`: {
`input.go:4: [rules.go:5] rejected by m["x"].Text == "f()"`,
` $x []string: []string{"x"}`,
},
},
}

exprToRules := func(s string) *GoRuleSet {
Expand Down
246 changes: 246 additions & 0 deletions ruleguard/filters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package ruleguard

import (
"go/ast"
"go/constant"
"go/token"
"go/types"
"path/filepath"
"regexp"

"github.com/quasilyte/go-ruleguard/ruleguard/typematch"
)

const filterSuccess = matchFilterResult("")

func filterFailure(reason string) matchFilterResult {
return matchFilterResult(reason)
}

func makeNotFilter(src string, x matchFilter) filterFunc {
return func(params *filterParams) matchFilterResult {
if x.fn(params).Matched() {
return matchFilterResult(src)
}
return ""
}
}

func makeAndFilter(lhs, rhs matchFilter) filterFunc {
return func(params *filterParams) matchFilterResult {
if lhsResult := lhs.fn(params); !lhsResult.Matched() {
return lhsResult
}
return rhs.fn(params)
}
}

func makeOrFilter(lhs, rhs matchFilter) filterFunc {
return func(params *filterParams) matchFilterResult {
if lhsResult := lhs.fn(params); lhsResult.Matched() {
return filterSuccess
}
return rhs.fn(params)
}
}

func makeFileImportsFilter(src, pkgPath string) filterFunc {
return func(params *filterParams) matchFilterResult {
_, imported := params.imports[pkgPath]
if imported {
return filterSuccess
}
return filterFailure(src)
}
}

func makeFilePkgPathMatchesFilter(src string, re *regexp.Regexp) filterFunc {
return func(params *filterParams) matchFilterResult {
pkgPath := params.ctx.Pkg.Path()
if re.MatchString(pkgPath) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeFileNameMatchesFilter(src string, re *regexp.Regexp) filterFunc {
return func(params *filterParams) matchFilterResult {
if re.MatchString(filepath.Base(params.filename)) {
return filterSuccess
}
return filterFailure(src)
}
}

func makePureFilter(src, varname string) filterFunc {
return func(params *filterParams) matchFilterResult {
n := params.subExpr(varname)
if isPure(params.ctx.Types, n) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeConstFilter(src, varname string) filterFunc {
return func(params *filterParams) matchFilterResult {
n := params.subExpr(varname)
if isConstant(params.ctx.Types, n) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeAddressableFilter(src, varname string) filterFunc {
return func(params *filterParams) matchFilterResult {
n := params.subExpr(varname)
if isAddressable(params.ctx.Types, n) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeTypeImplementsFilter(src, varname string, iface *types.Interface) filterFunc {
return func(params *filterParams) matchFilterResult {
typ := params.typeofNode(params.subExpr(varname))
if types.Implements(typ, iface) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeTypeIsFilter(src, varname string, underlying bool, pat *typematch.Pattern) filterFunc {
if underlying {
return func(params *filterParams) matchFilterResult {
typ := params.typeofNode(params.subExpr(varname)).Underlying()
if pat.MatchIdentical(typ) {
return filterSuccess
}
return filterFailure(src)
}
}
return func(params *filterParams) matchFilterResult {
typ := params.typeofNode(params.subExpr(varname))
if pat.MatchIdentical(typ) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeTypeConvertibleToFilter(src, varname string, dstType types.Type) filterFunc {
return func(params *filterParams) matchFilterResult {
typ := params.typeofNode(params.subExpr(varname))
if types.ConvertibleTo(typ, dstType) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeTypeAssignableToFilter(src, varname string, dstType types.Type) filterFunc {
return func(params *filterParams) matchFilterResult {
typ := params.typeofNode(params.subExpr(varname))
if types.AssignableTo(typ, dstType) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeTypeSizeConstFilter(src, varname string, op token.Token, rhsValue constant.Value) filterFunc {
return func(params *filterParams) matchFilterResult {
typ := params.typeofNode(params.subExpr(varname))
lhsValue := constant.MakeInt64(params.ctx.Sizes.Sizeof(typ))
if constant.Compare(lhsValue, op, rhsValue) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeValueIntConstFilter(src, varname string, op token.Token, rhsValue constant.Value) filterFunc {
return func(params *filterParams) matchFilterResult {
lhsValue := intValueOf(params.ctx.Types, params.subExpr(varname))
if lhsValue == nil {
return filterFailure(src) // The value is unknown
}
if constant.Compare(lhsValue, op, rhsValue) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeValueIntFilter(src, varname string, op token.Token, rhsVarname string) filterFunc {
return func(params *filterParams) matchFilterResult {
lhsValue := intValueOf(params.ctx.Types, params.subExpr(varname))
if lhsValue == nil {
return filterFailure(src)
}
rhsValue := intValueOf(params.ctx.Types, params.subExpr(rhsVarname))
if rhsValue == nil {
return filterFailure(src)
}
if constant.Compare(lhsValue, op, rhsValue) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeTextConstFilter(src, varname string, op token.Token, rhsValue constant.Value) filterFunc {
return func(params *filterParams) matchFilterResult {
s := params.nodeText(params.subExpr(varname))
lhsValue := constant.MakeString(string(s))
if constant.Compare(lhsValue, op, rhsValue) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeTextFilter(src, varname string, op token.Token, rhsVarname string) filterFunc {
return func(params *filterParams) matchFilterResult {
s1 := params.nodeText(params.subExpr(varname))
lhsValue := constant.MakeString(string(s1))
s2 := params.nodeText(params.values[rhsVarname])
rhsValue := constant.MakeString(string(s2))
if constant.Compare(lhsValue, op, rhsValue) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeTextMatchesFilter(src, varname string, re *regexp.Regexp) filterFunc {
return func(params *filterParams) matchFilterResult {
if re.Match(params.nodeText(params.subExpr(varname))) {
return filterSuccess
}
return filterFailure(src)
}
}

func makeNodeIsFilter(src, varname string, cat nodeCategory) filterFunc {
return func(params *filterParams) matchFilterResult {
n := params.subExpr(varname)
var matched bool
switch cat {
case nodeExpr:
_, matched = n.(ast.Expr)
case nodeStmt:
_, matched = n.(ast.Stmt)
default:
matched = (cat == categorizeNode(n))
}
if matched {
return filterSuccess
}
return filterFailure(src)
}
}
Loading

0 comments on commit b787354

Please sign in to comment.