Analyse statique dans Go: comment gagner du temps lors de la vérification du code


Salut, Habr. Je m'appelle Sergey Rudachenko, je suis expert technique chez Roistat. Au cours des deux dernières années, notre équipe a traduit différentes parties du projet en microservices sur Go. Ils sont développés par plusieurs équipes, nous avons donc dû définir une barre de qualité de code dur. Pour ce faire, nous utilisons plusieurs outils, dans cet article, nous nous concentrerons sur l'un d'eux - l'analyse statique.


L'analyse statique est le processus de vérification automatique du code source à l'aide d'utilitaires spéciaux. Cet article parlera de ses avantages, décrira brièvement les outils populaires et donnera des instructions pour la mise en œuvre. Cela vaut la peine d'être lu si vous n'avez jamais rencontré d'outils similaires ou si vous ne les utilisez pas de manière systématique.


Dans les articles sur ce sujet, le terme "linter" est souvent trouvé. Pour nous, c'est un nom pratique pour des outils simples pour l'analyse statique. La tâche du linter est de rechercher des erreurs simples et une conception incorrecte.


Pourquoi faut-il des linters?


Lorsque vous travaillez en équipe, vous effectuez très probablement des révisions de code. Les erreurs ignorées dans la revue sont des bogues potentiels. Vous avez manqué une error non error - ne recevez pas de message informatif et vous rechercherez le problème à l'aveugle. Erreur dans le casting de caractères ou carte nulle - pire encore, le binaire tombera de panique.


Les erreurs décrites ci-dessus peuvent être ajoutées aux conventions de code , mais les trouver lors de la lecture de la demande d'extraction n'est pas si simple, car le réviseur devra lire le code. S'il n'y a pas de compilateur dans votre tête, certains problèmes iront de toute façon au combat. De plus, la recherche d'erreurs mineures distrait de la vérification de la logique et de l'architecture. À distance, la prise en charge d'un tel code deviendra plus coûteuse. Nous écrivons dans un langage typé, il est étrange de ne pas l'utiliser.


Outils populaires


