ruleeguard: pemeriksaan dinamis untuk Go


Dalam artikel ini, saya akan berbicara tentang perpustakaan analisis statis go-ruleguard (dan utilitas) yang mengadaptasi gogrep untuk digunakan di dalam linter.


Fitur khas: Anda menguraikan aturan analisis statis pada DSL khusus Go-like, yang pada awal ruleguard berubah menjadi seperangkat diagnostik. Mungkin ini adalah salah satu alat yang paling mudah dikonfigurasi untuk menerapkan inspeksi khusus untuk Go.


Sebagai bonus, kita akan membicarakan go/analysis dan pendahulunya .


Extensibility Analisis Statis


Ada banyak linter untuk Go, beberapa di antaranya dapat diperluas. Biasanya, untuk memperpanjang linter, Anda perlu menulis kode Go menggunakan API linter khusus.


Ada dua cara utama: Go plugins and monolith. Monolith menyiratkan bahwa semua cek (termasuk yang pribadi Anda) tersedia pada tahap kompilasi.


revive membutuhkan pemeriksaan baru untuk dimasukkan ke dalam kernel untuk ekspansi. go-critic selain plug-in ini, yang memungkinkan Anda untuk mengumpulkan ekstensi terlepas dari kode utama. Kedua pendekatan ini menyiratkan bahwa Anda menerapkan manipulasi go/ast dan go/types on Go menggunakan linter API. Bahkan cek sederhana membutuhkan banyak kode .


go/analysis bertujuan untuk menyederhanakan gambar dengan fakta bahwa "kerangka kerja" linter menjadi hampir identik, tetapi itu tidak menyelesaikan masalah kompleksitas implementasi teknis dari diagnostik itu sendiri.


Digresi pada `loader` dan` go / packages`


Saat Anda menulis penganalisis untuk Go, tujuan akhir Anda adalah untuk berinteraksi dengan AST dan tipe, tetapi sebelum Anda dapat melakukan ini, kode sumber perlu "dimuat" dengan cara yang benar. Untuk menyederhanakan, konsep memuat termasuk penguraian , pemeriksaan jenis, dan impor dependensi .


Langkah pertama dalam menyederhanakan pipa ini adalah paket go/loader , yang memungkinkan Anda untuk "mengunduh" semua yang Anda butuhkan melalui beberapa panggilan. Semuanya hampir baik-baik saja, dan kemudian ia menjadi usang demi go/packages . go/packages memiliki API yang sedikit ditingkatkan dan, secara teori, bekerja dengan baik dengan modul.


Sekarang, yang terbaik adalah tidak menggunakan salah satu di atas secara langsung untuk menulis analisis, karena go/analysis memberikan go/packages sesuatu yang tidak dimiliki oleh solusi sebelumnya - sebuah struktur untuk program Anda. Sekarang kita dapat menggunakan paradigma go/analysis didiktekan dan menggunakan kembali analisis secara lebih efisien. Paradigma ini memiliki poin kontroversial, misalnya, go/analysis cocok untuk analisis pada tingkat satu paket dan dependensinya, tetapi untuk membuat analisis global pada itu tanpa trik teknik licik tidak akan mudah.


go/analysis juga menyederhanakan pengujian analyzer .




Apa itu peraturan?



go-ruleguard adalah utilitas analisis statis yang secara default tidak menyertakan pemeriksaan tunggal.


ruleguard aturan dimuat pada awalnya, dari file gorules khusus yang secara gorules menggambarkan pola kode yang harus dikeluarkan peringatan. File ini dapat diedit secara bebas oleh pengguna ruleguard .


Tidak perlu gorules program kontrol untuk menghubungkan pemeriksaan baru, sehingga aturan dari gorules dapat disebut dinamis .


Program kontrol pengawas terlihat seperti ini:


 package main import ( "github.com/quasilyte/go-ruleguard/analyzer" "golang.org/x/tools/go/analysis/singlechecker" ) func main() { singlechecker.Main(analyzer.Analyzer) } 

Pada saat yang sama, analyzer diimplementasikan melalui paket ruleguard , yang harus Anda gunakan jika Anda ingin menggunakannya sebagai perpustakaan.


memerintah kembali VS menghidupkan kembali


Ambil contoh sederhana namun nyata: misalkan kita ingin menghindari panggilan runtime.GC() di program kami. Dalam menghidupkan kembali sudah ada diagnostik terpisah untuk ini, itu disebut "call-to-gc" .


