Skip to content

Commit 9f27f07

Browse files
authored
ruleguard: implement dsl Do() function (#379)
1 parent a307c89 commit 9f27f07

File tree

16 files changed

+305
-21
lines changed

16 files changed

+305
-21
lines changed

analyzer/analyzer.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ var (
5454
flagGoVersion string
5555

5656
flagDebug string
57-
flagDebugFilter string
57+
flagDebugFunc string
5858
flagDebugImports bool
5959
flagDebugEnableDisable bool
6060
)
6161

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

190190
ctx := &ruleguard.LoadContext{
191191
Fset: fset,
192-
DebugFilter: flagDebugFilter,
192+
DebugFunc: flagDebugFunc,
193193
DebugImports: flagDebugImports,
194194
DebugPrint: debugPrint,
195195
GroupFilter: func(g *ruleguard.GoRuleGroup) bool {

analyzer/analyzer_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var tests = []struct {
3939
{name: "regression"},
4040
{name: "testvendored"},
4141
{name: "quasigo"},
42+
{name: "do"},
4243
{name: "matching"},
4344
{name: "dgryski"},
4445
{name: "comments"},

analyzer/testdata/src/do/rules.go

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//go:build ignore
2+
// +build ignore
3+
4+
package gorules
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
10+
"github.com/quasilyte/go-ruleguard/dsl"
11+
"github.com/quasilyte/go-ruleguard/dsl/types"
12+
)
13+
14+
func reportHello(ctx *dsl.DoContext) {
15+
ctx.SetReport("Hello, World!")
16+
}
17+
18+
func suggestHello(ctx *dsl.DoContext) {
19+
ctx.SetSuggest("Hello, World!")
20+
}
21+
22+
func reportX(ctx *dsl.DoContext) {
23+
ctx.SetReport(ctx.Var("x").Text())
24+
}
25+
26+
func unquote(s string) string {
27+
return s[1 : len(s)-1]
28+
}
29+
30+
func reportTrimPrefix(ctx *dsl.DoContext) {
31+
s := unquote(ctx.Var("x").Text())
32+
prefix := unquote(ctx.Var("y").Text())
33+
ctx.SetReport(strings.TrimPrefix(s, prefix))
34+
}
35+
36+
func reportEmptyString(ctx *dsl.DoContext) {
37+
x := ctx.Var("x")
38+
if x.Text() == `""` {
39+
ctx.SetReport("empty string")
40+
} else {
41+
ctx.SetReport("non-empty string")
42+
}
43+
}
44+
45+
func reportType(ctx *dsl.DoContext) {
46+
ctx.SetReport(ctx.Var("x").Type().String())
47+
}
48+
49+
func reportTypesIdentical(ctx *dsl.DoContext) {
50+
xtype := ctx.Var("x").Type()
51+
ytype := ctx.Var("y").Type()
52+
ctx.SetReport(fmt.Sprintf("%v", types.Identical(xtype, ytype)))
53+
}
54+
55+
func testRules(m dsl.Matcher) {
56+
m.Match(`test("custom report")`).
57+
Do(reportHello)
58+
59+
m.Match(`test("custom suggest")`).
60+
Do(suggestHello)
61+
62+
m.Match(`test("var text", $x)`).
63+
Do(reportX)
64+
65+
m.Match(`test("trim prefix", $x, $y)`).
66+
Do(reportTrimPrefix)
67+
68+
m.Match(`test("report empty string", $x)`).
69+
Do(reportEmptyString)
70+
71+
m.Match(`test("report type", $x)`).
72+
Do(reportType)
73+
74+
m.Match(`test("types identical", $x, $y)`).
75+
Do(reportTypesIdentical)
76+
}

analyzer/testdata/src/do/target.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package do
2+
3+
func Example() {
4+
test("custom report") // want `\QHello, World!`
5+
test("custom suggest") // want `\Qsuggestion: Hello, World!`
6+
7+
var x int
8+
test("var text", "str") // want `\Q"str"`
9+
test("var text", x+1) // want `\Qx+1`
10+
11+
test("trim prefix", "hello, world", "hello") // want `\Q, world`
12+
test("trim prefix", "hello, world", "hello, ") // want `\Qworld`
13+
test("trim prefix", "hello, world", "???") // want `\Qhello, world`
14+
15+
test("report empty string", "") // want `\Qempty string`
16+
test("report empty string", "example") // want `\Qnon-empty string`
17+
18+
test("report type", 13) // want `\Qint`
19+
test("report type", "str") // want `\Qstring`
20+
test("report type", []int{1}) // want `\Q[]int`
21+
test("report type", x) // want `\Qint`
22+
test("report type", &x) // want `\Q*int`
23+
24+
test("types identical", 1, 1) // want `true`
25+
test("types identical", x, x) // want `true`
26+
test("types identical", x, &x) // want `false`
27+
test("types identical", 1, 1.5) // want `false`
28+
}
29+
30+
func test(args ...interface{}) {}

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.17
55
require (
66
github.com/go-toolsmith/astcopy v1.0.0
77
github.com/google/go-cmp v0.5.6
8-
github.com/quasilyte/go-ruleguard/dsl v0.3.16
8+
github.com/quasilyte/go-ruleguard/dsl v0.3.17
99
github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71
1010
github.com/quasilyte/gogrep v0.0.0-20220120141003-628d8b3623b5
1111
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ github.com/quasilyte/go-ruleguard v0.3.1-0.20210203134552-1b5a410e1cc8/go.mod h1
1212
github.com/quasilyte/go-ruleguard/dsl v0.3.0/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
1313
github.com/quasilyte/go-ruleguard/dsl v0.3.16 h1:yJtIpd4oyNS+/c/gKqxNwoGO9+lPOsy1A4BzKjJRcrI=
1414
github.com/quasilyte/go-ruleguard/dsl v0.3.16/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
15+
github.com/quasilyte/go-ruleguard/dsl v0.3.17 h1:L5xf3nifnRIdYe9vyMuY2sDnZHIgQol/fDq74FQz7ZY=
16+
github.com/quasilyte/go-ruleguard/dsl v0.3.17/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
1517
github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc=
1618
github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71 h1:CNooiryw5aisadVfzneSZPswRWvnVW8hF1bS/vo8ReI=
1719
github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50=

ruleguard/engine.go

+6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import (
1717
"github.com/quasilyte/go-ruleguard/internal/goenv"
1818
"github.com/quasilyte/go-ruleguard/ruleguard/ir"
1919
"github.com/quasilyte/go-ruleguard/ruleguard/quasigo"
20+
"github.com/quasilyte/go-ruleguard/ruleguard/quasigo/stdlib/qfmt"
21+
"github.com/quasilyte/go-ruleguard/ruleguard/quasigo/stdlib/qstrconv"
22+
"github.com/quasilyte/go-ruleguard/ruleguard/quasigo/stdlib/qstrings"
2023
"github.com/quasilyte/go-ruleguard/ruleguard/typematch"
2124
"github.com/quasilyte/stdinfo"
2225
)
@@ -141,6 +144,9 @@ type engineState struct {
141144

142145
func newEngineState() *engineState {
143146
env := quasigo.NewEnv()
147+
qstrings.ImportAll(env)
148+
qstrconv.ImportAll(env)
149+
qfmt.ImportAll(env)
144150
state := &engineState{
145151
env: env,
146152
pkgCache: make(map[string]*types.Package),

ruleguard/gorule.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type goRule struct {
3838
location string
3939
suggestion string
4040
filter matchFilter
41+
do *quasigo.Func
4142
}
4243

4344
type matchFilterResult string
@@ -66,14 +67,19 @@ type filterParams struct {
6667
match matchData
6768
nodePath *nodePath
6869

69-
nodeText func(n ast.Node) []byte
70+
nodeText func(n ast.Node) []byte
71+
nodeString func(n ast.Node) string
7072

7173
deadcode bool
7274

7375
currentFunc *ast.FuncDecl
7476

7577
// varname is set only for custom filters before bytecode function is called.
7678
varname string
79+
80+
// Both of these are Do() function related fields.
81+
reportString string
82+
suggestString string
7783
}
7884

7985
func (params *filterParams) subNode(name string) ast.Node {

ruleguard/ir/ir.go

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type Rule struct {
5151

5252
ReportTemplate string
5353
SuggestTemplate string
54+
DoFuncName string
5455

5556
WhereExpr FilterExpr
5657

ruleguard/ir_loader.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,12 @@ func (l *irLoader) compileFilterFuncs(filename string, irfile *ir.File) error {
178178
buf.WriteString("package gorules\n")
179179
buf.WriteString("import \"github.com/quasilyte/go-ruleguard/dsl\"\n")
180180
buf.WriteString("import \"github.com/quasilyte/go-ruleguard/dsl/types\"\n")
181-
buf.WriteString("type _ = dsl.Matcher\n")
182-
buf.WriteString("type _ = types.Type\n")
183181
for _, src := range irfile.CustomDecls {
184182
buf.WriteString(src)
185183
buf.WriteString("\n")
186184
}
185+
buf.WriteString("type _ = dsl.Matcher\n")
186+
buf.WriteString("type _ = types.Type\n")
187187

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

276+
if rule.DoFuncName != "" {
277+
doFn := l.state.env.GetFunc(l.file.PkgPath, rule.DoFuncName)
278+
if doFn == nil {
279+
return l.errorf(rule.Line, nil, "can't find a compiled version of %s", rule.DoFuncName)
280+
}
281+
proto.do = doFn
282+
}
283+
276284
info := filterInfo{
277285
Vars: make(map[string]struct{}),
278286
group: group,

ruleguard/irconv/irconv.go

+32-5
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ func (conv *converter) ConvertFile(f *ast.File) *ir.File {
9595
conv.dslPkgname = imp.Name.Name
9696
}
9797
}
98+
// Right now this list is hardcoded from the knowledge of which
99+
// stdlib packages are supported inside the bytecode.
100+
switch importPath {
101+
case "fmt", "strings", "strconv":
102+
conv.addCustomImport(result, importPath)
103+
}
98104
}
99105

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

170+
func (conv *converter) addCustomImport(dst *ir.File, pkgPath string) {
171+
dst.CustomDecls = append(dst.CustomDecls, `import "`+pkgPath+`"`)
172+
}
173+
164174
func (conv *converter) addCustomDecl(dst *ir.File, decl ast.Decl) {
165175
begin := conv.fset.Position(decl.Pos())
166176
end := conv.fset.Position(decl.End())
@@ -436,6 +446,7 @@ func (conv *converter) convertRuleExpr(call *ast.CallExpr) {
436446
suggestArgs *[]ast.Expr
437447
reportArgs *[]ast.Expr
438448
atArgs *[]ast.Expr
449+
doArgs *[]ast.Expr
439450
)
440451

441452
for {
@@ -475,6 +486,8 @@ func (conv *converter) convertRuleExpr(call *ast.CallExpr) {
475486
panic(conv.errorf(chain.Sel, "Report() can't be repeated"))
476487
}
477488
reportArgs = &call.Args
489+
case "Do":
490+
doArgs = &call.Args
478491
case "At":
479492
if atArgs != nil {
480493
panic(conv.errorf(chain.Sel, "At() can't be repeated"))
@@ -527,13 +540,27 @@ func (conv *converter) convertRuleExpr(call *ast.CallExpr) {
527540
rule.SuggestTemplate = conv.parseStringArg((*suggestArgs)[0])
528541
}
529542

530-
if suggestArgs == nil && reportArgs == nil {
531-
panic(conv.errorf(origCall, "missing Report() or Suggest() call"))
543+
if suggestArgs == nil && reportArgs == nil && doArgs == nil {
544+
panic(conv.errorf(origCall, "missing Report(), Suggest() or Do() call"))
532545
}
533-
if reportArgs == nil {
534-
rule.ReportTemplate = "suggestion: " + rule.SuggestTemplate
546+
if doArgs != nil {
547+
if suggestArgs != nil || reportArgs != nil {
548+
panic(conv.errorf(origCall, "can't combine Report/Suggest with Do yet"))
549+
}
550+
if matchCommentArgs != nil {
551+
panic(conv.errorf(origCall, "can't use Do() with MatchComment() yet"))
552+
}
553+
funcName, ok := (*doArgs)[0].(*ast.Ident)
554+
if !ok {
555+
panic(conv.errorf((*doArgs)[0], "only named function args are supported"))
556+
}
557+
rule.DoFuncName = funcName.String()
535558
} else {
536-
rule.ReportTemplate = conv.parseStringArg((*reportArgs)[0])
559+
if reportArgs == nil {
560+
rule.ReportTemplate = "suggestion: " + rule.SuggestTemplate
561+
} else {
562+
rule.ReportTemplate = conv.parseStringArg((*reportArgs)[0])
563+
}
537564
}
538565

539566
for i, alt := range alternatives {

0 commit comments

Comments
 (0)