règle: contrôle dynamique de Go


Dans cet article, je parlerai de la nouvelle bibliothèque (et utilitaire) d'analyse statique go-ruleguard qui adapte gogrep pour une utilisation à l'intérieur des linters.


Particularité: vous décrivez les règles de l'analyse statique sur un DSL spécial Go-like, qui au début de ruleguard se transforme en un ensemble de diagnostics. C'est peut-être l'un des outils les plus faciles à configurer pour implémenter des inspections personnalisées pour Go.


En bonus, nous parlerons de go/analysis et de ses prédécesseurs .


Extensibilité de l'analyse statique


Il existe de nombreux linters pour Go, dont certains peuvent être étendus. Habituellement, pour étendre le linter, vous devez écrire du code Go à l'aide de l'API linter spéciale.


Il existe deux méthodes principales: les plugins Go et monolith. Le monolithe implique que tous les chèques (y compris les vôtres personnels) sont disponibles au stade de la compilation.


revive nécessite que de nouveaux contrôles soient inclus dans son noyau pour l'expansion. go-critic en plus de cela peut plug-ins, ce qui vous permet de collecter des extensions quel que soit le code principal. Ces deux approches impliquent que vous implémentez les manipulations go/ast et go/types sur Go à l'aide de l'API linter. Même les vérifications simples nécessitent beaucoup de code .


go/analysis vise à simplifier l'image par le fait que le "framework" du linter devient presque identique, mais il ne résout pas le problème de la complexité de la mise en œuvre technique des diagnostics eux-mêmes.


Digression sur `loader` et` go / packages`


Lorsque vous écrivez un analyseur pour Go, votre objectif final est d'interagir avec l'AST et les types, mais avant de pouvoir le faire, le code source doit être «chargé» correctement. Pour simplifier, le concept de chargement inclut l' analyse , la vérification de type et l' importation de dépendances .


La première étape de la simplification de ce pipeline a été le package go/loader , qui vous permettra de "télécharger" tout ce dont vous avez besoin en quelques appels. Tout allait presque bien, puis il est devenu obsolète au profit de go/packages . go/packages a une API légèrement améliorée et, en théorie, fonctionne bien avec les modules.


Maintenant, pour écrire des analyseurs, il est préférable de ne pas utiliser directement ce qui précède, car go/analysis donné à go/packages quelque chose qu'aucune solution précédente n'avait - une structure pour votre programme. Nous pouvons maintenant utiliser le paradigme dicté go/analysis et réutiliser les analyseurs plus efficacement. Ce paradigme a des points controversés, par exemple, go/analysis bien adapté pour l'analyse au niveau d'un package et de ses dépendances, mais faire une analyse globale sur celui-ci sans astuces d'ingénierie astucieuses ne sera pas facile.


go/analysis simplifie également les tests de l'analyseur .




Qu'est-ce que la règle?



go-ruleguard est un utilitaire d'analyse statique qui, par défaut, n'inclut pas une seule vérification.


Les règles ruleguard chargées au début, à partir d'un fichier spécial gorules qui décrit de manière déclarative les modèles de code auxquels les avertissements doivent être émis. Ce fichier peut être édité librement par les utilisateurs de ruleguard .


Il n'est pas nécessaire de gorules programme de contrôle pour connecter de nouveaux chèques, de sorte que les règles des gorules peuvent être appelées dynamiques .


Le programme de contrôle des ruleguard ressemble à ceci:


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

Dans le même temps, l' analyzer implémenté via le package ruleguard , que vous devez utiliser si vous souhaitez l'utiliser comme bibliothèque.


règleguard VS revive


Prenons un exemple simple mais réel: supposons que nous voulons éviter les runtime.GC() dans nos programmes. Dans Revive, il existe déjà un diagnostic distinct pour cela, il est appelé "call-to-gc" .


Implémentation de Call-to-gc (70 lignes en elfique)


 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 } 



Comparez maintenant avec la façon dont cela se fait dans 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`) } 

Rien de plus, juste ce qui compte vraiment - runtime.GC et le message qui doit être émis en cas de déclenchement de la règle.


Vous pouvez vous demander: c'est tout? J'ai spécifiquement commencé avec un exemple aussi simple pour montrer combien de code pourrait être nécessaire pour un diagnostic très trivial dans le cas de l'approche traditionnelle. Je promets qu'il y aura des exemples plus excitants.


Démarrage rapide


go-critic a un diagnostic rangeExprCopy qui trouve des copies de tableau potentiellement inattendues dans le code.


Ce code est itéré sur une copie du tableau:


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

La solution à ce problème consiste à ajouter un caractère:


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

Très probablement, vous n'avez pas besoin de cette copie et les performances de la version corrigée sont toujours meilleures. Vous pouvez attendre que le compilateur Go s'améliore, ou vous pouvez détecter de tels endroits dans le code et les corriger aujourd'hui en utilisant le même go-critic .


Ce diagnostic peut être implémenté dans le langage rules.go (fichier 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`) } 

