Analyse statique de gros volumes de code Python: expérience Instagram. 2e partie

Nous publions aujourd'hui la deuxième partie de la traduction de matériel consacré à l'analyse statique de gros volumes de code Python côté serveur dans Instagram.



La première partie

Des programmeurs fatigués de peluches


Étant donné que nous avons une centaine de nos propres règles sur les peluches, une comptabilité pédante des recommandations émises par ces règles peut rapidement entraîner une perte de temps pour les développeurs. Il serait préférable de passer du temps à redresser le style de code ou à se débarrasser des modèles obsolètes pour créer quelque chose de nouveau et développer le projet.

Nous avons constaté que lorsque les programmeurs voient trop de notifications provenant du linter, ils commencent à ignorer tous ces messages. Cela s'applique également aux notifications importantes.
Supposons que nous décidions de déclarer la fonction fn obsolète et d'utiliser la fonction avec un meilleur nom, add place. Si vous n'en informez pas les développeurs, ils ne sauront pas qu'ils n'ont plus besoin d'utiliser la fonction fn . Pire encore, ils ne savent pas quoi utiliser à la place de cette fonction. Dans cette situation, vous pouvez créer une règle de linter. Mais toute grande base de code contiendra déjà de nombreuses règles. En conséquence, il est probable qu'une importante notification de linter sera perdue dans le tas de notifications de bugs mineurs.


Le linter est trop tatillon et le «signal utile» peut facilement se perdre dans le «bruit»

Qu'allons-nous en faire?

Vous pouvez corriger automatiquement de nombreux problèmes détectés par le linter. Si le linter lui-même peut être comparé à la documentation qui apparaît là où il est nécessaire, alors ces corrections automatiques sont un peu une refactorisation de code qui est exécutée là où elle est nécessaire. Étant donné le grand nombre de développeurs travaillant sur Instagram, il est presque impossible de former chacun d'eux à nos meilleures techniques d'écriture de code. L'ajout de capacités de correction automatique de code au système nous permet d'éduquer les développeurs sur les nouvelles techniques lorsqu'ils ne sont pas au courant de ces techniques. Cela nous aide à mettre rapidement à jour les développeurs. De plus, les corrections automatiques nous ont permis de faire en sorte que les programmeurs se concentrent sur des choses importantes, plutôt que de se concentrer sur des changements de code mineurs monotones. En général, on peut noter que les corrections automatiques de code sont plus efficaces et utiles en termes de formation des développeurs que les simples notifications de linter.

Alors, comment créer un système de correction automatique de code? Un lint basé sur un arbre de syntaxe nous donne des informations sur un nœud dysfonctionnel. Par conséquent, nous n'avons pas besoin de créer de logique pour détecter les problèmes, car nous avons déjà les règles correspondantes pour le linter! Puisque nous savons quel nœud particulier ne nous convient pas et où se trouve son code source, nous pouvons, sans risquer de gâcher quelque chose, par exemple, remplacer le nom de la fonction fn par add . Ceci est bien adapté pour corriger des violations uniques des règles qui sont exécutées lorsque de telles violations sont détectées. Mais que se passe-t-il si nous introduisons une nouvelle règle pour le linter, ce qui signifie qu'il peut y avoir des centaines de fragments de code dans la base de code qui ne respectent pas cette règle? Toutes ces incohérences peuvent-elles être corrigées à l'avance?

Mods de code


Un codemod est juste un moyen de trouver des problèmes et d'apporter des modifications au code source. Les codemods sont basés sur des scripts. Codemod peut être considéré comme une «refactorisation des stéroïdes». La gamme de tâches résolues par les modes de code est extrêmement large: des tâches simples, telles que le changement de nom d'une variable dans une fonction, aux tâches complexes, telles que la réécriture d'une fonction pour qu'elle prenne un nouvel argument. Lorsque vous travaillez avec le codemod, les mêmes concepts sont utilisés qu'avec le fonctionnement du linter. Mais au lieu d'informer le programmeur du problème, comme le fait le linter, le mode code résout automatiquement ce problème.

Comment écrire un codemod? Prenons un exemple. Ici, nous voulons arrêter d'utiliser get_global . Dans cette situation, vous pouvez utiliser le linter, mais on ne saura pas combien de temps il faudra pour corriger l'intégralité du code.En outre, cette tâche sera répartie entre de nombreux développeurs. Dans le même temps, même si le projet utilise un système de correction automatique de code, le traitement de tout le code peut prendre un certain temps.


Nous voulons éviter d'utiliser get_global et utiliser des variables d'instance à la place

