Ruleguardv0.3.0リリース

この宣言的な方法でGoのリンターを作成できると言ったらどうしますか?







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





1年前、私はすでにルールガードユーティリティについて話しました本日は、この時期に新たに登場したものをお伝えしたいと思います。







主な革新:














簡単な紹介



ruleguard



動的診断を実行するためのプラットフォームです。静的分析に特化したスクリプトのインタプリタのようなもの。







DSLでルールのセットを記述し(または既製のセットを使用して)、ユーティリティを介してそれらを実行しますruleguard





















, , . , golangci-lint. golangci-lint



.







, CodeQL



Semgrep



. , ( ).







, , . .







- , .







,



, .







EN RU
Rule AST-, ( — ).
Rules group . "", , .
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 testdata/target1.go

package test

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





want



, . \Q



, .







go test



.


















All Articles