La plupart des outils d'analyse statique utilisent les packages go/ast et go/parser . Ils fournissent des fonctions pour analyser la syntaxe des fichiers .go. Le thread d'exécution standard (par exemple, pour l'utilitaire golint) est le suivant:


  • la liste des fichiers des packages requis est chargée
  • parser.ParseFile(...) (*ast.File, error) est exécuté pour chaque fichier
  • vérifie les règles prises en charge pour chaque fichier ou package
  • la vérification passe par chaque instruction, par exemple, comme ceci:

 f, err := parser.ParseFile(/* ... */) ast.Walk(func (n *ast.Node) { switch v := node.(type) { case *ast.FuncDecl: if strings.Contains(v.Name, "_") { panic("wrong function naming") } } }, f) 

En plus de l'AST, il existe une attribution statique unique (SSA). Il s'agit d'une façon plus complexe d'analyser le code qui fonctionne avec un thread d'exécution plutôt qu'avec des constructions syntaxiques. Dans cet article, nous ne l'examinerons pas en détail, vous pouvez lire la documentation et jeter un œil à l' exemple de l' utilitaire stackcheck .


Ensuite, seuls les utilitaires populaires qui effectuent des vérifications utiles pour nous seront pris en compte.


gofmt


Il s'agit de l'utilitaire standard du package go qui vérifie la correspondance de style et peut le corriger automatiquement. Le respect du style est une exigence obligatoire pour nous, donc la vérification du gouvernement est incluse dans tous nos projets.


vérification typographique


Typecheck vérifie la correspondance de type dans le code et prend en charge le fournisseur (contrairement à gotype). Son lancement est nécessaire pour vérifier la compilation, mais ne donne pas toutes les garanties.


aller vétérinaire


L'utilitaire go vet fait partie du package standard et est recommandé pour une utilisation par l'équipe Go. Vérifie un certain nombre d'erreurs courantes, par exemple:


  • mauvaise utilisation de printf et de fonctions similaires
  • balises de construction incorrectes
  • comparaison de la fonction et de zéro

golint


Golint est développé par l'équipe Go et valide le code basé sur les documents Effective Go et CodeReviewComments . Malheureusement, il n'y a pas de documentation détaillée, mais le code montre que les éléments suivants sont vérifiés:


 f.lintPackageComment() f.lintImports() f.lintBlankImports() f.lintExported() f.lintNames() f.lintVarDecls() f.lintElses() f.lintRanges() f.lintErrorf() f.lintErrors() f.lintErrorStrings() f.lintReceiverNames() f.lintIncDec() f.lintErrorReturn() f.lintUnexportedReturn() f.lintTimeNames() f.lintContextKeyTypes() f.lintContextArgs() 

vérification statique


Les développeurs eux-mêmes présentent le contrôle statique comme un vétérinaire amélioré. Il y a beaucoup de contrôles, ils sont divisés en groupes:


  • mauvaise utilisation des bibliothèques standard
  • problèmes de multithreading
  • problèmes avec les tests
  • code inutile
  • problèmes de performances
  • conceptions douteuses

gosimple


Il est spécialisé dans la recherche de structures qui méritent d'être simplifiées, par exemple:


Avant ( code source golint )


 func (f *file) isMain() bool { if ffName.Name == "main" { return true } return false } 

Après


 func (f *file) isMain() bool { return ffName.Name == "main" } 

La documentation est similaire à staticcheck et comprend des exemples détaillés.


vérification d'erreur


Les erreurs renvoyées par les fonctions ne peuvent pas être ignorées. Les raisons sont décrites en détail dans le document contraignant Effective Go . Errcheck ne sautera pas le code suivant:


 json.Unmarshal(text, &val) f, _ := os.OpenFile(/* ... */) 

gaz


Recherche les vulnérabilités dans le code: accès codés en dur, injections SQL et utilisation de fonctions de hachage non sécurisées.


Exemples d'erreurs:


 //    IP  l, err := net.Listen("tcp", ":2000") //  sql  q := fmt.Sprintf("SELECT * FROM foo where name = '%s'", name) q := "SELECT * FROM foo where name = " + name //     import "crypto/md5" 

décrié


Dans Go, l'ordre des champs dans les structures affecte la consommation de mémoire. Malignes trouve un tri non optimal. Avec cet ordre de champs:


 struct { a bool b string c bool } 

La structure occupera 32 bits en mémoire en raison de l'ajout de bits vides après les champs a et c.


image


Si nous changeons le tri et mettons deux champs booléens ensemble, la structure ne prendra que 24 bits:


image


Image originale sur stackoverflow


goconst


Les variables magiques du code ne reflètent pas le sens et compliquent la lecture. Goconst trouve les littéraux et les nombres qui apparaissent dans le code 2 fois ou plus. Veuillez noter que même une seule utilisation peut souvent être une erreur.


gocyclo


Nous considérons que la complexité cyclomatique du code est une métrique importante. Gocycle montre la complexité de chaque fonction. Seules les fonctions dépassant la valeur spécifiée peuvent être affichées.


 gocyclo -over 7 package/name 

Nous avons choisi une valeur seuil de 7 pour nous-mêmes, car nous n'avons pas trouvé de code avec une complexité plus élevée qui ne nécessitait pas de refactoring.


Code mort


Il existe plusieurs utilitaires pour trouver du code inutilisé; leurs fonctionnalités peuvent se chevaucher partiellement.


  • ineffassign: vérifie les affectations inutiles

 func foo() error { var res interface{} log.Println(res) res, err := loadData() //  res    return err } 

  • code mort: trouve les fonctions inutilisées
  • inutilisé: trouve les fonctions inutilisées, mais le fait mieux que le code mort

 func unusedFunc() { formallyUsedFunc() } func formallyUsedFunc() { } 

En conséquence, inutilisé pointera vers les deux fonctions à la fois, et le code mort uniquement vers non utiliséFunc. Grâce à cela, le code supplémentaire est supprimé en un seul passage. Également inutilisé trouve les variables et les champs de structure inutilisés.


  • varcheck: recherche les variables inutilisées
  • déconvertir: trouve les conversions de types inutiles

 var res int return int(res) // unconvert error 

S'il n'y a aucune tâche pour économiser sur le temps nécessaire pour lancer les contrôles, il est préférable de les exécuter tous ensemble. Si l'optimisation est nécessaire, je vous recommande d'utiliser inutilisé et annuler la conversion.


Comme il est pratique de configurer


L'exécution des outils ci-dessus dans l'ordre n'est pas pratique: les erreurs sont émises dans un format différent, l'exécution prend beaucoup de temps. La vérification de l'un de nos services avec une taille de ~ 8 000 lignes de code a pris plus de deux minutes. Vous devrez également installer les utilitaires séparément.


Il existe des utilitaires d'agrégation pour résoudre ce problème, par exemple goreporter et gometalinter . Goreporter affiche le rapport en html et gometalinter écrit sur la console.


Gometalinter est toujours utilisé dans certains grands projets (par exemple docker ). Il sait installer tous les utilitaires avec une seule commande, les exécuter en parallèle et formater les erreurs selon le modèle. Le temps d'exécution dans le service ci-dessus a été réduit à une minute et demie.


L'agrégation ne fonctionne que par la coïncidence exacte du texte d'erreur, par conséquent, des erreurs répétées sont inévitables en sortie.


En mai 2018, le projet golangci-lint est apparu sur le github, qui surpasse largement gometalinter en termes de commodité:


  • le temps d'exécution sur le même projet a été réduit à 16 secondes (8 fois)
  • presque aucune erreur en double
  • config yaml claire
  • belle sortie d'erreur avec une ligne de code et un pointeur sur un problème
  • pas besoin d'installer des utilitaires supplémentaires

Maintenant, l'augmentation de la vitesse est fournie par la réutilisation de SSA et du programme de chargement . À l'avenir, il est également prévu de réutiliser l'arborescence AST, dont j'ai parlé au début de la section Outils.


Au moment de la rédaction de cet article sur hub.docker.com, il n'y avait pas d'image avec la documentation, nous avons donc fait le nôtre, personnalisé selon nos idées sur la commodité. À l'avenir, la configuration changera, donc pour la production, nous vous recommandons de la remplacer par la vôtre. Pour ce faire, ajoutez simplement le fichier .golangci.yaml au répertoire racine du projet ( un exemple se trouve dans le référentiel golangci-lint).


 PACKAGE=package/name docker run --rm -t \ -v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE) \ -w /go/src/$(PACKAGE) \ roistat/golangci-lint 

Cette commande peut tester l'ensemble du projet. Par exemple, s'il se trouve dans ~/go/src/project , changez la valeur de la variable en PACKAGE=project . La validation fonctionne récursivement sur tous les packages internes.


Veuillez noter que cette commande ne fonctionne correctement que lorsque vous utilisez le fournisseur.


Implémentation


Tous nos services de développement utilisent Docker. Tout projet s'exécute sans l'environnement go installé. Pour exécuter les commandes, utilisez le Makefile et ajoutez-y la commande lint:


 lint: @docker run --rm -t -v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE) -w /go/src/$(PACKAGE) roistat/golangci-lint 

Maintenant, la vérification est lancée avec cette commande:


 make lint 

Il existe un moyen simple de bloquer le code contenant des erreurs en entrant dans le maître - créez un crochet de pré-réception. Il convient si:


  1. Vous avez un petit projet et peu de dépendances (ou elles sont dans le référentiel)
  2. Ce n'est pas un problème pour vous d'attendre que la commande git push se termine pendant quelques minutes

Instructions de configuration du crochet : Gitlab , Bitbucket Server , Github Enterprise .


Dans d'autres cas, il est préférable d'utiliser CI et d'interdire le code de fusion dans lequel il y a au moins une erreur. C'est exactement ce que nous faisons, en ajoutant le lancement du linter avant les tests.


Conclusion


L'introduction d'examens systématiques a considérablement réduit la période d'examen. Cependant, une autre chose est plus importante: nous pouvons maintenant discuter de la grande image et de l'architecture la plupart du temps. Cela vous permet de penser au développement du projet au lieu de boucher les trous.

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


All Articles