Pour résoudre ce problème, nous pouvons, avec la règle de linter qui le détecte, écrire un codemod. Nous pensons qu'autoriser des modèles et des API obsolètes à laisser progressivement du code distraira les développeurs et dégradera la lisibilité du code. Nous préférons supprimer immédiatement le code obsolète, et ne pas regarder comment il disparaît progressivement du projet.

Compte tenu du volume de notre code et du nombre de développeurs actifs, cela signifie souvent l'élimination automatique des conceptions obsolètes. Si nous sommes en mesure de supprimer rapidement le code des modèles obsolètes, cela signifie que nous pouvons maintenir la productivité de tous les développeurs Instagram.

Alors, comment faire un codemod? Comment remplacer uniquement le fragment de code qui nous intéresse, tout en préservant les commentaires, l'indentation et tout le reste? Il existe des outils basés sur une arborescence de syntaxe spécifique (comme ce que LibCST crée) qui vous permettent de modifier le code avec une précision chirurgicale et d'y enregistrer toutes les constructions auxiliaires. Par conséquent, si nous devons changer le nom de la fonction de fn pour l' add dans l'arborescence ci-dessous, nous pouvons écrire le nom add au lieu de fn dans le nœud Name , puis écrire l'arborescence sur le disque!


Le mode code peut être effectué en écrivant le nom add dans le nœud Name au lieu du nom fn. Ensuite, l'arborescence modifiée peut être écrite sur le disque. Vous pouvez en savoir plus à ce sujet dans la documentation LibCST.

Maintenant que nous nous sommes un peu familiarisés avec les mods de code, regardons un exemple pratique. Les employés d'Instagram travaillent dur pour que la base de code du projet soit entièrement tapée. Kodmody les aide sérieusement dans cette affaire.

Si nous avons un certain ensemble de fonctions non typées qui doivent être typées, nous pouvons essayer de générer les types retournés par elles par l'inférence de type habituelle! Par exemple, si une fonction renvoie des valeurs d'un seul type primitif, nous attribuons simplement ce type de valeur de retour à la fonction. Si la fonction renvoie des valeurs d'un type logique, par exemple, si elle compare quelque chose avec quelque chose ou vérifie quelque chose, alors nous pouvons lui affecter le type de valeur de retour bool . Nous avons constaté qu'au cours des travaux pratiques avec la base de code Instagram, c'est une opération assez sûre.


Connaître les types de valeurs renvoyées par les fonctions

Mais que se passe-t-il si la fonction ne renvoie explicitement aucune valeur ou renvoie implicitement None ? Si la fonction ne renvoie rien de manière explicite, elle peut être affectée au type None .

Contrairement à l'exemple précédent, cela peut être plus dangereux en raison de l'existence de modèles courants utilisés par les développeurs. Par exemple, dans une méthode de classe de base, vous pouvez NotImplemented une exception NotImplemented , et dans les méthodes de sous-classes qui remplacent cette méthode, vous pouvez renvoyer une chaîne. Il est important de noter que toutes ces techniques sont heuristiques, mais les résultats de leur application s'avèrent assez souvent corrects. En conséquence, ils peuvent être considérés comme utiles.


Fonctions qui ne renvoient rien

Extension des modules de code avec Pyre


Allons plus loin. Instagram utilise Pyre, un système de vérification de type statique complet similaire à mypy. L'utilisation de Pyre nous permet de vérifier les types dans une base de code. Et si nous utilisions les données générées par Pyre pour étendre les capacités des codemods? Voici un exemple de ces données. Il est facile de voir qu'il y a presque tout ce dont vous avez besoin pour corriger automatiquement les annotations de type!

 $ pyre ƛ Found 2 type errors! testing/utils.py:7:0 Missing return annotation [3]: Returning `SomeClass` but no return type is specified. testing/utils.py:10:0 Missing return annotation [3]: Returning `testing.other.SomeOtherClass` but no return type is specified. 

Pyre pendant le travail effectue une analyse détaillée de l'ordre d'exécution de chaque fonction. Par conséquent, cet outil peut parfois, avec une probabilité très élevée, faire l'hypothèse qu'une fonction non annotée devrait revenir. Cela signifie que si Pyre pense que la fonction renvoie un type simple, nous attribuons à cette fonction le type de retour. Cependant, maintenant, en potentiel, nous devons également traiter les commandes d'importation. Cela signifie que nous devons savoir si quelque chose est importé ou déclaré localement. Plus tard, nous aborderons brièvement ce sujet.

