Skip to content

Commit

Permalink
ruleguard: implement dsl Do() function (#379)
Browse files Browse the repository at this point in the history
  • Loading branch information
quasilyte committed Feb 13, 2022
1 parent a307c89 commit 9f27f07
Show file tree
Hide file tree
Showing 16 changed files with 305 additions and 21 deletions.
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

0 comments on commit 9f27f07

Please sign in to comment.