Rilis Ruleguard v0.3.0

Bagaimana jika saya memberi tahu Anda bahwa Anda dapat membuat linter untuk Go dengan cara deklaratif?







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)`)
}
      
      





Setahun yang lalu saya sudah berbicara tentang utilitas ruleguard . Hari ini saya ingin berbagi apa yang baru muncul selama ini.







Inovasi utama:














Pengenalan kecil



ruleguard



Adalah platform untuk menjalankan diagnostik dinamis. Sesuatu seperti penerjemah untuk skrip yang berspesialisasi dalam analisis statis.







Anda menjelaskan kumpulan aturan Anda di DSL (atau menggunakan kumpulan yang sudah jadi) dan menjalankannya melalui utilitas ruleguard



.



















Aturan ini ditafsirkan pada waktu proses, jadi penganalisis tidak perlu membangun kembali setiap kali Anda menambahkan diagnostik baru. Ini sangat penting jika kita mempertimbangkan integrasi dengan golangci-lint . Akan sangat canggung untuk mengkompilasi ulang golangci-lint



menggunakan kumpulan aturan Anda sendiri jika Anda mau.







, CodeQL



Semgrep



. , ( ).







, , . .







- , .







,



Karena saya terkadang menggunakan terminologi khusus proyek, berikut adalah beberapa transkripnya.







RU RU Nilai
Aturan Peraturan Template AST dikombinasikan dengan filter dan tindakan terkait (paling sering membuat peringatan).
Grup aturan Grup aturan . "", , .
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
    ...
      
      





File uji akan berisi komentar ajaib:







// file testdata/target1.go

package test

func f(cond bool) {
    if cond == true { // want `omit bool literal in expression`
    }
}
      
      





Setelah itu want



muncul ekspresi reguler yang harus sesuai dengan peringatan yang dikeluarkan. Saya sarankan menggunakannya \Q



di awal sehingga Anda tidak perlu menyaring apa pun.







Tes dijalankan secara normal go test



dari direktori bundel.







Tautan dan materi tambahan












All Articles