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: implement dsl Do() function #379

Merged
merged 1 commit into from Feb 13, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions analyzer/analyzer.go
Expand Up @@ -54,13 +54,13 @@ var (
flagGoVersion string

flagDebug string
flagDebugFilter string
flagDebugFunc string
flagDebugImports bool
flagDebugEnableDisable bool
)

func init() {
Analyzer.Flags.StringVar(&flagDebugFilter, "debug-filter", "", "[experimental!] enable debug for the specified filter function")
Analyzer.Flags.StringVar(&flagDebugFunc, "debug-func", "", "[experimental!] enable debug for the specified bytecode function")
Analyzer.Flags.StringVar(&flagDebug, "debug-group", "", "[experimental!] enable debug for the specified matcher function")
Analyzer.Flags.BoolVar(&flagDebugImports, "debug-imports", false, "[experimental!] enable debug for rules compile-time package lookups")
Analyzer.Flags.BoolVar(&flagDebugEnableDisable, "debug-enable-disable", false, "[experimental!] enable debug for -enable/-disable related info")
Expand Down Expand Up @@ -189,7 +189,7 @@ func newEngine() (*ruleguard.Engine, error) {

ctx := &ruleguard.LoadContext{
Fset: fset,
DebugFilter: flagDebugFilter,
DebugFunc: flagDebugFunc,
DebugImports: flagDebugImports,
DebugPrint: debugPrint,
GroupFilter: func(g *ruleguard.GoRuleGroup) bool {
Expand Down
1 change: 1 addition & 0 deletions analyzer/analyzer_test.go
Expand Up @@ -39,6 +39,7 @@ var tests = []struct {
{name: "regression"},
{name: "testvendored"},
{name: "quasigo"},
{name: "do"},
{name: "matching"},
{name: "dgryski"},
{name: "comments"},
Expand Down
76 changes: 76 additions & 0 deletions analyzer/testdata/src/do/rules.go
@@ -0,0 +1,76 @@
//go:build ignore
// +build ignore

package gorules

import (
"fmt"
"strings"

"github.com/quasilyte/go-ruleguard/dsl"
"github.com/quasilyte/go-ruleguard/dsl/types"
)

func reportHello(ctx *dsl.DoContext) {
ctx.SetReport("Hello, World!")
}

func suggestHello(ctx *dsl.DoContext) {
ctx.SetSuggest("Hello, World!")
}

func reportX(ctx *dsl.DoContext) {
ctx.SetReport(ctx.Var("x").Text())
}

func unquote(s string) string {
return s[1 : len(s)-1]
}

func reportTrimPrefix(ctx *dsl.DoContext) {
s := unquote(ctx.Var("x").Text())
prefix := unquote(ctx.Var("y").Text())
ctx.SetReport(strings.TrimPrefix(s, prefix))
}

func reportEmptyString(ctx *dsl.DoContext) {
x := ctx.Var("x")
if x.Text() == `""` {
ctx.SetReport("empty string")
} else {
ctx.SetReport("non-empty string")
}
}

func reportType(ctx *dsl.DoContext) {
ctx.SetReport(ctx.Var("x").Type().String())
}

func reportTypesIdentical(ctx *dsl.DoContext) {
xtype := ctx.Var("x").Type()
ytype := ctx.Var("y").Type()
ctx.SetReport(fmt.Sprintf("%v", types.Identical(xtype, ytype)))
}

func testRules(m dsl.Matcher) {
m.Match(`test("custom report")`).
Do(reportHello)

m.Match(`test("custom suggest")`).
Do(suggestHello)

m.Match(`test("var text", $x)`).
Do(reportX)

m.Match(`test("trim prefix", $x, $y)`).
Do(reportTrimPrefix)

m.Match(`test("report empty string", $x)`).
Do(reportEmptyString)

m.Match(`test("report type", $x)`).
Do(reportType)

m.Match(`test("types identical", $x, $y)`).
Do(reportTypesIdentical)
}
30 changes: 30 additions & 0 deletions analyzer/testdata/src/do/target.go
@@ -0,0 +1,30 @@
package do

func Example() {
test("custom report") // want `\QHello, World!`
test("custom suggest") // want `\Qsuggestion: Hello, World!`

var x int
test("var text", "str") // want `\Q"str"`
test("var text", x+1) // want `\Qx+1`

test("trim prefix", "hello, world", "hello") // want `\Q, world`
test("trim prefix", "hello, world", "hello, ") // want `\Qworld`
test("trim prefix", "hello, world", "???") // want `\Qhello, world`

test("report empty string", "") // want `\Qempty string`
test("report empty string", "example") // want `\Qnon-empty string`

test("report type", 13) // want `\Qint`
test("report type", "str") // want `\Qstring`
test("report type", []int{1}) // want `\Q[]int`
test("report type", x) // want `\Qint`
test("report type", &x) // want `\Q*int`

test("types identical", 1, 1) // want `true`
test("types identical", x, x) // want `true`
test("types identical", x, &x) // want `false`
test("types identical", 1, 1.5) // want `false`
}

func test(args ...interface{}) {}
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -5,7 +5,7 @@ go 1.17
require (
github.com/go-toolsmith/astcopy v1.0.0
github.com/google/go-cmp v0.5.6
github.com/quasilyte/go-ruleguard/dsl v0.3.16
github.com/quasilyte/go-ruleguard/dsl v0.3.17
github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71
github.com/quasilyte/gogrep v0.0.0-20220120141003-628d8b3623b5
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -12,6 +12,8 @@ github.com/quasilyte/go-ruleguard v0.3.1-0.20210203134552-1b5a410e1cc8/go.mod h1
github.com/quasilyte/go-ruleguard/dsl v0.3.0/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/quasilyte/go-ruleguard/dsl v0.3.16 h1:yJtIpd4oyNS+/c/gKqxNwoGO9+lPOsy1A4BzKjJRcrI=
github.com/quasilyte/go-ruleguard/dsl v0.3.16/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/quasilyte/go-ruleguard/dsl v0.3.17 h1:L5xf3nifnRIdYe9vyMuY2sDnZHIgQol/fDq74FQz7ZY=
github.com/quasilyte/go-ruleguard/dsl v0.3.17/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc=
github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71 h1:CNooiryw5aisadVfzneSZPswRWvnVW8hF1bS/vo8ReI=
github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50=
Expand Down
6 changes: 6 additions & 0 deletions ruleguard/engine.go
Expand Up @@ -17,6 +17,9 @@ import (
"github.com/quasilyte/go-ruleguard/internal/goenv"
"github.com/quasilyte/go-ruleguard/ruleguard/ir"
"github.com/quasilyte/go-ruleguard/ruleguard/quasigo"
"github.com/quasilyte/go-ruleguard/ruleguard/quasigo/stdlib/qfmt"
"github.com/quasilyte/go-ruleguard/ruleguard/quasigo/stdlib/qstrconv"
"github.com/quasilyte/go-ruleguard/ruleguard/quasigo/stdlib/qstrings"
"github.com/quasilyte/go-ruleguard/ruleguard/typematch"
"github.com/quasilyte/stdinfo"
)
Expand Down Expand Up @@ -141,6 +144,9 @@ type engineState struct {

func newEngineState() *engineState {
env := quasigo.NewEnv()
qstrings.ImportAll(env)
qstrconv.ImportAll(env)
qfmt.ImportAll(env)
state := &engineState{
env: env,
pkgCache: make(map[string]*types.Package),
Expand Down
8 changes: 7 additions & 1 deletion ruleguard/gorule.go
Expand Up @@ -38,6 +38,7 @@ type goRule struct {
location string
suggestion string
filter matchFilter
do *quasigo.Func
}

type matchFilterResult string
Expand Down Expand Up @@ -66,14 +67,19 @@ type filterParams struct {
match matchData
nodePath *nodePath

nodeText func(n ast.Node) []byte
nodeText func(n ast.Node) []byte
nodeString func(n ast.Node) string

deadcode bool

currentFunc *ast.FuncDecl

// varname is set only for custom filters before bytecode function is called.
varname string

// Both of these are Do() function related fields.
reportString string
suggestString string
}

func (params *filterParams) subNode(name string) ast.Node {
Expand Down
1 change: 1 addition & 0 deletions ruleguard/ir/ir.go
Expand Up @@ -51,6 +51,7 @@ type Rule struct {

ReportTemplate string
SuggestTemplate string
DoFuncName string

WhereExpr FilterExpr

Expand Down
14 changes: 11 additions & 3 deletions ruleguard/ir_loader.go
Expand Up @@ -178,12 +178,12 @@ func (l *irLoader) compileFilterFuncs(filename string, irfile *ir.File) error {
buf.WriteString("package gorules\n")
buf.WriteString("import \"github.com/quasilyte/go-ruleguard/dsl\"\n")
buf.WriteString("import \"github.com/quasilyte/go-ruleguard/dsl/types\"\n")
buf.WriteString("type _ = dsl.Matcher\n")
buf.WriteString("type _ = types.Type\n")
for _, src := range irfile.CustomDecls {
buf.WriteString(src)
buf.WriteString("\n")
}
buf.WriteString("type _ = dsl.Matcher\n")
buf.WriteString("type _ = types.Type\n")

fset := token.NewFileSet()
f, err := goutil.LoadGoFile(goutil.LoadConfig{
Expand Down Expand Up @@ -215,7 +215,7 @@ func (l *irLoader) compileFilterFuncs(filename string, irfile *ir.File) error {
if err != nil {
return err
}
if l.ctx.DebugFilter == decl.Name.String() {
if l.ctx.DebugFunc == decl.Name.String() {
l.ctx.DebugPrint(quasigo.Disasm(l.state.env, compiled))
}
ctx.Env.AddFunc(f.Pkg.Path(), decl.Name.String(), compiled)
Expand Down Expand Up @@ -273,6 +273,14 @@ func (l *irLoader) loadRule(group *ir.RuleGroup, rule *ir.Rule) error {
location: rule.LocationVar,
}

if rule.DoFuncName != "" {
doFn := l.state.env.GetFunc(l.file.PkgPath, rule.DoFuncName)
if doFn == nil {
return l.errorf(rule.Line, nil, "can't find a compiled version of %s", rule.DoFuncName)
}
proto.do = doFn
}

info := filterInfo{
Vars: make(map[string]struct{}),
group: group,
Expand Down
37 changes: 32 additions & 5 deletions ruleguard/irconv/irconv.go
Expand Up @@ -95,6 +95,12 @@ func (conv *converter) ConvertFile(f *ast.File) *ir.File {
conv.dslPkgname = imp.Name.Name
}
}
// Right now this list is hardcoded from the knowledge of which
// stdlib packages are supported inside the bytecode.
switch importPath {
case "fmt", "strings", "strconv":
conv.addCustomImport(result, importPath)
}
}

for _, decl := range f.Decls {
Expand Down Expand Up @@ -161,6 +167,10 @@ func (conv *converter) convertInitFunc(dst *ir.File, decl *ast.FuncDecl) {
}
}

func (conv *converter) addCustomImport(dst *ir.File, pkgPath string) {
dst.CustomDecls = append(dst.CustomDecls, `import "`+pkgPath+`"`)
}

func (conv *converter) addCustomDecl(dst *ir.File, decl ast.Decl) {
begin := conv.fset.Position(decl.Pos())
end := conv.fset.Position(decl.End())
Expand Down Expand Up @@ -436,6 +446,7 @@ func (conv *converter) convertRuleExpr(call *ast.CallExpr) {
suggestArgs *[]ast.Expr
reportArgs *[]ast.Expr
atArgs *[]ast.Expr
doArgs *[]ast.Expr
)

for {
Expand Down Expand Up @@ -475,6 +486,8 @@ func (conv *converter) convertRuleExpr(call *ast.CallExpr) {
panic(conv.errorf(chain.Sel, "Report() can't be repeated"))
}
reportArgs = &call.Args
case "Do":
doArgs = &call.Args
case "At":
if atArgs != nil {
panic(conv.errorf(chain.Sel, "At() can't be repeated"))
Expand Down Expand Up @@ -527,13 +540,27 @@ func (conv *converter) convertRuleExpr(call *ast.CallExpr) {
rule.SuggestTemplate = conv.parseStringArg((*suggestArgs)[0])
}

if suggestArgs == nil && reportArgs == nil {
panic(conv.errorf(origCall, "missing Report() or Suggest() call"))
if suggestArgs == nil && reportArgs == nil && doArgs == nil {
panic(conv.errorf(origCall, "missing Report(), Suggest() or Do() call"))
}
if reportArgs == nil {
rule.ReportTemplate = "suggestion: " + rule.SuggestTemplate
if doArgs != nil {
if suggestArgs != nil || reportArgs != nil {
panic(conv.errorf(origCall, "can't combine Report/Suggest with Do yet"))
}
if matchCommentArgs != nil {
panic(conv.errorf(origCall, "can't use Do() with MatchComment() yet"))
}
funcName, ok := (*doArgs)[0].(*ast.Ident)
if !ok {
panic(conv.errorf((*doArgs)[0], "only named function args are supported"))
}
rule.DoFuncName = funcName.String()
} else {
rule.ReportTemplate = conv.parseStringArg((*reportArgs)[0])
if reportArgs == nil {
rule.ReportTemplate = "suggestion: " + rule.SuggestTemplate
} else {
rule.ReportTemplate = conv.parseStringArg((*reportArgs)[0])
}
}

for i, alt := range alternatives {
Expand Down