La règle recherche toutes les boucles for-range où les deux variables itérables sont utilisées (c'est le cas qui conduit à la copie). L'expression itérable $x doit être addressable et doit être supérieure au seuil sélectionné en octets.


Report() définit le message à quickfix à l'utilisateur, et Suggest() décrit un modèle de quickfix qui peut être utilisé dans votre éditeur via gopls (LSP), ainsi qu'interactivement si ruleguard appelé avec l'argument -fix (nous y reviendrons). At() attache l'avertissement et le quickfix à une partie spécifique du modèle. Nous en avons besoin pour remplacer $x par &$x , plutôt que de réécrire la boucle entière.


Report() et Suggest() acceptent une chaîne dans laquelle les expressions capturées par le modèle à partir de Match() peuvent être interpolées. La variable prédéfinie $$ signifie «tout fragment capturé» (comme $0 dans les expressions régulières).


Créez le fichier 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 } 

Maintenant, nous pouvons exécuter le ruleguard :


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

Si après cela, nous regardons rangecopy.go , nous verrons une version fixe, car ruleguard été appelé avec le paramètre -fix .


Les règles les plus simples peuvent être déboguées sans créer de fichier gorules :


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

Grâce à l'utilisation de go/analysis/singlechecker , nous avons l'option -c , qui nous permet d'afficher les lignes de contexte spécifiées avec l'avertissement lui-même. Contrôler ce paramètre est un peu contre-intuitif: la valeur par défaut est -c=-1 , ce qui signifie "pas de contexte", et -c=0 affichera une ligne de contexte (celle indiquée par les diagnostics).


Voici quelques gorules plus intéressantes:


  • Modèles de type qui vous permettent de spécifier les types attendus. Par exemple, l'expression map[$t]$t décrit toutes les maps dont le type de valeur correspond au type de clé et *[$len]$elem capture tous les pointeurs vers les tableaux.
  • Dans une même fonction, il peut y avoir plusieurs règles,
    et les fonctions elles-mêmes devraient être appelées groupes de règles .
  • Les règles du groupe sont appliquées les unes après les autres, dans l'ordre dans lequel elles sont définies. La première règle déclenchée annule la comparaison avec les autres règles. Ceci est important non pas tant pour l'optimisation que pour la spécialisation des règles pour des cas spécifiques. Un exemple où cela est utile est la règle de réécriture de $x=$x+$y en $x+=$y , pour le cas avec $y=1 vous voulez offrir $x++ , pas $x+=1 .

Plus d'informations sur la DSL utilisée peuvent être trouvées dans docs/gorules.md .


Plus d'exemples


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

S'il n'y a pas d'appel Report() pour la règle, la sortie de message de Suggest() sera utilisée. Cela permet dans certains cas d'éviter les doublons.


Les filtres de type et les sous-expressions peuvent vérifier diverses propriétés. Par exemple, les propriétés Pure et Const sont utiles:


  • Var.Pure signifie que l'expression n'a aucun effet secondaire.
  • Var.Const signifie que l'expression peut être utilisée dans un contexte constant (par exemple, la dimension d'un tableau).

Pour les noms package-qualified dans les conditions Where() , vous devez utiliser la méthode Import() . Pour plus de commodité, tous les packages standard ont été importés pour vous, donc dans l'exemple ci-dessus, nous n'avons pas besoin d'effectuer des importations supplémentaires.


go/analysis actions quickfix


Le support de quickfix par go/analysis pour nous.


Dans le modèle go/analysis , l'analyseur génère des diagnostics et des faits . Les diagnostics sont envoyés aux utilisateurs et les faits sont destinés à être utilisés par d'autres analyseurs.


Les diagnostics peuvent avoir un ensemble de correctifs suggérés , chacun décrivant comment modifier les codes source dans la plage spécifiée afin de résoudre le problème détecté par les diagnostics.


La description officielle est disponible dans go/analysis/doc/suggested_fixes.md .


Conclusion



Essayez ruleguard sur vos projets, et si vous trouvez un bug ou si vous souhaitez demander une nouvelle fonctionnalité, ouvrez le problème .


Si vous trouvez toujours difficile de trouver une application de ruleguard , voici quelques exemples:


  • Implémentez vos propres diagnostics pour Go.
  • -fix niveau ou refactoriser automatiquement le code avec -fix .
  • Collection de statistiques de code avec traitement -json du résultat de l' -json .

Plans de développement de ruleguard dans un avenir proche:



Liens et ressources utiles


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


All Articles