Implementasi panggilan-ke-gc (70 jalur di Elven)


 package rule import ( "go/ast" "github.com/mgechev/revive/lint" ) // CallToGCRule lints calls to the garbage collector. type CallToGCRule struct{} // Apply applies the rule to given file. func (r *CallToGCRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure { var failures []lint.Failure onFailure := func(failure lint.Failure) { failures = append(failures, failure) } var gcTriggeringFunctions = map[string]map[string]bool{ "runtime": map[string]bool{"GC": true}, } w := lintCallToGC{onFailure, gcTriggeringFunctions} ast.Walk(w, file.AST) return failures } // Name returns the rule name. func (r *CallToGCRule) Name() string { return "call-to-gc" } type lintCallToGC struct { onFailure func(lint.Failure) gcTriggeringFunctions map[string]map[string]bool } func (w lintCallToGC) Visit(node ast.Node) ast.Visitor { ce, ok := node.(*ast.CallExpr) if !ok { return w // nothing to do, the node is not a call } fc, ok := ce.Fun.(*ast.SelectorExpr) if !ok { return nil // nothing to do, the call is not of the form pkg.func(...) } id, ok := fc.X.(*ast.Ident) if !ok { return nil // in case X is not an id (it should be!) } fn := fc.Sel.Name pkg := id.Name if !w.gcTriggeringFunctions[pkg][fn] { return nil // it isn't a call to a GC triggering function } w.onFailure(lint.Failure{ Confidence: 1, Node: node, Category: "bad practice", Failure: "explicit call to the garbage collector", }) return w } 



Sekarang bandingkan dengan bagaimana hal ini dilakukan di go-ruleguard :


 package gorules import "github.com/quasilyte/go-ruleguard/dsl/fluent" func callToGC(m fluent.Matcher) { m.Match(`runtime.GC()`).Report(`explicit call to the garbage collector`) } 

Tidak lebih, hanya yang benar-benar penting - runtime.GC dan pesan yang perlu dikeluarkan jika aturan dipicu.


Anda mungkin bertanya: apakah hanya itu? Saya secara khusus mulai dengan contoh sederhana untuk menunjukkan berapa banyak kode yang mungkin diperlukan untuk diagnosis yang sangat sepele dalam kasus pendekatan tradisional. Saya berjanji akan ada contoh yang lebih menarik.


Mulai cepat


go-critic memiliki diagnostik rangeExprCopy yang menemukan salinan array berpotensi tak terduga dalam kode.


Kode ini diulang pada salinan array:


 var xs [2048]byte for _, x := range xs { // Copies 2048 bytes // Loop body. } 

Perbaikan untuk masalah ini adalah menambahkan satu karakter:


  var xs [2048]byte - for _, x := range xs { // Copies 2048 bytes + for _, x := range &xs { // No copy // Loop body. } 

Kemungkinan besar, Anda tidak perlu menyalin ini, dan kinerja versi yang diperbaiki selalu lebih baik. Anda dapat menunggu hingga kompiler Go menjadi lebih baik, atau Anda dapat mendeteksi tempat-tempat tersebut dalam kode dan memperbaikinya hari ini menggunakan go-critic .


Diagnosis ini dapat diimplementasikan dalam bahasa rules.go (file rules.go ):


 package gorules import "github.com/quasilyte/go-ruleguard/dsl/fluent" func _(m fluent.Matcher) { m.Match(`for $_, $_ := range $x { $*_ }`, `for $_, $_ = range $x { $*_ }`). Where(m["x"].Addressable && m["x"].Type.Size >= 128). Report(`$x copy can be avoided with &$x`). At(m["x"]). Suggest(`&$x`) } 

Aturan menemukan semua loop for-range mana kedua variabel iterable digunakan (ini adalah kasus yang mengarah ke menyalin). Ekspresi iterable $x harus addressable dan harus lebih besar dari ambang yang dipilih dalam byte.


Report() mendefinisikan pesan yang akan dikeluarkan untuk pengguna, dan Suggest() menguraikan templat quickfix yang dapat digunakan dalam editor Anda melalui gopls (LSP), serta secara interaktif jika ruleguard dipanggil dengan argumen- -fix (kami akan kembali ke ini). At() melampirkan peringatan dan quickfix ke bagian tertentu dari templat. Kita membutuhkan ini untuk mengganti $x dengan &$x , daripada menulis ulang seluruh loop.


Baik Report() dan Suggest() menerima string ke mana ekspresi yang diambil oleh templat dari Match() dapat diinterpolasi. Variabel predefined $$ berarti "semua fragmen yang ditangkap" (sebagai $0 dalam ekspresi reguler).


Buat file rangecopy.go :


 package example // sizeof(builtins[...]) = 240 on x86-64 var builtins = [...]string{ "append", "cap", "close", "complex", "copy", "delete", "imag", "len", "make", "new", "panic", "print", "println", "real", "recover", } func builtinID(name string) int { for i, s := range builtins { if s == name { return i } } return -1 } 

Sekarang kita bisa menjalankan ruleguard :


 $ ruleguard -rules rules.go -fix rangecopy.go rangecopy.go:12:20: builtins copy can be avoided with &builtins 

Jika setelah itu kita melihat rangecopy.go , kita akan melihat versi yang tetap, karena ruleguard dipanggil dengan parameter -fix .


Aturan paling sederhana dapat gorules tanpa membuat file gorules :


 $ ruleguard -c 1 -e 'm.Match(`return -1`)' rangecopy.go rangecopy.go:17:2: return -1 16 } 17 return -1 18 } 

