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:
- Dukungan untuk menetapkan aturan melalui bundel Go
- Filter yang dapat diprogram (dikompilasi ke bytecode)
- Menambahkan mode filter debug
- Ada bahan ajar yang bagus: ruleguard dengan teladan
- Proyek ini memiliki pengguna nyata dan seperangkat aturan eksternal
- Kotak pasir online memungkinkan Anda untuk mencoba ruleguard langsung di browser Anda
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.
, , . .
- , .
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?
! 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
, :
-
ruleguard
( ) -
go get -v github.com/quasilyte/go-ruleguard/dsl
-
go get -v github.com/quasilyte/go-ruleguard/rules
-
rules.go
, -
ruleguard
-rules rules.go
$ ruleguard -rules rules.go ./...
:
- Go
-
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`)
}
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
