Regelwächter: Dynamische Prüfungen für Go


In diesem Artikel werde ich über die neue statische Analysebibliothek (und das Hilfsprogramm) von go-ruleguard sprechen, die gogrep für die Verwendung in Linters anpasst.


Besonderheit: Sie beschreiben die Regeln der statischen Analyse auf einem speziellen Go-like DSL, das zu Beginn von ruleguard zu einer Reihe von Diagnosen wird. Dies ist möglicherweise eines der am einfachsten konfigurierbaren Tools für die Implementierung benutzerdefinierter Inspektionen für Go.


Als Bonus werden wir über go/analysis und seine Vorgänger sprechen.


Erweiterbarkeit der statischen Analyse


Es gibt viele Linters für Go, von denen einige erweitert werden können. Um den Linter zu erweitern, müssen Sie normalerweise Go-Code mit der speziellen Linter-API schreiben.


Es gibt zwei Möglichkeiten: Go Plugins und Monolith. Der Monolith impliziert, dass alle Schecks (einschließlich Ihrer persönlichen) in der Kompilierungsphase verfügbar sind.


Für die Erweiterung von revive müssen neue Überprüfungen in den Kernel aufgenommen werden. go-critic kann darüber hinaus Plug-ins, mit denen Sie Erweiterungen unabhängig vom Hauptcode sammeln können. Beide Ansätze implizieren, dass Sie die Manipulationen go/ast und go/types unter Verwendung der linter-API in Go implementieren. Selbst einfache Prüfungen erfordern viel Code .


go/analysis zielt darauf ab, das Bild dadurch zu vereinfachen, dass der "Rahmen" des Linter nahezu identisch wird, löst jedoch nicht das Problem der Komplexität der technischen Implementierung der Diagnose selbst.


Exkurs zu `loader` und` go / packages`


Wenn Sie einen Analyzer für Go schreiben, besteht Ihr letztes Ziel darin, mit AST und Typen zu interagieren. Bevor Sie dies tun können, muss der Quellcode jedoch auf die richtige Weise "geladen" werden. Zur Vereinfachung umfasst das Konzept des Ladens das Parsen , die Typprüfung und das Importieren von Abhängigkeiten .


Der erste Schritt zur Vereinfachung dieser Pipeline war das go/loader Paket, mit dem Sie über ein paar Aufrufe alles "herunterladen" können, was Sie benötigen. Alles war fast in Ordnung, und dann wurde er zugunsten von go/packages veraltet. go/packages hat eine leicht verbesserte API und funktioniert theoretisch gut mit Modulen.


Nun ist es am besten, keine der oben genannten Methoden direkt zum Schreiben von Analysatoren zu verwenden, da go/analysis go/packages etwas gab, das keine der vorherigen Lösungen hatte - eine Struktur für Ihr Programm. Jetzt können wir das vorgegebene go/analysis Paradigma verwenden und Analysegeräte effizienter wiederverwenden. Dieses Paradigma ist umstritten. go/analysis eignet sich beispielsweise gut für die Analyse auf der Ebene eines Pakets und seiner Abhängigkeiten. Eine globale Analyse ohne raffinierte technische Tricks ist jedoch nicht einfach.


go/analysis vereinfacht auch das Testen von Analysegeräten .




Was ist Herrscher?



go-ruleguard ist ein statisches Analysedienstprogramm, das standardmäßig keine einzelne Prüfung enthält.


Die Regeln für die ruleguard zu Beginn aus einer speziellen gorules Datei gorules , die deklarativ die gorules beschreibt, für die Warnungen ausgegeben werden sollen. Diese Datei kann von Benutzern des ruleguard frei bearbeitet werden.


Es ist nicht notwendig, gorules Steuerprogramm gorules zu gorules , um neue Schecks zu verbinden, daher können die Regeln von gorules als dynamisch bezeichnet werden .


Das ruleguard Kontrolle der ruleguard sieht folgendermaßen aus:


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

Gleichzeitig wird der analyzer über das Paket ruleguard implementiert, das Sie verwenden müssen, wenn Sie ihn als Bibliothek verwenden möchten.


Herrscher VS wiederbeleben


Nehmen wir ein einfaches, aber realistisches Beispiel: Nehmen wir an, wir möchten runtime.GC() -Aufrufe in unseren Programmen vermeiden. In revive gibt es dafür bereits eine eigene Diagnose, sie heißt "call-to-gc" .


Call-to-GC-Implementierung (70 Zeilen in 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 } 