Quels avantages retirons-nous de l'ajout automatique d'informations de type qui s'affichent facilement dans le code? Eh bien, les types sont de la documentation! Si la fonction est entièrement tapée, le développeur n'aura pas à lire son code pour connaître les fonctionnalités de son appel et les fonctionnalités d'utilisation de ce qu'il retourne.

 def get_description(page: WikiPage) -> Optional[str]:    if page.draft:        return None    return page.metadata["description"]  # <-    ? 

Beaucoup d'entre nous ont rencontré du code Python similaire. La base de code Instagram a également quelque chose de similaire. Si la fonction get_description n'était pas typée, alors vous auriez besoin de regarder dans plusieurs modules afin de savoir ce qu'elle retourne. Dans le même temps, même si nous parlons de fonctions plus simples, dont les types de valeurs de retour sont faciles à dériver, leurs variantes typées sont perçues plus facilement que celles non typées.

De plus, Pyre ne vérifie pas le bon fonctionnement du corps de fonction si la fonction n'est pas complètement annotée. Dans l'exemple suivant, l'appel à some_function échouera. Ce serait bien de le savoir avant que le code ne soit mis en production.

 def some_function(in: int) -> bool:    return in > 0 def some_other_function():    if some_function("bla"): # <-             print("Yay!") 

Dans ce cas, nous pouvons bien découvrir une erreur similaire après la mise en production du code. Le fait est que some_other_function n'a pas d'annotation de type de retour. Si nous l'avions annoté en utilisant nos mécanismes heuristiques en utilisant le type automatiquement déduit None , alors nous aurions découvert un problème avec les types avant qu'il puisse causer des problèmes. Ceci, bien sûr, est un exemple artificiel, mais sur Instagram, ces problèmes sont graves. Si vous avez des millions de lignes de code, alors vous, dans le processus de révision du code, risquez de passer à côté de choses qui semblent complètement évidentes dans un exemple simple.

Dans Instagram, les méthodes ci-dessus basées sur des types déduits automatiquement ont permis de taper environ 10% des fonctions. En conséquence, les utilisateurs n'avaient plus à modifier manuellement des milliers et des milliers de fonctions. Les avantages du code tapé sont évidents, mais cela, dans le contexte de notre conversation, conduit à un autre avantage important. Une base de code entièrement typée ouvre des possibilités encore plus grandes de traitement de code à l'aide de modemods.

Si nous faisons confiance aux annotations de type, cela signifie que Pyre peut nous ouvrir des possibilités supplémentaires. Regardons à nouveau l'exemple où nous avons renommé les fonctions. Et si l'entité que nous renommons est représentée par une méthode de classe et non par une fonction globale?


La fonction est une méthode de classe

Si vous combinez les informations de type reçues de Pyre et le mode de code qui renomme les fonctions, vous pouvez, de façon inattendue, apporter des corrections à l'endroit où la fonction est appelée et où elle est déclarée! Dans cet exemple, puisque nous savons ce qu'il y a sur le côté gauche de la construction a.fn , nous savons également qu'il est sûr de changer cette construction en a.add .

Analyse statique plus avancée



Python a quatre types de portées: portée globale, portée au niveau de la classe et de la fonction, portée imbriquée

L'analyse de la portée nous permet d'utiliser des codemods encore plus puissants. Rappelez-vous l'un des exemples ci-dessus, où nous avons parlé du fait que l'ajout d'annotations de type peut également signifier la nécessité de travailler avec des commandes d'importation? Si le système analyse la portée, cela signifie que nous pouvons savoir quels types utilisés dans le fichier y sont présents grâce aux commandes d'importation, qui sont déclarées localement et qui sont manquantes. De même, si vous savez qu'une variable globale est chevauchée par un argument de fonction, vous pouvez éviter de modifier accidentellement le nom d'un tel argument lorsque vous renommez une variable globale.

Résumé


Dans notre quête pour corriger toutes les erreurs dans le code Instagram, nous avons compris une chose. Elle consiste dans le fait que la recherche du code à corriger est souvent plus importante que le correctif lui-même. Les programmeurs doivent souvent résoudre des tâches simples - comme renommer des fonctions, ajouter des arguments aux méthodes ou diviser des modules en parties. Tout cela est banal, mais la taille de notre base de code signifie qu'une personne ne pourra pas trouver toutes les lignes à modifier. C'est pourquoi il est si important de combiner les capacités des codemods avec une analyse statique fiable. Cela nous permet de trouver avec plus de confiance les parties du code qui doivent être modifiées, ce qui signifie qu'il nous permet de rendre les modes de code plus sûrs et plus puissants.

Chers lecteurs! Utilisez-vous des mods de code?


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


All Articles