Version de Ruleguard v0.3.0

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.







, CodeQL



Semgrep



. , ( ).







, , . .







- , .







,



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





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












All Articles