Vergleichen Sie nun, wie das in 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`) } 

Nichts mehr, nur was wirklich wichtig ist - runtime.GC und die Nachricht, die ausgegeben werden muss, falls die Regel ausgelöst wird.


Sie fragen sich vielleicht: Ist das alles? Ich habe speziell mit einem so einfachen Beispiel begonnen, um zu zeigen, wie viel Code für eine sehr triviale Diagnose im Fall des herkömmlichen Ansatzes erforderlich sein könnte. Ich verspreche, es wird spannendere Beispiele geben.


Schnellstart


go-critic verfügt über eine rangeExprCopy Diagnose, die potenziell unerwartete Array-Kopien im Code findet.


Dieser Code wird über eine Kopie des Arrays iteriert:


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

Die Lösung für dieses Problem besteht darin, ein Zeichen hinzuzufügen:


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

Höchstwahrscheinlich benötigen Sie dieses Kopieren nicht und die Leistung der korrigierten Version ist immer besser. Sie können warten, bis der Go-Compiler besser wird, oder Sie können solche Stellen im Code erkennen und sie heute mit demselben go-critic korrigieren.


Diese Diagnose kann in der gorules Sprache ( rules.go Datei) rules.go werden:


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

Die Regel findet alle for-range Schleifen, in denen beide iterierbaren Variablen verwendet werden (dies ist der Fall, der zum Kopieren führt). Der iterierbare Ausdruck $x muss addressable und größer als der ausgewählte Schwellenwert in Bytes sein.


Report() definiert die Nachricht, die an den Benutzer ausgegeben werden soll, und Suggest() beschreibt eine quickfix Vorlage, die in Ihrem Editor über gopls (LSP) sowie interaktiv verwendet werden kann, wenn ruleguard mit dem Argument ruleguard aufgerufen wird (wir werden darauf zurückkommen). At() hängt die Warnung und den quickfix an einen bestimmten Teil der Vorlage an. Wir brauchen dies, um $x durch &$x zu ersetzen, anstatt die gesamte Schleife neu zu schreiben.


Sowohl Report() als auch Suggest() akzeptieren eine Zeichenfolge, in die die von der Vorlage aus Match() erfassten Ausdrücke interpoliert werden können. Die vordefinierte Variable $$ bedeutet "alle erfassten Fragmente" (als $0 in regulären Ausdrücken).


Erstellen Sie die Datei 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 } 

Jetzt können wir ruleguard :


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

Wenn wir uns danach rangecopy.go , werden wir eine feste Version sehen, da ruleguard mit dem Parameter ruleguard aufgerufen wurde.


Die einfachsten Regeln können gorules werden, ohne eine gorules Datei zu erstellen:


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

Dank der Verwendung von go/analysis/singlechecker haben wir die Option -c , mit der wir die angegebenen Kontextzeilen zusammen mit der Warnung selbst anzeigen können. Die Steuerung dieses Parameters ist ein wenig eingängig: Der Standardwert ist -c=-1 , was bedeutet, dass "kein Kontext" -c=0 , und -c=0 gibt eine Kontextzeile aus (die von der Diagnose angegebene).


Hier sind einige weitere interessante gorules :


  • Geben Sie Vorlagen ein , mit denen Sie die erwarteten Typen angeben können. Beispielsweise beschreibt die Ausdruckszuordnung map[$t]$t alle Zuordnungen, für die der Werttyp mit dem Typ des Schlüssels übereinstimmt, und *[$len]$elem erfasst alle Zeiger auf Arrays.
  • Innerhalb einer einzelnen Funktion kann es mehrere Regeln geben,
    und die Funktionen selbst sollten Regelgruppen genannt werden .
  • Die Regeln in der Gruppe werden nacheinander in der Reihenfolge angewendet, in der sie definiert sind. Die erste Regel, die ausgelöst wird, bricht den Vergleich mit den übrigen Regeln ab. Dies ist weniger für die Optimierung als vielmehr für die Spezialisierung von Regeln für bestimmte Fälle wichtig. Ein Beispiel, in dem dies nützlich ist, ist die Regel, $x=$x+$y in $x+=$y umzuschreiben. In dem Fall mit $y=1 möchten Sie $x++ und nicht $x+=1 anbieten.

Weitere Informationen zum verwendeten DSL finden Sie in docs/gorules.md .


Weitere Beispiele


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

Wenn für die Regel kein Report() -Aufruf vorhanden ist, wird die von Suggest() ausgegebene Nachricht verwendet. Dies ermöglicht in einigen Fällen die Vermeidung von Doppelspurigkeiten.


Typfilter und Unterausdrücke können verschiedene Eigenschaften prüfen. Beispielsweise sind die Eigenschaften Pure und Const nützlich:


  • Var.Pure bedeutet, dass der Ausdruck keine Nebenwirkungen hat.
  • Var.Const bedeutet, dass der Ausdruck in einem konstanten Kontext verwendet werden kann (z. B. die Dimension eines Arrays).

Für package-qualified Namen unter Where() -Bedingungen müssen Sie die Import() -Methode verwenden. Der Einfachheit halber wurden alle Standardpakete für Sie importiert, sodass im obigen Beispiel keine zusätzlichen Importe erforderlich sind.


go/analysis Quickfix-Aktionen


Support für quickfix von go/analysis für uns quickfix .


Im go/analysis Modell generiert der Analysator Diagnosen und Fakten . Die Diagnose wird an die Benutzer gesendet, und die Fakten sind für die Verwendung durch andere Analysegeräte vorgesehen.


Die Diagnose kann eine Reihe von vorgeschlagenen Korrekturen enthalten , von denen jede beschreibt, wie die Quellcodes im angegebenen Bereich geändert werden, um das von der Diagnose festgestellte Problem zu beheben.


Die offizielle Beschreibung finden Sie unter go/analysis/doc/suggested_fixes.md .


Fazit



Versuchen Sie es mit ruleguard für Ihre Projekte. Wenn Sie einen Fehler finden oder nach einer neuen Funktion fragen möchten, öffnen Sie ein Problem .


Wenn Sie immer noch Schwierigkeiten haben, eine Anwendung für die ruleguard finden, finden Sie hier einige Beispiele:


  • Implementieren Sie Ihre eigene Diagnose für Go.
  • Aktualisieren oder refaktorisieren Sie den Code automatisch mit -fix .
  • Erfassung von -json mit -json Verarbeitung des -json .

Entwicklungspläne für ruleguard in naher Zukunft:



Nützliche Links und Ressourcen


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


All Articles