¿Y si te dijera que puedes crear linters para Go de esta forma declarativa?
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)`)
}
Hace un año ya hablé de la utilidad ruleguard . Hoy me gustaría compartir lo nuevo que ha aparecido durante este tiempo.
Principales innovaciones:
- Soporte para configurar conjuntos de reglas a través de paquetes Go
- Filtros programables (compilados en bytecode)
- Modo de filtro de depuración agregado
- Hay un buen material didáctico: Ruleguard con el ejemplo.
- El proyecto tiene usuarios reales y conjuntos de reglas externos.
- Sandbox en línea que le permite probar Ruleguard directamente en su navegador
Pequeña introducción
ruleguard
Es una plataforma para ejecutar diagnósticos dinámicos. Algo así como un intérprete de guiones especializado en análisis estático.
Usted describe su conjunto de reglas en DSL (o usa conjuntos prefabricados) y los ejecuta a través de la utilidad ruleguard
.
Estas reglas se interpretan en tiempo de ejecución, por lo que no es necesario reconstruir el analizador cada vez que agrega nuevos diagnósticos. Esto es especialmente importante si estamos considerando la integración con golangci-lint . Sería muy incómodo volver a compilar golangci-lint
usando su propio conjunto de reglas si lo desea.
, , . .
- , .
Dado que a veces utilizo terminología específica del proyecto, aquí hay algunas transcripciones.
RU | RU | Valor |
---|---|---|
Regla | La regla | Plantilla AST combinada con filtros y acciones asociadas (la mayoría de las veces crea una alerta). |
Grupo de reglas | Grupo de reglas | . "", , . |
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 ...
Los archivos de prueba contendrán comentarios mágicos:
// file testdata/target1.go
package test
func f(cond bool) {
if cond == true { // want `omit bool literal in expression`
}
}
Después want
viene una expresión regular que debe coincidir con la advertencia emitida. Recomiendo usarlo \Q
al principio para que no tenga que filtrar nada.
La prueba se ejecuta normalmente go test
desde el directorio del paquete.