Berkat penggunaan go/analysis/singlechecker , kami memiliki opsi -c , yang memungkinkan kami untuk menampilkan garis konteks yang ditentukan bersama dengan peringatan itu sendiri. Mengontrol parameter ini sedikit berlawanan dengan intuisi: nilai defaultnya adalah -c=-1 , yang berarti "tidak ada konteks", dan -c=0 akan menampilkan satu baris konteks (yang ditunjukkan oleh diagnostik).


Berikut adalah beberapa gorules lebih menarik:


  • Ketik templat yang memungkinkan Anda menentukan jenis yang diharapkan. Misalnya, map[$t]$t ekspresi map[$t]$t menjelaskan semua peta dengan tipe nilai yang cocok dengan jenis kunci, dan *[$len]$elem menangkap semua pointer ke array.
  • Dalam satu fungsi, mungkin ada beberapa aturan,
    dan fungsi itu sendiri harus disebut kelompok aturan .
  • Aturan dalam grup diterapkan satu demi satu, sesuai dengan urutannya. Aturan pertama yang dipicu membatalkan perbandingan dengan aturan lainnya. Ini penting bukan untuk optimasi tetapi untuk aturan khusus untuk kasus-kasus tertentu. Contoh di mana ini berguna adalah aturan penulisan ulang $x=$x+$y hingga $x+=$y , untuk kasus dengan $y=1 Anda ingin menawarkan $x++ , bukan $x+=1 .

Informasi lebih lanjut tentang DSL yang digunakan dapat ditemukan di docs/gorules.md .


Lebih banyak contoh


 package gorules import "github.com/quasilyte/go-ruleguard/dsl/fluent" func exampleGroup(m fluent.Matcher) { //     json.Decoder. // . http://golang.org/issue/36225 m.Match(`json.NewDecoder($_).Decode($_)`). Report(`this json.Decoder usage is erroneous`) //   unconvert,    . m.Match(`time.Duration($x) * time.Second`). Where(m["x"].Const). Suggest(`$x * time.Second`) //   fmt.Sprint()    String(), //   $x  . m.Match(`fmt.Sprint($x)`). Where(m["x"].Type.Implements(`fmt.Stringer`)). Suggest(`$x.String()`) //   . m.Match(`!($x != $y)`).Suggest(`$x == $y`) m.Match(`!($x == $y)`).Suggest(`$x != $y`) } 

Jika tidak ada panggilan Report() untuk aturan, maka output pesan dari Suggest() akan digunakan. Dalam beberapa kasus ini memungkinkan untuk menghindari duplikasi.


Ketik filter dan subekspresi dapat memeriksa berbagai properti. Misalnya, properti Pure dan Const berguna:


  • Var.Pure berarti bahwa ekspresi tidak memiliki efek samping.
  • Var.Const berarti bahwa ekspresi dapat digunakan dalam konteks konstan (misalnya, dimensi array).

Untuk nama yang package-qualified dalam kondisi Where() , Anda perlu menggunakan metode Import() . Untuk kenyamanan, semua paket standar diimpor untuk Anda, jadi dalam contoh di atas kami tidak perlu melakukan impor tambahan.


go/analysis tindakan perbaikan cepat


Dukungan untuk quickfix oleh go/analysis untuk kami.


Dalam model go/analysis , alat analisa menghasilkan diagnostik dan fakta . Diagnostik dikirimkan kepada pengguna, dan fakta-fakta dimaksudkan untuk digunakan oleh analis lain.


Diagnostik dapat memiliki satu set perbaikan yang disarankan , yang masing-masing menjelaskan cara mengubah kode sumber dalam kisaran yang ditentukan untuk memperbaiki masalah yang ditemukan oleh diagnostik.


Deskripsi resmi tersedia di go/analysis/doc/suggested_fixes.md .


Kesimpulan



Coba ruleguard proyek Anda, dan jika Anda menemukan bug atau ingin meminta fitur baru, buka masalah .


Jika Anda masih merasa kesulitan untuk membuat ruleguard , berikut adalah beberapa contoh:


  • Terapkan diagnostik Anda sendiri untuk Go.
  • Secara otomatis memutakhirkan atau memperbaiki kode dengan -fix .
  • Koleksi statistik kode dengan -json pemrosesan hasil -json .

Rencana pengembangan untuk ruleguard dalam waktu dekat:



Tautan dan sumber daya yang bermanfaat


Source: https://habr.com/ru/post/id481696/


All Articles