Was wäre, wenn ich Ihnen sagen würde, dass Sie auf diese deklarative Weise Linters für Go erstellen können?
func alwaysTrue(m dsl.Matcher) {
m.Match(`strings.Count($_, $_) >= 0`).Report(`always evaluates to true`)
m.Match(`bytes.Count($_, $_) >= 0`).Report(`always evaluates to true`)
}
func replaceAll() {
m.Match(`strings.Replace($s, $d, $w, $n)`).
Where(m["n"].Value.Int() <= 0).
Suggest(`strings.ReplaceAll($s, $d, $w)`)
}
Vor einem Jahr habe ich bereits über das Dienstprogramm Ruleguard gesprochen . Heute möchte ich mitteilen, was in dieser Zeit neu erschienen ist.
Hauptinnovationen:
- Unterstützung für das Festlegen von Regelsätzen über Go- Bundles
- Programmierbare Filter (zu Bytecode kompiliert)
- Debug-Filtermodus hinzugefügt
- Es gibt ein gutes Lehrmaterial: Vorbild mit gutem Beispiel
- Das Projekt hat echte Benutzer und externe Regelsätze
- Online-Sandbox , mit der Sie Ruleguard direkt in Ihrem Browser ausprobieren können
Kleine Einführung
ruleguard
Ist eine Plattform zum Ausführen einer dynamischen Diagnose. So etwas wie ein Interpreter für Skripte, die sich auf statische Analysen spezialisiert haben.
Sie beschreiben Ihre Regeln in DSL (oder verwenden vorgefertigte Sätze) und führen sie über das Dienstprogramm aus ruleguard
.


, , . , golangci-lint. golangci-lint
.
, , . .
- , .
Da ich manchmal projektspezifische Terminologie verwende, sind hier einige Transkripte.
| RU | RU | Wert |
|---|---|---|
| Regel | Die Regel | AST-, ( — ). |
| Rules group | . "", , . | |
| Rule set | . | |
| Rule bundle | () | , Go , . |
| Module | Go; — , . |
, , .
:
: , .
, , .
, Damian Gryski. — .
: , . . , .
:
-
go get
- Go :
, ruleguard , — Go ( autocomplete ).
, , :
package gorules
import (
"github.com/quasilyte/go-ruleguard/dsl"
damianrules "github.com/dgryski/semgrep-go"
)
func init() {
// , .
dsl.ImportRules("", damianrules.Bundle)
}
func emptyStringTest(m dsl.Matcher) {
m.Match(`len($s) == 0`).
Where(m["s"].Type.Is("string")).
Report(`maybe use $s == "" instead?`)
m.Match(`len($s) != 0`).
Where(m["s"].Type.Is("string")).
Report(`maybe use $s != "" instead?`)
}
, -disable
.
: DSL
dsl.Matcher
, ruleguard
.
, , . Filter()
, Go - . .
package gorules
import (
"github.com/quasilyte/go-ruleguard/dsl"
"github.com/quasilyte/go-ruleguard/dsl/types"
)
// implementsStringer .
// , T *T `fmt.Stringer`.
func implementsStringer(ctx *dsl.VarFilterContext) bool {
stringer := ctx.GetInterface(`fmt.Stringer`)
return types.Implements(ctx.Type, stringer) ||
types.Implements(types.NewPointer(ctx.Type), stringer)
}
func sprintStringer(m dsl.Matcher) {
// m["x"].Type.Implements(`fmt.Stringer`),
// : $x
// fmt.Stringer *T, T .
// : .
m.Match(`fmt.Sprint($x)`).
Where(m["x"].Filter(implementsStringer) && m["x"].Addressable).
Report(`can use $x.String() directly`)
}
:
package main
import "fmt"
func main() {
fooPtr := &Foo{}
foo := Foo{}
println(fmt.Sprint(foo))
println(fmt.Sprint(fooPtr))
println(fmt.Sprint(0)) // fmt.Stringer
println(fmt.Sprint(&foo)) // addressable
}
type Foo struct{}
func (*Foo) String() string { return "Foo" }
:
$ ruleguard -rules rules.go main.go main.go:9:10: can use foo.String() directly main.go:10:10: can use fooPtr.String() directly
-debug-filter
, :

- , , yaegi.
:
Where()
, , .
debug-group
, .
, :
func offBy1(m dsl.Matcher) {
m.Match(`$s[len($s)]`).
Where(m["s"].Type.Is(`[]$elem`) && m["s"].Pure).
Report(`index expr always panics; maybe you wanted $s[len($s)-1]?`)
}
:
func lastByte(s string) byte {
return s[len(s)]
}
func f() byte {
return randString()[len(randString())]
}
… .
$ ruleguard -rules rules.go -debug-group offBy1 test.go
test.go:6: [rules.go:6] rejected by m["s"].Type.Is(`[]$elem`)
$s string: s
test.go:10: [rules.go:6] rejected by m["s"].Pure
$s []byte: randBytes()
Where()
, . Go AST ( $s
), .
[]$elem
, — . - ( pure
).
, , string
:
- Where(m["s"].Type.Is(`[]$elem`) && m["s"].Pure).
+ Where((m["s"].Type.Is(`[]$elem`) || m["s"].Type.Is(`string`)) && m["s"].Pure).
:
test.go:6:9: offBy1: index expr always panics; maybe you wanted s[len(s)-1]?
: DSL
, , .
Go by Example. , . , .

ruleguard?
! ruleguard , Go .
, golangci-lint .
, golangci-lint
, ruleguard
{linux/amd64, linux/arm64, darwin/amd64, windows/amd64}.
. : github.com/quasilyte/go-ruleguard/rules
github.com/dgryski/semgrep-go
. .
, github.com/quasilyte/go-ruleguard/rules
, :
-
ruleguard
( ) -
go get -v github.com/quasilyte/go-ruleguard/dsl
-
go get -v github.com/quasilyte/go-ruleguard/rules
-
rules.go
, -
ruleguard
-rules rules.go
$ ruleguard -rules rules.go ./...
:
- Go
-
Bundle
, .
Go , . , Go .
package gorules
import "github.com/quasilyte/go-ruleguard/dsl"
// Bundle .
var Bundle = dsl.Bundle{}
func boolComparison(m dsl.Matcher) {
m.Match(`$x == true`,
`$x != true`,
`$x == false`,
`$x != false`).
Report(`omit bool literal in expression`)
}
testdata
, Go , .
:
// file rules_test.go
package gorules_test
import (
"testing"
"github.com/quasilyte/go-ruleguard/analyzer"
"golang.org/x/tools/go/analysis/analysistest"
)
func TestRules(t *testing.T) {
// , "rules.go"
// , : "style.go,perf.go".
if err := analyzer.Analyzer.Flags.Set("rules", "rules.go"); err != nil {
t.Fatalf("set rules flag: %v", err)
}
analysistest.Run(t, analysistest.TestData(), analyzer.Analyzer, "./...")
}
:
mybundle/ go.mod -- , "go mod init" rules.go -- ( ) rules_test.go -- testdata/ -- , target1.go target2.go ...
:
// file testdata/target1.go
package test
func f(cond bool) {
if cond == true { // want `omit bool literal in expression`
}
}
want
, . \Q
, .
go test
.

- ruleguard
- , go-critic ruleguard
- ruleguard golangci-lint
- DSL
- go-critic — , ruleguard