go/analysis : le framework modulaire d'analyse statique de l'équipe Go, en petites briques réutilisables

Dev & Code il y a 21 hAjouter aux favoris

go/analysis : le framework modulaire d'analyse statique de l'équipe Go, en petites briques réutilisables
Illustration : Momiji Shirogane

Sous le capot de `go vet`, `staticcheck` et compagnie : l'équipe Go maintient un framework officiel qui laisse écrire un linter en quelques dizaines de lignes.

Le cas concret

Vous héritez d'un projet Go de 200 000 lignes. La revue de code laisse passer, depuis six mois, un pattern qui provoque des goroutine leaks : un context.WithTimeout créé sans defer cancel(). Personne ne le voit à la lecture. go vet ne le signale pas. Ajouter une règle dans le linter maison prendrait, dans un autre langage, une petite semaine.

En Go, on écrit un Analyzer en une soirée. Parce que l'équipe du langage maintient depuis longtemps un package quasi discret et pourtant fondamental : golang.org/x/tools/go/analysis.

Pourquoi c'est important

Le package go/analysis (« Go Analysis Framework ») n'est pas nouveau - il est publié et documenté sur pkg.go.dev/golang.org/x/tools/go/analysis - mais il ressort régulièrement sur Hacker News (dernière remontée le 2026-07-26) parce que beaucoup de développeurs Go l'ignorent encore. Or c'est l'infrastructure sur laquelle tourne l'écosystème d'analyse statique du langage : go vet s'appuie dessus, staticcheck (Dominik Honnef) l'utilise, gopls (le language server officiel) y branche ses diagnostics, et l'immense majorité des linters de la CI moderne - golangci-lint en tête - sont juste des collections d'Analyzer empilés.

Le pari du framework est simple : un analyseur = une struct. Cette struct déclare son nom, sa documentation, ses dépendances vers d'autres analyseurs, et une fonction Run qui reçoit un objet *analysis.Pass (le code parsé, le type-checker, les fichiers, les commentaires) et signale ses diagnostics via pass.Report. C'est tout.

Comment ça marche sous le capot

Trois idées portent l'architecture :

  1. Modularité par composition. Un analyseur peut déclarer Requires: []*analysis.Analyzer{inspect.Analyzer} : le framework résout le graphe de dépendances et injecte le résultat des analyseurs prérequis dans le Pass.ResultOf. Résultat : votre analyseur métier ne re-parse pas les fichiers, il consomme le résultat d'inspect (qui, lui, fait le walk AST typé une seule fois pour toute la chaîne).

  2. Un seul « driver », plusieurs bootstrappings. Vous écrivez votre Analyzer. singlechecker.Main(myAnalyzer) en fait un binaire CLI autonome, multichecker.Main(a1, a2, a3) en fait un binaire multi-checks, unitchecker.Main(...) le branche dans go vet -vettool=.... Zéro glue code : le même code d'Analyzer tourne en local, en CI, dans l'IDE via gopls, et dans golangci-lint.

  3. Fix automatique typé. Un diagnostic peut porter des SuggestedFix : des remplacements textuels ancrés sur des positions token.Pos. gopls les applique en un clic dans l'éditeur. C'est ce qui permet, par exemple, à staticcheck de proposer « remplacer errors.Is(err, sql.ErrNoRows) par une check plus idiomatique » et à l'IDE de faire la substitution proprement.

Pour donner un ordre de grandeur : singlechecker - la fonction qui transforme un *Analyzer en binaire complet - tient en un fichier. Le squelette minimal d'un linter Go publiable, c'est grosso modo 40 lignes, la doc et les tests compris. Comparez à écrire un plugin ESLint from scratch : ce n'est pas la même échelle.

Ce que ça change en pratique

Trois usages qu'on voit passer sur des vrais projets :

  • Règles internes à l'entreprise. « Interdit d'importer directement net/http, il faut passer par notre wrapper de tracing. » Trente lignes, un Analyzer qui inspecte les *ast.ImportSpec, un test avec analysistest.Run sur un fichier de fixture, et la règle est branchée en CI le lendemain.
  • Migration de code massive. Refactor à grande échelle (renommage d'une API deprecated dans 500 fichiers) : un Analyzer avec SuggestedFix transforme l'opération en go vet -vettool=./monfix -fix ./.... Zéro humain dans la boucle.
  • Analyse de dataflow custom. Le framework fournit des packages compagnons - ctrlflow (control flow graph), buildssa (représentation SSA du code Go) - sur lesquels on peut brancher des analyses de dataflow non triviales. Détecter les fuites de sql.Rows non fermées, par exemple, se fait sérieusement à partir de la SSA, pas de l'AST brut.

Les limites qu'on doit connaître

Deux angles morts à garder en tête :

  • Analyse intra-package. Par défaut, un Analyzer voit un package à la fois. Les analyses inter-packages (whole-program) demandent du travail supplémentaire - c'est possible avec les « facts » (analysis.Fact), qui permettent à un analyseur d'exporter des faits sur les symboles d'un package pour que ces faits soient réutilisés quand un autre package l'importe, mais c'est un cran plus complexe à écrire.
  • Pas d'écosystème de plugins dynamiques. On ne « plugue » pas un Analyzer dans go vet par config runtime : on le compile dans un binaire. C'est un choix (Go n'aime pas les plugins dynamiques), pas un oubli.

À retenir

  • go/analysis est l'API officielle pour tout ce qui touche à l'analyse statique en Go, du linter maison à la migration automatique de code.
  • Un analyseur = une struct avec un Run(*Pass). Le reste (CLI, intégration go vet, intégration gopls, CI) vous est offert.
  • Si vous écrivez encore vos règles de qualité de code en shell ou en sed, cette soirée-là est bien investie.
Ressources, à tester

Article produit par intelligence artificielle, relu sous contrôle éditorial humain.

Notre rédaction
Cet article vous a-t-il été utile ?

3 personnes ont aimé cet article

J'aime
K
Kaito KuroganeRédacteur dev senior
Développeur senior polyvalent, backend Go + frontend TS, contributeur open source.
Partager :
LIVERadio Geek Kitsune
Clique pour écouter, le même son pour tout le monde
0··
// Programme
// toutes les stations
// partager un titre →
Rubriques
Explorer
Informations