tominardi.fr

La qualité dans un projet web, Partie 4 - Code Linting

09 novembre 2022

Suite de notre série d’articles sur la qualité, aujourd’hui, nous allons aborder une autre bonne pratique visant à améliorer votre code et à le rendre maintenable : les linters.

Le dossier

Linter son code : qu’est-ce que ça ça veut dire ?

Ceux qui ont des animaux vont vite comprendre l’image. Pour les autres, je vous demanderais de faire un effort d’imagination, vous devriez vous en sortir. OK, vous y êtes ? Alors imaginons que vous ayez un chat, un bon gros matou bien fluffy. C’est mignon, ces petites bêtes, mais c’est fourni avec un défaut assez ennuyeux : ils ont tendances à perdre leurs poils, et ces derniers ont pour habitude de venir se coller sur vos polos ou vos débardeurs favoris. C’est excessivement ennuyeux.

Bien heureusement, après des siècles d’ingénieries acharnés, le génie humain a inventé des petits rouleaux de ruban adhésif, que l’on peut utiliser pour retirer les poils disgracieux. En anglais, on appelle ce petit outil un “lint roller”, Lint faisant référence à la peluche laissée par nos amis félins.

Et bien les linters, qui sont une famille de logiciels, proposent, grosso-modo, d’effectuer ce passage de “lint roller” sur votre code. Ils font une analyse statique du code (c'est-à-dire que le code n’est pas exécuté) et remontent à l’auteur les potentielles erreurs dans le code. Ce dernier n’a plus qu’à modifier le code pour le rendre plus robuste, plus maintenable sur le long terme.

Un exemple avec pylint

Pour cet article, comme pour la plupart des articles de cette série, Python nous servira d’exemple. Mais ce qui est dit ici est applicable à tous les langages de programmation modernes.

En python, donc, le linter historique s’appelle pylint. C’est un logiciel que l’on peut télécharger sur pypi, avec un petit :

pip install pylint

On l’utilise en lui passant un ou plusieurs fichiers à analyser comme argument.

pylint test.py test2.py

En retour, pylint nous fera une liste de tous les problèmes qu’il a détectés :

************* Module test
test.py:5:0: C0305: Trailing newlines (trailing-newlines)
test.py:1:0: C0114: Missing module docstring (missing-module-docstring)
test.py:1:0: W0611: Unused import os (unused-import)
test.py:2:0: W0611: Unused import sys (unused-import)
************* Module test2
test2.py:1:0: C0114: Missing module docstring (missing-module-docstring)
test2.py:1:0: C0116: Missing function or method docstring (missing-function-docstring)
test2.py:2:11: E0602: Undefined variable 'os' (undefined-variable)
test2.py:3:4: E0602: Undefined variable 'os' (undefined-variable)
test2.py:2:4: W0612: Unused variable 'path' (unused-variable)

Et il nous montre tout ça sans que nous ayons besoin de lancer réellement notre code, ce qui est super pratique. Par exemple, si on a oublié un import, on n'a pas besoin de faire échouer le code pour le voir. Si on a fait une faute de frappe en voulant réutiliser une variable, on le verra aussi tout de suite.

Note : parmi les linters disponibles pour d’autres langages, on trouvera par exemple ESLint pour JavaScript et TypeScript, phplint pour PHP, et même gherkin-lint pour les fichiers gherkin.
En python, flake8 est une alternative à pylint, mais les 2 peuvent être utilisés ensemble.

Quels sont les problèmes que les linters détectent

En une phrase : les linters cherchent à trouver des problèmes de style, mais aussi d’éventuels bugs et des problèmes de qualité.

Sur certains sujets, pylint pourra être redondant avec pycodestyle, que nous avons abordé dans l’article précédent. Par exemple, pylint aussi vérifie la longueur des lignes. Mais pycodestyle est un outil qui vérifie que le code est conforme à la pep8, alors que pylint est globalement agnostique du choix de convention de code choisi et va chercher à remonter des problèmes qu’il estime être des problèmes de qualité, pas de style. En simplifiant : Pylint vise la maintenabilité, quand pycodestyle vise l’uniformité.

Le petit exemple vu juste au-dessus nous donne déjà un très bref aperçu de ce que pylint remonte, je vais compléter un peu ici :

Vous trouverez à cette adresse une liste exhaustive, avec des exemples, des erreurs traitées par pylint. Il y en a un paquet.

Comment personnaliser l’analyse de pylint ?

Alors, c’est bien beau tout ça, mais figurez-vous qu’on n’est pas obligé d’être tout à fait d’accord avec l’analyse de pylint. En effet, c’est assez subjectif, finalement, tout ça.

Un exemple : par défaut, pylint considère comme une erreur l’absence de docstring sur chaque module, chaque fonction, chaque classe et chaque méthode ! Si vous voulez mon avis, c’est un peu radical. Si on suit ça comme un dogme, c’est un coup à créer des docstrings inutiles juste pour faire plaisir à pylint :

def is_a_larger_than_b(a, b):
    “””
    is_a_larger_than_b docstring
    “””
    return a > b


class Pizza:
    “””
    Pizza is a dish of Italian origin consisting of a usually round, flat base of leavened wheat-based
    dough topped with tomatoes, cheese, and often various other ingredients (such as various types of
    sausage, anchovies, mushrooms, onions, olives, vegetables, meat, ham, etc.), which is then baked
    at a high temperature, traditionally in a wood-fired oven.
    “””
    def __init__(self, toppings):
        “””
        constructor
        “””
        self.toppings = toppings

    def get_toppings(self):
        “””
        return toppings
        “””
        return self.toppings

C’est moche. Pourquoi paraphraser un nom de méthode si correctement nommé que la méthode en elle-même en devient auto-documentée ? Alors oui, la docstring, c’est souvent important. Mais pas systématiquement.

De la même manière, pylint établie “arbitrairement” des règles comme :

Ce n’est pas dénué de sens, au contraire. C’est bourré de bonnes pratiques. Mais ce sont des choix arbitraires, et vous avez le droit de vouloir adapter ces règles, d’utiliser les vôtres.

Pylint vous permet donc de créer un fichier .pylintrc à la racine de votre projet, qui sera utilisé pour personnaliser la configuration. Ces réglages peuvent aussi être indiqués dans la section [pylint] du fichier setup.cfg, dans le fichier pyproject.toml, ou de quelques autres manières encore.

Vous pouvez ainsi personnaliser les réglages par défaut, vous pouvez même faire le choix d’ignorer purement et simplement certaines règles, d’exclure certains fichiers ou certains répertoires, etc.

Voici un exemple de fichier de configuration :

max-line-length=120
ignore=migrations
output-format=colorized
disable=C0114,C0115,C0116,W0212,W0105,R0903,W0614,W0221,W0401,W0511
max-args=6
max-attributes=12
notes=FIXME,XXX,TODO
min-similarity-lines=6

Les règles à désactiver peuvent être appelées par leur code ou par leurs noms.

Un dernier point enfin : on peut avoir envie de respecter une règle, mais avoir besoin de la transgresser localement.

Pylint permet, via des commentaires python, de désactiver, sur certaines lignes, une règle qui pourrait poser un problème. Voici un exemple :

from behave import given, when, then  # pylint: disable=no-name-in-module

(cas d’utilisation documenté sur ce ticket de bug de behave https://github.com/behave/behave/issues/641)

C’est très utile, parce que ça permet de faire passer pylint au vert, et ça permet de garder une trace des exceptions qu’on a pu mettre dans le code. Évidemment, si vous en avez beaucoup, si vous avez tendance à régulièrement passer sous silence les mêmes règles, peut-être qu’il vaudrait mieux mettre à jour votre configuration globale.

Pylint en pratique

Une fois configuré, nous allons pouvoir utiliser les analyses pour améliorer notre code.

Imaginons le fichier music.py écrit un vendredi à 17h54:

import os
import sys
class musical_band:
    def __init__(self, bandName):
        self.BandName = bandName

    def addMusician(self, guyName):
        if hasattr(self, 'musicians') is not True:
            self.musicians = []
        self.musicians.append(guyName)

    def getMusiciansBySharingInitials(toto):
        same_initial = []
        initials = [foo[:1] for foo in toto.split(' ')]
        for musician in self.musicians:
            if [foo[:1] for foo in musician.split(' ')] == initials:
                same_initials = []
                same_initials.append(musician)
        return same_initials

Voici les retours que pylint va nous donner :

> pylint --rcfile setup.cfg music.py
************* Module music
music.py:3:0: C0103: Class name "musical_band" doesn't conform to PascalCase naming style (invalid-name)
music.py:5:8: C0103: Attribute name "BandName" doesn't conform to snake_case naming style (invalid-name)
music.py:4:23: C0103: Argument name "bandName" doesn't conform to snake_case naming style (invalid-name)
music.py:7:4: C0103: Method name "addMusician" doesn't conform to snake_case naming style (invalid-name)
music.py:7:26: C0103: Argument name "guyName" doesn't conform to snake_case naming style (invalid-name)
music.py:12:4: C0103: Method name "getMusiciansBySharingInitials" doesn't conform to snake_case naming style (invalid-name)
music.py:12:38: C0104: Disallowed name "toto" (disallowed-name)
music.py:12:4: E0213: Method should have "self" as first argument (no-self-argument)
music.py:14:32: C0104: Disallowed name "foo" (disallowed-name)
music.py:14:39: E1101: Instance of 'musical_band' has no 'split' member (no-member)
music.py:15:24: E0602: Undefined variable 'self' (undefined-variable)
music.py:16:28: C0104: Disallowed name "foo" (disallowed-name)
music.py:13:8: W0612: Unused variable 'same_initial' (unused-variable)
music.py:9:12: W0201: Attribute 'musicians' defined outside __init__ (attribute-defined-outside-init)
music.py:1:0: W0611: Unused import os (unused-import)
music.py:2:0: W0611: Unused import sys (unused-import)

------------------------------------------------------------------
Your code has been rated at 0.00/10 (previous run: 0.00/10, +0.00)

On peut donc traiter chacun de ces retours uns à uns, pour nettoyer notre fichier :

class MusicalBand:
    def __init__(self, band_name):
        self.band_name = band_name
        self.musicians = []

    def add_musician(self, guy_name):
        self.musicians.append(guy_name)

    def get_musicians_by_shared_initials(self, name):
        same_initials = []
        initials = [part[:1] for part in name.split(' ')]
        for musician in self.musicians:
            if [part[:1] for part in musician.split(' ')] == initials:
                same_initials.append(musician)
        return same_initials

Cette fois, pylint va nous donner une note de 10/10 pour ce fichier.

> pylint --rcfile setup.cfg music.py

-------------------------------------------------------------------
Your code has been rated at 10.00/10 (previous run: 9.29/10, +0.71)

À noter que le fichier peut encore être optimisé, mais on en reparlera ;)

Conclusion

Dans le prochain article, nous aborderons la configuration de l’éditeur de code. Nous verrons en particulier comment intégrer le linter directement dans l’IDE afin de ne pas avoir à lancer les commandes à la main.

Dans des articles futurs, nous verrons aussi comment intégrer le linter à d'autres endroits de la chaîne afin d'automatiser et de systématiser les vérifications. Spoiler : on parlera hooks de pré-commit et Intégration Continue.

Cet article est maintenant terminé, il est temps pour vous de reprendre une activité normale.

Tumblr
Pinterest
LinkedIn
WhatsApp

<< La qualité dans un projet web, Partie 3 - Code Styling >> La qualité dans un projet web, Partie 5 - Tests unitaires

2022 - tominardi.fr