Ruleguard v0.3.0 release

What if I told you that you can create linters for Go in this declarative way?







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





A year ago I already talked about the ruleguard utility . Today I would like to share what new has appeared during this time.







Main innovations:







  • Support for setting rulesets via Go bundles
  • Programmable filters (compiled to bytecode)
  • Added debug filter mode
  • There is a good teaching material: ruleguard by example
  • The project has real users and external rulesets
  • Online sandbox allowing you to try ruleguard right in your browser







Small introduction



ruleguard



Is a platform for running dynamic diagnostics. Something like an interpreter for scripts specializing in static analysis.







You describe your set of rules on DSL (or use ready-made sets) and run them through the utility ruleguard



.



















These rules are interpreted at runtime, so there is no need to rebuild the analyzer every time you add new diagnostics. This is especially important if we are considering integration with golangci-lint . It would be very awkward to recompile golangci-lint



using your own ruleset if you want to.







, CodeQL



Semgrep



. , ( ).







, , . .







- , .







,



Since I sometimes use project-specific terminology, here are a few transcripts.







RU RU Value
Rule The rule AST template combined with filters and associated actions (most often creating an alert).
Rules group Rule 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
    ...
      
      





Test files will contain magical comments:







// file testdata/target1.go

package test

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





After that want



comes a regular expression that should match the issued warning. I recommend using it \Q



at the beginning so that you don't have to screen anything.







The test is run normally go test



from the bundle directory.







Links and additional materials












All Articles