Et si je vous disais que vous pouvez créer des linters pour Go de cette manière déclarative?
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)`)
}
Il y a un an, j'ai déjà parlé de l'utilitaire ruleguard . Aujourd'hui, je voudrais partager ce que de nouveau est apparu pendant cette période.
Principales innovations:
- Appui à la mise rulesets via Go faisceaux
- Filtres programmables (compilés en bytecode)
- Ajout du mode de filtre de débogage
- Il y a un bon matériel didactique: la règle par l'exemple
- Le projet a de vrais utilisateurs et des règles externes
- Bac à sable en ligne vous permettant d'essayer ruleguard directement dans votre navigateur
Petite introduction
ruleguard
Est une plate-forme pour exécuter des diagnostics dynamiques. Quelque chose comme un interpréteur de scripts spécialisé dans l'analyse statique.
Vous décrivez votre ensemble de règles sur DSL (ou utilisez des ensembles prêts à l'emploi) et les exécutez via l'utilitaire ruleguard
.


Ces règles sont interprétées lors de l'exécution, il n'est donc pas nécessaire de reconstruire l'analyseur chaque fois que vous ajoutez de nouveaux diagnostics. Ceci est particulièrement important si nous envisageons l'intégration avec golangci-lint . Il serait très difficile de recompiler en golangci-lint
utilisant votre propre jeu de règles si vous le souhaitez.
, , . .
- , .
Comme j'utilise parfois une terminologie spécifique à un projet, voici quelques transcriptions.
| RU | RU | Valeur |
|---|---|---|
| Règle | La règle | Modèle AST combiné avec des filtres et des actions associées (créant le plus souvent une alerte). |
| Groupe de règles | Groupe de règles | . "", , . |
| 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 ...
Les fichiers de test contiendront des commentaires magiques:
// file testdata/target1.go
package test
func f(cond bool) {
if cond == true { // want `omit bool literal in expression`
}
}
Après cela want
vient une expression régulière qui devrait correspondre à l'avertissement émis. Je recommande de l'utiliser \Q
au début afin que vous n'ayez rien à filtrer.
Le test est exécuté normalement à go test
partir du répertoire du bundle.
Liens et matériel supplémentaire
