Go-Kritiker: Der hartnäckigste statische Analysator für Go


Wir kündigen einen neuen Linter (statischen Analysator) für Go an , der auch eine Sandbox für das Prototyping Ihrer Ideen in der Welt der statischen Analyse ist.


go -kritiker baut auf folgenden Beobachtungen auf:


  • Es ist besser, den Test „gut genug“ zu implementieren, als ihn überhaupt nicht zu haben
  • Wenn die Prüfung umstritten ist, bedeutet dies nicht, dass sie nicht nützlich sein kann. Wir markieren als "meinungsbildend" und gießen
  • Das Schreiben eines Linter von Grund auf ist normalerweise schwieriger als das Hinzufügen eines neuen Checks zu einem vorhandenen Framework, wenn das Framework selbst leicht zu verstehen ist.

In diesem Beitrag werden wir uns mit der Verwendung und Architektur von Go-Critical und einigen darin implementierten Überprüfungen befassen und auch die Hauptschritte zum Hinzufügen unserer Analysatorfunktion beschreiben.


Schnellstart


$ cd $GOPATH $ go get -u github.com/go-critic/go-critic/... $ ./bin/gocritic check-package strings $GOROOT/src/strings/replace.go:450:22: unslice: could simplify s[:] to s $GOROOT/src/strings/replace.go:148:2: elseif: should rewrite if-else to switch statement $GOROOT/src/strings/replace.go:156:3: elseif: should rewrite if-else to switch statement $GOROOT/src/strings/replace.go:219:3: elseif: should rewrite if-else to switch statement $GOROOT/src/strings/replace.go:370:1: paramTypeCombine: func(pattern string, value string) *singleStringReplacer could be replaced with func(pattern, value string) *singleStringReplacer $GOROOT/src/strings/replace.go:259:2: rangeExprCopy: copy of r.mapping (256 bytes) can be avoided with &r.mapping $GOROOT/src/strings/replace.go:264:2: rangeExprCopy: copy of r.mapping (256 bytes) can be avoided with &r.mapping $GOROOT/src/strings/strings.go:791:1: paramTypeCombine: func(s string, cutset string) string could be replaced with func(s, cutset string) string $GOROOT/src/strings/strings.go:800:1: paramTypeCombine: func(s string, cutset string) string could be replaced with func(s, cutset string) string $GOROOT/src/strings/strings.go:809:1: paramTypeCombine: func(s string, cutset string) string could be replaced with func(s, cutset string) string $GOROOT/src/strings/strings.go:44:1: unnamedResult: consider to give name to results $GOROOT/src/strings/strings.go:61:1: unnamedResult: consider to give name to results $GOROOT/src/strings/export_test.go:28:3: rangeExprCopy: copy of r.mapping (256 bytes) can be avoided with &r.mapping $GOROOT/src/strings/export_test.go:42:1: unnamedResult: consider to give name to results 

(Die Formatierung der Warnungen wurde bearbeitet. Die Originale sind in der Übersicht verfügbar.)


Das Dienstprogramm gocritic kann einzelne Pakete anhand ihres Importpfads ( check-package ) check-package und auch alle Verzeichnisse rekursiv durchlaufen ( check-project ). Sie können beispielsweise den gesamten $GOROOT oder $GOPATH mit einem einzigen Befehl überprüfen:


 $ gocritic check-project $GOROOT/src $ gocritic check-project $GOPATH/src 

Die „weiße Liste“ für Prüfungen wird unterstützt, um explizit -enable welche Prüfungen durchgeführt werden sollen (Flag -enable ). Standardmäßig werden alle Prüfungen ausgeführt, die nicht mit dem Symbol Experimental oder VeryOpinionated sind.


Integrationen in Golangci-Lint und Gometalinter sind geplant .


Wie alles begann


Wenn Sie die nächste Codeüberprüfung des Go-Projekts durchführen oder eine Bibliothek von Drittanbietern prüfen, können Sie immer wieder dieselben Probleme feststellen.


