Lanzamiento de Ruleguard v0.3.0

¿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:














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.







, CodeQL



Semgrep



. , ( ).







, , . .







- , .







,



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 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



, :







  1. ruleguard



    ( )
  2. go get -v github.com/quasilyte/go-ruleguard/dsl



  3. go get -v github.com/quasilyte/go-ruleguard/rules



  4. rules.go



    ,
  5. ruleguard



    -rules rules.go





$ ruleguard -rules rules.go ./...
      
      





ruleguard



, .









:







  1. Go
  2. 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`)
}
      
      





, ruleguard-rules-test.









go/analysis analysistest.







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.







Enlaces y materiales adicionales












All Articles