Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ruleguard: filter parsing improvements #142

Merged
merged 1 commit into from
Dec 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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