Zu Ihrem Bedauern war es nicht möglich, einen Linter zu finden, der diese Klasse von Problemen diagnostizieren würde.


Ihr erster Schritt könnte ein Versuch sein, das Problem zu kategorisieren und die Autoren des vorhandenen Linter zu kontaktieren, und ihnen vorzuschlagen, eine neue Prüfung hinzuzufügen. Die Wahrscheinlichkeit, dass Ihr Vorschlag angenommen wird, hängt stark vom Projekt ab und kann sehr gering sein. Weitere, höchstwahrscheinlich werden Monate der Erwartung folgen.


Was aber, wenn die Prüfung völlig mehrdeutig ist und von jemandem als zu subjektiv oder unzureichend genau wahrgenommen werden kann?


Vielleicht ist es sinnvoll, diesen Scheck selbst zu schreiben?


go-critic gibt es, um ein Zuhause für experimentelle Tests zu werden, die von uns einfacher zu implementieren sind, als sie an vorhandene statische Analysegeräte anzuschließen. Das go-critic Gerät selbst minimiert die Menge an Kontext und Aktionen, die zum Hinzufügen einer neuen Prüfung erforderlich sind. Wir können sagen, dass Sie nur eine Datei hinzufügen müssen (ohne die Tests).


Wie Go-Kritiker funktioniert


Ein Kritiker ist eine Reihe von Regeln , die Prüfeigenschaften beschreiben, und Mikroprüfer, die eine Codeprüfung zur Einhaltung einer Regel implementieren.


Eine Anwendung, die den Linter einbettet (z. B. cmd / gocritic oder golangci-lint ), empfängt eine Liste der unterstützten Regeln, filtert sie auf eine bestimmte Weise, erstellt eine Überprüfungsfunktion für jede ausgewählte Regel und startet jede über das zu untersuchende Paket.


Das Hinzufügen eines neuen Checkers besteht aus drei Hauptschritten:


  1. Tests hinzufügen.
  2. Die Durchführung der Überprüfung selbst.
  3. Dokumentation für den Linter hinzufügen.

Wir werden alle diese Punkte anhand der Beispiel- CaptLocal- Regel durchgehen , die das Fehlen lokaler Namen erfordert, die mit einem Großbuchstaben beginnen.



Tests hinzufügen


Um Testdaten für eine neue Prüfung hinzuzufügen, müssen Sie ein neues Verzeichnis in lint / testdata erstellen .


Jedes dieser Verzeichnisse sollte eine Datei positive_tests.go enthalten, in der Codebeispiele beschrieben werden, mit denen die Prüfung funktionieren soll. Um das Fehlen von Fehlalarmen zu testen, werden die Tests durch einen „richtigen“ Code ergänzt, in dem die neue Prüfung keine Probleme finden sollte ( negative_tests.go ).


Beispiele:


 // lint/testdata/positive_tests.go /// consider `in' name instead of `IN' /// `X' should not be capitalized /// `Y' should not be capitalized /// `Z' should not be capitalized func badFunc1(IN, X int) (Y, Z int) { /// `V' should not be capitalized V := 1 return V, 0 } 

 // lint/testdata/negative_tests.go func goodFunc1(in, x int) (x, y int) { v := 1 return v, 0 } 

Sie können Tests ausführen, nachdem Sie einen neuen Linter hinzugefügt haben.


Implementierung der Überprüfung


Erstellen Sie eine Datei mit dem Namen des lint/captLocal_checker.go : lint/captLocal_checker.go .
Konventionell haben alle Micro-Linter-Dateien das Suffix _checker .


 package lint //  “Checker”    . type captLocalChecker struct { checkerBase upcaseNames map[string]bool } 

