
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" )
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 {
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
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) {
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