checkerBase ist ein Typ, der in jeden Checker eingebettet sein muss.
Es bietet Standardimplementierungen, mit denen Sie weniger Code in jeden Linter schreiben können.
CheckerBase enthält unter anderem einen Zeiger auf lint.context , der lint.context und andere Metadaten zur überprüften Datei enthält.


Das Feld upcaseNames enthält eine Tabelle mit bekannten Namen, die wir durch die strings.ToLower(name) ersetzen werden. strings.ToLower(name) -Version. Für Namen, die nicht in der Karte enthalten sind, wird empfohlen, keinen Großbuchstaben zu verwenden, es wird jedoch kein korrekter Ersatz bereitgestellt.


Der interne Status wird für jede Instanz einmal initialisiert.
Die Init() -Methode muss nur für diejenigen Linters definiert werden, die eine vorläufige Initialisierung durchführen müssen.


 func (c *captLocalChecker) Init() { c.upcaseNames = map[string]bool{ "IN": true, "OUT": true, "INOUT": true, } } 

Jetzt müssen Sie die Prüffunktion selbst definieren.
Im Fall von captLocal müssen wir alle lokalen ast.Ident überprüfen, die neue Variablen einführen.


Um alle lokalen Definitionen von Namen zu überprüfen, sollten Sie eine Methode mit der folgenden Signatur in Ihrem Checker implementieren:


 VisitLocalDef(name astwalk.Name, initializer ast.Expr) 

Die Liste der verfügbaren Besucheroberflächen finden Sie in der Datei lint / internal / visiter.go .
captLocal implementiert LocalDefVisitor .


 //  ast.Expr,         //  .      . func (c *captLocalChecker) VisitLocalDef(name astwalk.Name, _ ast.Expr) { switch { case c.upcaseNames[name.ID.String()]: c.warnUpcase(name.ID) case ast.IsExported(name.ID.String()): c.warnCapitalized(name.ID) } } func (c *captLocalChecker) warnUpcase(id *ast.Ident) { c.ctx.Warn(id, "consider `%s' name instead of `%s'", strings.ToLower(id.Name), id) } func (c *captLocalChecker) warnCapitalized(id ast.Node) { c.ctx.Warn(id, "`%s' should not be capitalized", id) } 

Konventionell werden Methoden, die Warnungen generieren, normalerweise in separaten Methoden ausgegeben. Es gibt seltene Ausnahmen, aber das Befolgen dieser Regel wird als bewährte Methode angesehen.


Dokumentation hinzufügen


Eine weitere notwendige Implementierungsmethode ist InitDocumentation :


 func (c *captLocalChecker) InitDocumentation(d *Documentation) { d.Summary = "Detects capitalized names for local variables" d.Before = `func f(IN int, OUT *int) (ERR error) {}` d.After = `func f(in int, out *int) (err error) {}` } 

Füllen Sie normalerweise nur 3 Felder aus:


  • Summary - eine Beschreibung der Validierungsaktion in einem Satz.
  • Before - Code vor Korrektur.
  • Nachcode nach Korrektur (sollte keine Warnung verursachen).

Dokumentationserstellung

Das erneute Generieren der Dokumentation ist keine Voraussetzung für einen neuen Linter. Vielleicht wird dieser Schritt in naher Zukunft vollständig automatisiert. Wenn Sie dennoch überprüfen möchten, wie die Ausgabe-Markdown-Datei aussehen soll, verwenden Sie den Befehl make docs . Die Datei docs/overview.md wird aktualisiert.


Registrieren Sie einen neuen Linter und führen Sie Tests durch


Der letzte Schliff ist die Registrierung eines neuen Linter:


 //   captLocal_checker.go init . func init() { addChecker(&captLocalChecker{}, attrExperimental, attrSyntaxOnly) } 

addChecker erwartet einen Zeiger auf den Nullwert des neuen Linter. Als nächstes folgt das varadische Argument, mit dem Sie null oder mehr Attribute übergeben können, die die Eigenschaften der Implementierung der Regel beschreiben.


attrSyntaxOnly ist eine optionale Markierung für Linters, die in ihrer Implementierung keine attrSyntaxOnly sodass Sie sie ausführen können, ohne Typprüfungen durchzuführen. golangci-lint markiert solche Linters mit der Flagge „schnell“ (weil sie viel schneller laufen).


attrExperimental ist ein Attribut, das allen neuen Implementierungen zugewiesen wird. Das Entfernen dieses Attributs ist erst nach Stabilisierung der implementierten Prüfung möglich.


Nachdem der neue Linter über addChecker registriert wurde, können Sie die folgenden Tests ausführen:


 #  GOPATH: $ go test -v github.com/go-critic/go-critic/lint #  GOPATH/src/github.com/go-critic/go-critic: $ go test -v ./lint #  ,      make: $ make test 

Optimistisches Zusammenführen (fast)


Bei der Prüfung von Pull-Anfragen versuchen wir, die optimistische Zusammenführungsstrategie einzuhalten. Dies drückt sich hauptsächlich in der Akzeptanz der PRs aus, auf die der Prüfer möglicherweise einige, insbesondere rein subjektive Ansprüche hat. Unmittelbar nach der Injektion eines solchen Patches kann eine PR vom Prüfer folgen, die diese Mängel behebt. Der Autor des Original-Patches wird zu CC hinzugefügt (Kopie).


Wir haben auch zwei Linter-Marker, mit denen rote Fahnen vermieden werden können, wenn kein vollständiger Konsens besteht:


  1. Experimental : Eine Implementierung kann eine große Menge an falsch positiven Ergebnissen aufweisen, unwirksam sein (die Ursache des Problems wird identifiziert) oder in bestimmten Situationen „fallen“. Sie können eine solche Implementierung infundieren, wenn Sie sie mit dem Attribut attrExperimental markieren. Manchmal werden mit Hilfe von Experimenten solche Überprüfungen angezeigt, bei denen beim ersten Festschreiben kein guter Name gefunden wurde.
  2. VeryOpinionated : Wenn der Scheck sowohl Verteidiger als auch Feinde haben kann, lohnt es sich, ihn mit dem Attribut attrVeryOpinionated . Auf diese Weise können wir vermeiden, Ideen über den Codestil abzulehnen, die möglicherweise nicht dem Geschmack einiger Gophers entsprechen.

Experimental ist eine potenziell temporäre und behebbare Implementierungseigenschaft. VeryOpinionated ist eine grundlegendere VeryOpinionated , die von der Implementierung unabhängig ist.


Es wird empfohlen, vor dem Senden der Implementierung ein [checker-request] checker [checker-request] -Ticket auf github zu erstellen. Wenn Sie jedoch bereits eine Pull-Anfrage gesendet haben, können Sie das entsprechende Problem für Sie öffnen.


Weitere Informationen zum Entwicklungsprozess finden Sie unter CONTRIBUTING.md .
Die Grundregeln sind im Abschnitt Hauptregeln aufgeführt.


Abschiedswörter


Sie können am Projekt nicht nur teilnehmen, indem Sie einen neuen Linter hinzufügen.
Es gibt viele andere Möglichkeiten:


  • Probieren Sie es bei Ihren Projekten oder großen / bekannten Open-Source-Projekten aus und melden Sie falsch positive, falsch negative und andere Mängel. Wir wären Ihnen dankbar, wenn Sie der Trophäenseite auch einen Hinweis zu dem gefundenen / behobenen Problem hinzufügen würden.
  • Schlagen Sie Ideen für neue Inspektionen vor. Es reicht aus, ein Problem auf unserem Tracker zu erstellen.
  • Fügen Sie Tests für vorhandene Linters hinzu.

go-critic kritisiert Ihren Go-Code mit den Stimmen aller an seiner Entwicklung beteiligten Programmierer. Jeder kann also kritisieren - mitmachen!


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


All Articles