# code quality A series of 10 article(s), in reading order. # Markdown linter - corriger les problèmes de formatage dans les fichiers md > Corrigez automatiquement les problèmes de formatage Markdown avec l'outil markdownlint et Docker. Apprenez à vérifier, configurer et utiliser le flag --fix pour des fichiers .md propres et cohérents. --- ![Markdown linter - corriger les problèmes de formatage dans les fichiers md](/img/v2/clean_code.webp) **TL;DR** Cet article montre comment linter et corriger automatiquement des fichiers Markdown avec l'image Docker `peterdavehello/markdownlint` : scanner un projet, ignorer des dossiers comme `vendor` ou `node_modules` via `.markdownlint_ignore`, désactiver certaines règles via `.markdownlint.json`, et appliquer les corrections automatiques avec le flag `--fix`, le tout branché sur un target `make lint`. Vous écrivez des fichiers Markdown `.md` (et vous avez bien raison) et vous voulez juste vérifier (et corriger automatiquement) quelques problèmes comme plusieurs lignes vides d'affilée, un mélange de types de puces (`-` et `*` dans le même document), un titre `#` suivi d'un `###` (autrement dit, vous avez oublié le niveau `##`) et bien d'autres. Il existe un outil pour ça : Markdown lint et une image Docker `peterdavehello/markdownlint`. Voyons comment l'utiliser. *Comme tout linter, il est surtout utile quand vous n'avez plus à y penser : [Git - pre-commit-hooks](/blog/git-precommit) liste un hook `markdownlint` qui s'exécute à chaque commit.* En lançant `docker run --rm -v .:/md peterdavehello/markdownlint markdownlint .`, vous scannez votre dossier courant (récursivement), à la recherche de tous les fichiers Markdown, et vous obtenez la liste des erreurs éventuelles. Et la liste peut être énorme si vous utilisez des dépendances comme, pour PHP, le dossier `vendor` ou `node_modules` pour npm/yarn. Il suffit de les ignorer. ## Ignorer certains fichiers/dossiers {#ignore-some-filesfolders} Créez un fichier appelé `.markdownlint_ignore` à la racine de votre projet avec ce contenu : **.markdownlint\_ignore** ``` node_modules/**/*.markdown node_modules/**/*.md vendor/**/*.markdown vendor/**/*.md ``` Cette fois, lancez `docker run --rm -v .:/md peterdavehello/markdownlint markdownlint --ignore-path .markdownlint_ignore .`. La liste sera probablement plus courte et ne contiendra que vos fichiers. ## Ignorer certaines erreurs {#ignore-some-errors} Vous pouvez ignorer certaines erreurs détectées, comme l'utilisation de HTML inline, en créant un fichier appelé `.markdownlint.json`. Vous trouverez un exemple sur . Voir pour plus d'informations. Pour utiliser votre fichier de configuration, lancez maintenant `docker run --rm -v .:/md peterdavehello/markdownlint markdownlint --config .markdownlint.json --ignore-path .markdownlint_ignore .`. ## Certaines erreurs peuvent être corrigées automagiquement {#some-errors-can-be-fixed-automagically} Certaines erreurs peuvent être corrigées automagiquement, comme les lignes vides multiples ou les espaces superflus en fin de ligne. Le flag ici est `--fix` mais attention : puisque les corrections modifient les fichiers, vous devez aussi ajouter le flag `--user $(id -u):$(id -g)` pour que les fichiers soient mis à jour tout en restant votre propriété. ## On assemble le tout {#put-all-together} L'instruction finale devient : `docker run --rm --user $(id -u):$(id -g) -v .:/md peterdavehello/markdownlint markdownlint --fix --config .markdownlint.json --ignore-path .markdownlint_ignore .` et c'est celle que je lance chaque fois que je publie sur ce blog. ## Se simplifier la vie {#make-it-easy} Vous n'avez évidemment pas besoin de retenir la commande entière. Avec un [Makefile](/blog/tags/makefile) dans votre projet, vous pouvez créer un nouveau `target` qui contient la commande. Pour ce blog par exemple, je lance `make lint` dans mon processus de déploiement. Vous pouvez le voir [ici](https://github.com/cavo789/blog/blob/main/makefile#L42-L45). --- # Rector 1.0.0 🎉🎉🎉, mon ami, mon coach > Découvrez PHP Rector, l'outil gratuit et puissant pour moderniser et refactoriser automatiquement votre code PHP. Améliorez la qualité de votre code et devenez un meilleur développeur PHP. --- ![Rector 1.0.0 🎉🎉🎉, mon ami, mon coach](/img/v2/rector.webp) **TL;DR** Cet article présente Rector, un outil gratuit qui analyse votre code PHP et suggère (ou applique automatiquement) des améliorations de modernisation et de refactorisation. En prenant une simple fonction `sayHello` comme exemple, il montre Rector simplifiant un `if/else` en expression ternaire, supprimant une variable inutile et ajoutant les types corrects ainsi qu'un type de retour — le tout via `vendor/bin/rector process --dry-run --config rector.php`. S'il n'en fallait qu'un, ce serait [Rector](https://github.com/rectorphp/rector). **Rector est extrêmement puissant et brillant et, vous savez quoi, il est gratuit !** Rector est une application formidable pour analyser et mettre à jour automatiquement votre code vers une version donnée de PHP, et cela veut dire aussi : inspecter votre façon de coder. Rector va regarder chaque ligne de code, chaque boucle, chaque test conditionnel, chaque fonction, chaque classe d'un code source PHP et suggérer des améliorations. Dans ce rôle, il ne va pas seulement détecter des améliorations possibles dans mon code (comme un outil d'analyse statique) mais, et c'est le plus gros avantage à mes yeux, il va me montrer comment devenir un meilleur développeur. J'ADORE VRAIMENT RECTOR 💕 Comme beaucoup de gens, j'ai appris PHP tout seul et c'était à l'époque de PHP 5. Comme beaucoup de gens, j'ai certainement pris de mauvaises habitudes et, comme il est impossible de suivre chaque évolution du langage, mes compétences n'augmentent pas au même rythme que le langage. Alors, peut-être que j'utilise encore une syntaxe *à l'ancienne* comme un simple `if ... then ... else` alors qu'en fait ce n'est pas utile. Peut-être que je vais utiliser une variable pour stocker une valeur alors que c'est totalement inutile. Prenons un exemple très simple (`sayHello.php`) : **sayHello.php** ```php **Je suis un excellent développeur. Merci et au revoir.:** > > Bon, en fait, **j'étais un excellent développeur** mais j'ai arrêté de l'être il y a dix ans. Qu'est-ce qui cloche dans mon code ? * `$firstname` est une chaîne, non ? Alors pourquoi ne pas écrire `string $firstname` ?, * depuis PHP 7.x, il existe un truc appelé `ternary operator` ; vous ne le saviez pas ?, * en utilisant le `ternary operator`, la variable `$text` devient inutile et * la fonction retourne une chaîne (la variable `$text`) mais le prototype de la fonction ne le mentionne pas. Alors pourquoi ne pas écrire `function sayHello($firstname = ''): string` ## Voici ce que Rector en dit {#here-is-what-rector-says-about-it} Avant même d'installer quoi que ce soit, voici la réponse que Rector donne sur ce fichier précis : ![Rector is simplifying our sayHello function](./images/rector_say_hello.webp) Un diff coloré — le code tel qu'il est, le code tel qu'il pourrait être — et, en bas de l'écran, une section `Applied rules:`. ## Pourquoi ça fonctionne {#why-it-works} * **Rector raisonne en règles nommées, pas en motifs.** Chaque changement vient avec le nom de la règle qui l'a produit, et ce nom est un lien vers une explication documentée. * **Il parse votre code, il ne fait pas un grep.** Rector construit l'arbre syntaxique du fichier, c'est pour ça qu'il peut voir qu'une variable n'existe que pour être retournée, ou qu'une valeur de retour ne peut être qu'une chaîne. * **Rien ne bouge tant que vous ne l'avez pas décidé.** Avec `--dry-run`, vous obtenez le diff et vos fichiers restent exactement tels qu'ils étaient : lire les suggestions ne coûte rien. ## Installer Rector {#install-rector} > **Vous pouvez essayer Rector sans l'installer:** > > Rector fait partie des outils fournis dans l'image Docker [jakzal/phpqa](/blog/php-jakzal-phpqa) et il est préinstallé dans le [PHP devcontainer](/blog/php-devcontainer). Si vous voulez juste voir ce qu'il dit de votre code, commencez par là. Quand vous utilisez composer pour gérer les dépendances de votre projet, lancez simplement `composer require rector/rector --dev` dans une console. Cela va installer Rector comme dépendance de développement (ce qui est plutôt bien). L'étape suivante consiste à créer un fichier de configuration. Vous pouvez utiliser un fichier standard (pour ça, lancez juste `vendor/bin/rector` et le fichier sera créé) ou créer le vôtre. Je vous suggère de créer un fichier `rector.php` dans votre projet avec ce contenu : **rector.php** ```php withSkip( [ '.git', 'node_modules','vendor' ] ) ->withPhpSets(php82: true) ->withPreparedSets(codeQuality: true, deadCode: true, typeDeclarations: true); ``` Par simplicité, créez le fichier `rector.php` à la racine de votre projet. ## Lancer Rector {#run-rector} La commande qui a produit la capture ci-dessus est `vendor/bin/rector process sayHello.php --dry-run --config rector.php`. Regardons-la de plus près : * `vendor/bin/rector` : c'est l'exécutable à lancer, * `process` : l'action à exécuter, c'est toujours `process`, * `sayHello.php` : le fichier à analyser. Vous pouvez taper `.` pour tout votre projet ou p.ex. `app` quand `app` est un dossier, * `--dry-run` : demande à Rector de seulement afficher les suggestions et de ne pas modifier le fichier et * `--config rector.php` : c'est le nom de notre fichier de configuration. Lancer cette commande revient donc à demander : **s'il te plaît, jette un œil à mon script sayHello.php et montre-moi comment améliorer ma syntaxe.** Et Rector le fait ; vite et sans juger, en montrant les changements qu'il **pourrait** faire et en expliquant pourquoi il les trouve meilleurs. ## Règles appliquées {#applied-rules} Regardez la section `Applied rules:` à la fin de la capture d'écran ; voici les règles avec un lien vers leurs explications : * [SimplifyIfElseToTernaryRector](https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md#simplifyifelsetoternaryrector) * [SimplifyUselessVariableRector](https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md#simplifyuselessvariablerector) * [ReturnTypeFromStrictScalarReturnExprRector](https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md#returntypefromstrictscalarreturnexprrector) * [StrictStringParamConcatRector](https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md#strictstringparamconcatrector) ### SimplifyIfElseToTernaryRector {#simplifyifelsetoternaryrector} En utilisant le `ternary operator`, on peut souvent remplacer une structure `if then else` par un test sur une seule ligne : le `ternary operator`. ```diff function sayHello($firstname = '') { - if ($firstname == '') { - $text = 'Hello World'; - } else { - $text = 'Hello ' . $firstname; - } // highlight-next-line + $text = $firstname == '' ? 'Hello World' : 'Hello ' . $firstname; return $text; } ``` ### SimplifyUselessVariableRector {#simplifyuselessvariablerector} Je pense que tout le monde sera d'accord : stocker la valeur dans une variable `$text` puis retourner cette variable est inutile, ce n'est que de la pollution. ```diff function sayHello($firstname = '') - $text = $firstname == '' ? 'Hello World' : 'Hello ' . $firstname; - return $text; // highlight-next-line + return $firstname == '' ? 'Hello World' : 'Hello ' . $firstname; }; ``` ### ReturnTypeFromStrictScalarReturnExprRector {#returntypefromstrictscalarreturnexprrector} Quand le type de retour peut être déduit (ici, Rector comprend qu'on retourne une chaîne), le prototype de la fonction peut être adapté de cette manière. ```diff class SomeClass { - public function sayHello($firstname = '') // highlight-next-line + public function sayHello($firstname = ''): string { return $firstname == '' ? 'Hello World' : 'Hello ' . $firstname; } } ``` ### StrictStringParamConcatRector {#strictstringparamconcatrector} Quand un paramètre est une chaîne mais n'est pas typé comme tel, Rector le voit et suggère de mettre à jour le prototype ainsi : ```diff class SomeClass { - public function sayHello($firstname = ''): string // highlight-next-line + public function sayHello(string $firstname = ''): string { return $firstname == '' ? 'Hello World' : 'Hello ' . $firstname; } } ``` ## Code final {#final-code} Retour dans notre console : lancer `vendor/bin/rector process sayHello.php --dry-run --config rector.php` va m'afficher la nouvelle version *refactorisée* de mon code : **index.php** ```php **Est-ce que j'en suis content ? Oh que oui !!!:** > > Oui, je suis très content et fier de voir à quel point mon code est maintenant meilleur et plus facile à lire. > > Oui, je suis content d'avoir appris quelque chose et, désormais, je n'utiliserai plus de variable temporaire et je penserai à l'opérateur ternaire. > > Oui, je suis très reconnaissant envers Rector de m'avoir enseigné ces nouvelles approches et de faire de moi un meilleur programmeur. ## Dernière chose, appliquer le changement {#last-thing-make-the-change} Maintenant qu'on a pris le temps d'analyser les règles qui pourraient être appliquées et qu'on les trouve parfaitement adéquates, il est temps de refactoriser pour de vrai : il suffit de retirer le flag `--dry-run` et donc la commande finale à lancer est : `vendor/bin/rector process sayHello.php --config rector.php`. Imaginez ce que ça pourrait donner sur tout mon projet si je lance `vendor/bin/rector process . --dry-run --config rector.php`. ## Conclusion {#conclusion} Un linter vous dit qu'une ligne est mauvaise. Rector vous dit ce qu'il écrirait à la place, et nomme la règle pour que vous puissiez aller lire pourquoi. C'est la différence entre un outil qui vous note et un coach qui vous apprend — et après quelques séances, vous arrêtez d'écrire la variable temporaire inutile dès le départ. Il y a plus de 400 règles à l'heure actuelle ; voyez-les toutes sur , et visitez pour apprendre à configurer `rector.php` pour votre propre projet. Rector n'est qu'une pièce de ma boîte à outils de qualité de code PHP ; voyez aussi [comment reformater du code PHP mal formaté](/blog/online-php-linter). --- # Une image Docker qui fournit les outils d'analyse statique pour PHP > Utilisez l'image Docker jakzal/phpqa pour lancer instantanément des dizaines d'outils d'analyse statique et de qualité de code sur votre codebase PHP, linters et fixers inclus, sans rien installer. --- ![Une image Docker qui fournit les outils d'analyse statique pour PHP](/img/v2/clean_code.webp) **TL;DR** Cet article montre comment lancer toute une suite d'outils d'analyse statique PHP sans rien installer, grâce à l'image Docker `jakzal/phpqa` : `composer normalize` pour réordonner `composer.json`, `composer-unused` pour repérer les dépendances inutilisées, `parallel-lint` pour les erreurs de syntaxe, `php-cs-fixer` et `phpcbf` pour le formatage automatique — et aussi comment fixer un tag correspondant à une version précise de PHP quand vous scannez du code legacy. Depuis des années, j'utilise pour lancer une foule d'outils d'analyse statique sur ma codebase PHP. La liste des outils disponibles est énorme ; voyez par vous-même : [Available tools](https://github.com/jakzal/phpqa?tab=readme-ov-file#available-tools). Dans cet article, nous allons voir comment tirer parti de tous ces outils et augmenter la qualité de nos scripts. D'abord, comme toujours quand il faut découvrir un nouveau logiciel, parlons de son installation. La réponse est simple ici : il n'y a rien à faire. Ah bon ? Vraiment ? Mais oui ! `jakzal/phpqa` est une image Docker, donc pas besoin de l'installer, juste de la lancer une fois. Au premier appel, Docker la téléchargera donc, oui, aucune installation. Merci Docker ! > **Rappel sur la CLI Docker:** > > Pour rappel, la commande Docker run utilisée ressemblera toujours à ceci : > > * `docker run` pour lancer une image Docker (*Ah bon ? Vraiment ?*), > * `-it` pour démarrer Docker en mode interactif, ce qui permet au script exécuté dans le container de vous poser des questions par exemple, > * `--rm` pour demander à Docker de tuer et supprimer le container dès que le script a été exécuté (sinon vous vous retrouverez avec une tonne de containers Docker arrêtés mais pas supprimés ; vous pouvez le vérifier en n'utilisant pas le flag `--rm` puis en lançant `docker container list` dans la console), > * `-v "${PWD}":/project` pour [partager votre dossier courant](/blog/docker-volume) avec un dossier nommé `/project` dans le container Docker (utilisez `${PWD}` sous Linux, `%CD%` sous DOS), > * `-w /project` pour indiquer à Docker que le répertoire courant, dans le container, sera le dossier `/project` > * puis `jakzal/phpqa` qui est le nom de l'image Docker à utiliser (vous pouvez aussi préciser une version comme `python:3.9.18` si nécessaire ; voir ) ## Composer unused : repérer les dépendances inutilisées {#composer-unused} [Composer unused](https://github.com/composer-unused/composer-unused) va **essayer** de comprendre quels packages sont mentionnés dans votre fichier `composer.json` et, peut-être, ne sont plus utilisés. En lançant `docker run -it --rm -v "${PWD}":/project -w /project jakzal/phpqa composer-unused` dans votre projet, vous obtiendrez quelque chose comme ceci : ![composer unused](./images/composer_unused.webp) L'outil détecte donc que je référence `spatie/laravel-db-snapshots` dans mon fichier `composer.json` mais que, dans ma codebase, l'outil (en fait, le namespace ajouté par l'outil) n'est pas utilisé du tout : oui, je peux probablement supprimer cette dépendance. > **Caution:** > > Ce type d'outil peut produire des faux positifs ; faites quelques vérifications avant de supprimer la dépendance. La meilleure solution ici est bien sûr d'avoir plein de [tests unitaires](/blog/pest_tips) pour les lancer avant et après le changement et voir si votre code fonctionne toujours comme prévu. ## Composer normalize : réordonner composer.json {#composer-normalize} [Composer normalize](https://github.com/ergebnis/composer-normalize) va réordonner votre fichier `composer.json` selon les bonnes pratiques. Comme vous le savez, dans un fichier JSON, il n'y a pas d'ordre : vous pouvez définir vos propriétés dans n'importe quel ordre. Par exemple : **composer.json** ```json { "name": "ergebnis/composer-normalize", "description": "Provides a composer plugin for normalizing composer.json.", "license": "MIT", "type": "composer-plugin" } ``` ou **composer.json** ```json { "license": "MIT", "description": "Provides a composer plugin for normalizing composer.json.", "type": "composer-plugin", "name": "ergebnis/composer-normalize" } ``` ou n'importe quoi d'autre. L'exemple ci-dessus ne fait que quatre lignes ; imaginez un fichier de cent lignes. C'est plus logique, non, de retrouver d'abord le `name` de l'outil, puis une petite `description`, la `license`, le `type` (est-ce un projet, une bibliothèque, ...) et ainsi de suite, plutôt que, par exemple, la liste des dépendances en premier. Ok, donc, pour normaliser votre fichier `composer.json`, vous pouvez installer l'outil ou, et c'est l'objectif de cet article, simplement lancer `docker run -it --rm -v "${PWD}":/project -w /project jakzal/phpqa composer normalize` et c'est tout. Le résultat sera un fichier `composer.json` réécrit où les propriétés sont placées dans l'ordre standard. > **Information:** > > Ajoutez `--dry-run` si vous voulez juste voir quels seront les changements, sans modifier le moindre fichier. ## PHP-Parallel-Lint : traquer les erreurs de syntaxe {#php-parallel-lint} Troisième exemple : [PHP-Parallel-Lint](https://github.com/php-parallel-lint/PHP-Parallel-Lint). Cet outil va scanner tous les fichiers `.php` présents dans votre codebase et vérifier qu'il n'y a pas d'erreur de syntaxe, comme un `}` manquant. La commande à lancer ici est `docker run -it --rm -v "${PWD}":/project -w /project jakzal/phpqa parallel-lint . --exclude vendor`. Pensez à ajouter des exclusions comme `--exclude vendor` pour ne pas scanner du code qui n'est pas le vôtre. Comme vous le voyez ci-dessous, en moins de deux secondes, l'outil a vérifié 823 fichiers de ma codebase. Plutôt rapide. ```bash PHP 8.3.3 | 10 parallel jobs ............................................................ 60/823 (7 %) ............................................................ 120/823 (14 %) ............................................................ 180/823 (21 %) ............................................................ 240/823 (29 %) ............................................................ 300/823 (36 %) ............................................................ 360/823 (43 %) ............................................................ 420/823 (51 %) ............................................................ 480/823 (58 %) ............................................................ 540/823 (65 %) ............................................................ 600/823 (72 %) ............................................................ 660/823 (80 %) ............................................................ 720/823 (87 %) ............................................................ 780/823 (94 %) ........................................... 823/823 (100 %) ``` ## PHP-CS-FIXER : reformater le code automatiquement {#php-cs-fixer} Un autre outil, [PHP-CS-FIXER](https://cs.symfony.com/). *The PHP Coding Standards Fixer (PHP CS Fixer) tool fixes your code to follow standards.* Ici la commande sera `docker run -it --rm -v "${PWD}":/project -w /project jakzal/phpqa php-cs-fixer fix` pour reformater toute votre codebase selon les rulesets sélectionnés. Si aucun n'est mentionné, `PSR-12` sera celui par défaut. ## PHP\_CodeSniffer : contrôler les standards de codage {#php\_codesniffer} Un autre outil, [PHP\_CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer). *PHP\_CodeSniffer tokenizes PHP files and detects violations of a defined set of coding standards.* Cette fois, la commande sera `docker run -it --rm -v "${PWD}":/project -w /project jakzal/phpqa phpcbf .` ## Une foule d'autres outils vous attendent {#a-whole-host-of-other-tools-await-you} Comme vous le voyez, on peut utiliser énormément d'outils avec `jakzal/phpqa` sans rien installer. C'est vraiment impressionnant. Vous trouverez bien plus d'outils que ceux décrits dans cet article : rendez-vous sur [Available tools](https://github.com/jakzal/phpqa?tab=readme-ov-file#available-tools) et voyez lesquels peuvent vous aider. Deux suites logiques une fois vos outils choisis : les lancer **avant** de committer, grâce aux [hooks Git pre-commit](/blog/git-precommit), et les relancer dans votre CI, comme décrit dans [GitLab - Running Docker-out-of-Docker in your CI](/blog/gitlab-docker-out-of-docker). ## Changer de version de PHP {#change-php-versions} Imaginons que votre codebase soit du legacy (pas écrit pour PHP 8.3 mais, par exemple, pour PHP 7.4). Une instruction comme `docker run -it --rm -v "${PWD}":/project -w /project jakzal/phpqa parallel-lint . --exclude vendor` (comme vue plus haut) va scanner le code avec PHP 8.3 (situation fin mars 2024) et vous pouvez récolter plein d'erreurs puisque vous utilisez un outil PHP 8.3x sur une codebase PHP 7.4x. Comment résoudre ça ? En fait, c'est très simple. Comme vous le savez, en utilisant `docker run [...] jakzal/phpqa [...]` vous demandez à Docker d'utiliser la version `latest` de l'image, et cette version change. Chaque fois que le propriétaire de l'image publie une version plus récente, l'image `latest` est mise à jour et, peut-être, dans quelques mois, ce sera PHP 8.4 par défaut. Donc, pour régler notre problème : il faut utiliser un tag spécifique pour PHP 7.4. *Et si les outils se plaignent de vos réglages PHP plutôt que de votre code, voyez [Update php.ini when using a Docker image](/blog/docker-php-ini).* Rendez-vous simplement sur et tapez `7.4` dans la zone `Filter Tags`. ![Tags](./images/tags.webp) Vous y trouverez quelques tags. Vous pouvez maintenant lancer `docker run -it --rm -v "${PWD}":/project -w /project jakzal/phpqa:1.80-php7.4-alpine parallel-lint . --exclude vendor` pour exécuter le linter PHP 7.4. --- # Clean code - Linux Bash - Réduire au maximum le nombre de paramètres d'une fonction > Appliquez le Clean Code à vos scripts Linux Bash. Découvrez comment éviter d'avoir trop de paramètres de fonction grâce à une solution simple et robuste : le tableau associatif. --- ![Clean code - Linux Bash - Réduire au maximum le nombre de paramètres d'une fonction](/img/v2/bash.webp) **TL;DR** Cet article applique à Bash le principe de clean code qui consiste à limiter le nombre de paramètres d'une fonction. En Bash, on ne peut pas passer un objet : on passe donc un seul tableau associatif (`declare -A arr=()`) à la fonction. L'appelant n'a plus à se soucier de l'ordre des arguments positionnels, et de nouveaux champs peuvent être ajoutés plus tard sans casser les appels existants. Un des concepts du clean code est d'éviter d'avoir trop de paramètres de fonction (je dirais que quatre paramètres, c'est déjà trop). Quand vous programmez dans un langage plus évolué que Linux Bash, il est facile de contourner le problème. Par exemple, en PHP, si je dois appeler une fonction et lui passer plusieurs paramètres, je vais créer un objet qui sera mon seul paramètre et cet objet aura alors plusieurs propriétés. Ainsi, par exemple, si je dois passer des données comme un nom, un prénom, une date de naissance, un genre, etc., je vais créer un objet `person`, définir mes propriétés et voilà, je n'ai plus qu'un seul paramètre à passer à ma fonction. Dans la plupart des cas, c'est une très bonne solution. Mais comment faire en Linux Bash ? Impossible de passer un objet... je vais donc lui passer un tableau associatif. L'astuce, pour un script Bash, est de définir un tableau associatif (créé avec la commande `declare -A arr=()`) et de passer ce tableau en paramètre à la fonction. Voici un exemple : **curl.sh** ```bash #!/usr/bin/env bash # let script exit if an unused variable is used or if a command fails set -o nounset set -o errexit function runCurl() { local -n arrCall=$1 # The endpoint (the URL to request) is mandatory local -r endpoint="${arrCall["endpoint"]}" # The other properties are optional; initialize to an empty string or any other default values [[ ! -v arrCall[header] ]] && local -r header="" || local -r header="${arrCall["header"]}" [[ ! -v arrCall[method] ]] && local -r method="GET" || local -r method="${arrCall["method"]}" [[ ! -v arrCall[payload] ]] && local -r payload="" || local -r payload="${arrCall["payload"]}" [[ ! -v arrCall[proxy] ]] && local -r proxy="" || local -r proxy="${arrCall["proxy"]}" [[ ! -v arrCall[token] ]] && local -r token="" || local -r token="${arrCall["token"]}" # And now I can work with my multiple variables exactly as I would have done if I'd passed them all as parameters. # # Here is an example: cmd="curl -X ${method} ${endpoint} --no-progress-meter" [[ -n ${payload} ]] && cmd="${cmd} --data ${payload}" [[ -n ${header} ]] && cmd="${cmd} --header ${header}" [[ -n ${token} ]] && cmd="${cmd} ${token}" [[ -n ${proxy} ]] && cmd="${cmd} --proxy ${proxy}" if ! json="$(eval "${cmd}")"; then echo "CURL has failed" exit 1 fi echo "${json}" return 0 } declare -A arr=( [endpoint]=https://jsonplaceholder.typicode.com/todos/5 [token]="--header 'Authorization:Bearer ABCDEF'" ) # As you know, when using variables you should use the syntax like ${variable} # But, here below, no. We're passing our array and we should not use the `$` runCurl arr ``` Un des gros avantages, c'est que vous n'avez plus à vous préoccuper de la position des arguments (ah oui, donc le premier paramètre c'est l'url, le deuxième la méthode HTTP, le troisième c'est, euh ? le proxy ? ah non, mince). De nouveaux paramètres peuvent aussi être créés plus tard sans impacter les scripts existants. Tout est beaucoup plus propre avec un tableau associatif, et plus robuste aussi. Deux autres habitudes qui gardent une base de code Bash propre : [trier vos fonctions](/blog/linux-sort-functions-in-script) pour les retrouver facilement, et [les couvrir avec des tests unitaires bats](/blog/bats-unit-tests) pour qu'un refactoring comme celui-ci se fasse sans retenir votre souffle. --- # SQL - Outil de formatage > Nettoyez facilement du code SQL legacy illisible avec un simple outil de formatage. Rendez vos requêtes SQL immédiatement lisibles et démarrez votre revue de code. --- ![SQL - Outil de formatage](/img/v2/sql_format.webp) **TL;DR** Cet article présente l'outil SQL Formatter de l'auteur, gratuit et self-hosted (sources sur GitHub, démo hébergée sur sql-formatter.avonture.be). Il transforme des requêtes SQL legacy illisibles écrites sur une seule ligne en instructions correctement indentées et lisibles — une première étape utile avant de relire du vieux code de base de données. Face à du code legacy, il est souvent utile de le reformater pour le rendre lisible. Et à partir de là, l'étude du code peut commencer. Il existe énormément d'outils de reformatage pour [json](/blog/json-lint), [php](/blog/online-php-linter), javascript et d'autres langages, mais beaucoup moins pour une requête écrite en SQL. Copiez/collez simplement `SELECT LAT_N, CITY, TEMP_F FROM STATS, STATION WHERE MONTH = 7 AND STATS.ID = STATION.ID ORDER BY TEMP_F` dans l'outil et vous obtenez ```sql SELECT LAT_N, CITY, TEMP_F FROM STATS, STATION WHERE MONTH = 7 AND STATS.ID = STATION.ID ORDER BY TEMP_F ``` Récupérez mon outil **SQL Formatter** et ses sources sur . L'outil est accessible en ligne : ![Demo](./images/sql_formatter_demo.gif) J'ai créé quelques autres outils self-hosted du même genre, chacun dédié à une seule tâche : [JSON - Online linter](/blog/json-lint) et [Excel Formula Beautifier](/blog/excel-formatter). --- # JSON - Linter en ligne > Un outil de lint JSON en ligne, simple et rapide. Collez votre chaîne JSON pour l'afficher sous forme d'arborescence claire et lisible, avec folding du code. --- ![JSON - Linter en ligne](/img/v2/json.webp) **TL;DR** Cet article présente l'outil gratuit JSON Linter de l'auteur (sources sur GitHub, hébergé sur jsonlint.avonture.be) pour coller une chaîne JSON et la visualiser sous forme d'arborescence lisible et repliable — pratique à la fois pour vérifier la syntaxe du JSON que vous écrivez et pour inspecter les réponses d'une API. Il mentionne aussi l'extension Chrome « JSON Formatter », qui formate automatiquement les pages JSON dans le navigateur. Exactement comme mon [SQL - Formatting tool](/blog/sql-formatter), il est toujours utile d'avoir dans ses favoris un outil qui permet de copier/coller une chaîne de caractères JSON et de l'afficher sous forme d'arborescence, avec ou sans folding du code. Je l'utilise assez régulièrement quand j'écris une chaîne JSON et que je veux vérifier qu'il n'y a pas d'erreurs de syntaxe (lint) ou, à l'inverse, par exemple, quand j'appelle une API qui renvoie du JSON et que je veux analyser le code reçu. Dans ces cas-là, la fonction de repli est très pratique. *Deux alternatives selon le contexte : [`jq`](/blog/linux-jq) quand vous êtes déjà dans une console, et [Rendering a JSON file as a mind map](/blog/json-crack) quand la structure compte plus que les valeurs. Pour produire du JSON de test plutôt que le lire, voyez [JSON - Faker & Mockup](/blog/json-faker).* Retrouvez mon outil **JSON Linter** et ses sources sur . L'outil est accessible en ligne : ![Demo](./images/json_lint_demo.gif) ## Extension Chrome {#chrome-addon} Si vous utilisez Chrome et que la sortie d'une page web est une chaîne JSON, l'extension [JSON Formatter](https://chromewebstore.google.com/detail/json-formatter/bcjindcccaagfpapjjmafapmmgkkhgoa) l'affichera immédiatement sous une forme lisible. Par exemple, au lieu d'obtenir cette page : ![Json webpage](./images/json_page.webp) Chrome affichera ceci : ![Chrome addon](./images/chrome_addon.webp) D'autres outils JSON qui valent le détour : [afficher un fichier JSON sous forme de carte mentale](/blog/json-crack) et [générer de fausses données JSON](/blog/json-faker). --- # Formater du code PHP mal formaté > Besoin de nettoyer rapidement du code PHP en vrac ? Découvrez le meilleur linter PHP en ligne, Pint-express (basé sur Laravel Pint), pour formater et embellir votre code instantanément. Aucune installation nécessaire ! --- ![Formater du code PHP mal formaté](/img/v2/clean_code.webp) **TL;DR** Cet article présente Pint-express (benjamincrozat.com/pint-express), un formateur PHP en ligne basé sur Laravel Pint, pour nettoyer instantanément du code PHP mal formaté : il suffit de le coller dans une zone de texte, sans rien installer. Pour des projets suivis, il recommande plutôt de brancher PHP-CS-Fixer ou PHP\_CodeSniffer dans l'éditeur, le processus de build ou le pipeline CI. Vous récupérez du vieux code PHP ; vous voulez répondre à une question posée sur un forum et la personne qui a posté la question PHP n'a pas pris soin de la formater correctement ; ... les occasions sont trop nombreuses où la qualité syntaxique du code peut être pourrie. Vous aimeriez avoir un outil en ligne pour récupérer rapidement du code avec une mise en forme beaucoup plus soignée, juste par copier-coller. Sans rien installer et sans prise de tête. Par exemple, comment rendre le code ci-dessous plus propre en cinq secondes ? **my\_collection.php** ```php $val){$object->$key = $val;} $this->storage[] = $object; }} } ``` Il existe énormément de *linters* sur internet, mais celui-ci est peut-être l'un des meilleurs : . Il est basé sur l'outil [Laravel Pint](https://laravel.com/docs/13.x/pint) (mais absolument pas limité au code Laravel). Voici donc à quoi ressemblait le code avant : ![Code PHP correctement formaté](./images/before.webp) Rendez-vous sur [le formateur Pint de Hexmos](https://hexmos.com/freedevtools/tldr/common/pint), copiez le code dans la zone de texte **Code** du script et laissez le reformatage se faire : ![Code PHP correctement formaté](./images/after.webp) Bien mieux. Note : il existe d'autres outils comme par exemple . Un outil en ligne est parfait pour un copier-coller ponctuel. Pour vos propres projets, intégrez plutôt le formateur dans votre workflow : [jakzal/phpqa](/blog/php-jakzal-phpqa) vous donne `php-cs-fixer` et `phpcbf` sans rien installer, et [Git - pre-commit-hooks](/blog/git-precommit) les exécute avant chaque commit. ## Si vous êtes développeur PHP {#if-youre-a-php-developer} ... alors utilisez des outils comme [PHP-CS-Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) ou [PHP\_CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer), ou bien d'autres. Voyez aussi mon [Docker image that provides static analysis tools for PHP](/blog/php-jakzal-phpqa). Pensez à ajouter ces outils à votre éditeur (il existe de nombreuses extensions VSCode), à votre workflow (par exemple via des actions `make` locales), ou à ajouter ces étapes à un pipeline distant. --- # Python - Les outils de qualité de code > Améliorez la qualité de votre code Python ! Découvrez 9 outils d'analyse statique essentiels comme Pylint, Black et mypy, ainsi qu'un workflow QA optimal pour un code plus propre et sans bugs. --- ![Python - Les outils de qualité de code](/img/v2/clean_code.webp) **TL;DR** Cet article recense la chaîne d'outils de qualité de code Python de l'auteur — Pylint, Autoflake, isort, Vulture, pydocstyle, mypy, Pyright, Black et Prospector, plus Ruff comme alternative tout-en-un ultra rapide — avec, pour chacun, la commande CLI exacte et le fichier de configuration. Ils sont enchaînés, du plus rapide/plus fondamental en premier, dans une cible `make qa` qui s'arrête au premier échec et n'affiche « CONGRATULATIONS » que si toutes les vérifications passent. Si vous êtes un programmeur qui se respecte, vous ne pouvez pas développer sans outils d'analyse de code. Pour les programmeurs PHP, vous en connaissez déjà beaucoup (`rector`, `phpstan`, `phan`, `phpcs`, ...). Voyez mes articles précédents [Rector 1.0.0, my friend, my coach](/blog/php-rector) et [Docker image that provides static analysis tools for PHP](/blog/php-jakzal-phpqa). Et pour Python ? ## Le résultat : une commande, neuf vérifications, un seul point d'échec {#the-result-one-command-nine-checks-one-clear-failure-point} `make qa` enchaîne Pylint, Autoflake, isort, Vulture, pydocstyle, mypy, Pyright, Black et Prospector — l'outil le plus rapide d'abord — et s'arrête au tout premier échec. Voici à quoi ressemble une exécution propre : ```bash $ make qa Step Tool Run? Techno Description -------------------------------------------------------------------------------------------------------------- 1/9 RUNNING make Pylint Lint python scripts using Pylint (https://pypi.org/project/pylint/) Your code has been rated at 10.00/10 (previous run: 10.00/10, +0.00) 2/9 RUNNING make Autoflake Detect unused variables and unused imports (https://pypi.org/project/autoflake/) 3/9 RUNNING make isort Sort import statement using isort (https://pycqa.github.io/isort/) 4/9 RUNNING make Vulture Find dead Python code (https://github.com/jendrikseipp/vulture) 5/9 RUNNING make Pydocstyle pydocstyle is a static analysis tool for checking compliance with Python docstring conventions (https://www.pydocstyle.org/en/stable/) 6/9 RUNNING make Mypy Mypy is a program that will type check your Python code (https://github.com/python/mypy/) Success: no issues found in 12 source files 7/9 RUNNING make Pyright Pyright is a full-featured, standards-based static type checker for Python (https://github.com/microsoft/pyright) 0 errors, 0 warnings, 0 informations 8/9 RUNNING make Black Format the script using Black (https://black.readthedocs.io/en/stable/) All done! ✨ 🍰 ✨ 12 files left unchanged. 9/9 RUNNING make Prospector Inspects Python source files and provides information about type and location of classes, methods, ... (https://github.com/prospector-dev/prospector/) Messages Found: 0 CONGRATULATIONS!!! Not the slightest error or notice detected, that's .. amazing! 🥳 🎉 🎊 🤩 🕺 💃 👏 ``` Rien de vert à chercher : le silence (ou une note comme le `10.00/10` de Pylint) signifie que la vérification est passée. Il en rate une seule et la chaîne s'arrête là — pas de **CONGRATULATIONS**, et une seule chose à corriger avant de relancer. ## Pourquoi ça fonctionne {#why-it-works} * Les outils s'exécutent du plus rapide/plus fondamental en premier : inutile de lancer un vérificateur de types lent si le code ne se parse même pas — c'est pourquoi Pylint est 1/9 et Autoflake, qui nettoie les imports, est 2/9. * La chaîne s'arrête au premier échec : une exécution de `make qa`, une chose claire à corriger, pas neuf rapports à recouper. * `make qa` est un point d'entrée unique : c'est la même cible que vous lancez à la main, que lance un hook pre-commit et que lance la CI — j'y reviens en conclusion. ## Mise en place {#installation} Je suis un grand fan des outils statiques de qualité de code, et voici ma courte liste, câblée ensemble avec une action [makefile](/blog/tags/makefile) appelée `qa` : **makefile** ``` # cspell:disable .PHONY: qa qa: @printf "\e[1;37m%-35s%-10s%-10s\e[m%s\n" " Step Tool" "Run?" "Techno" "Description" @echo "--------------------------------------------------------------------------------------------------------------" @printf "\e[1;37m%-35s%-10s%-10s\e[m%s\n" " 1/9 RUNNING make Pylint" "Lint python scripts using Pylint (https://pypi.org/project/pylint/)" @pylint . --rcfile .config/.pylintrc @printf "\e[1;37m%-35s%-10s%-10s\e[m%s\n" " 2/9 RUNNING make Autoflake" "Detect unused variables and unused imports (https://pypi.org/project/autoflake/)" @autoflake --remove-unused-variables --remove-all-unused-import --recursive . @printf "\e[1;37m%-35s%-10s%-10s\e[m%s\n" " 3/9 RUNNING make isort" "Sort import statement using isort (https://pycqa.github.io/isort/)" @isort . @printf "\e[1;37m%-35s%-10s%-10s\e[m%s\n" " 4/9 RUNNING make Vulture" "Find dead Python code (https://github.com/jendrikseipp/vulture)" @vulture --min-confidence 100 . @printf "\e[1;37m%-35s%-10s%-10s\e[m%s\n" " 5/9 RUNNING make Pydocstyle" "pydocstyle is a static analysis tool for checking compliance with Python docstring conventions (https://www.pydocstyle.org/en/stable/)" @pydocstyle --config=.config/.pydocstyle @printf "\e[1;37m%-35s%-10s%-10s\e[m%s\n" " 6/9 RUNNING make Mypy" "Mypy is a program that will type check your Python code (https://github.com/python/mypy/)" @mypy --config-file .config/.mypy.ini . @printf "\e[1;37m%-35s%-10s%-10s\e[m%s\n" " 7/9 RUNNING make Pyright" "Pyright is a full-featured, standards-based static type checker for Python (https://github.com/microsoft/pyright)" @pyright --project .config/pyright.json @printf "\e[1;37m%-35s%-10s%-10s\e[m%s\n" " 8/9 RUNNING make Black" "Format the script using Black (https://black.readthedocs.io/en/stable/)" @black --config .config/black.toml . @printf "\e[1;37m%-35s%-10s%-10s\e[m%s\n\n" " 9/9 RUNNING make Prospector" "Inspects Python source files and provides information about type and location of classes, methods, ... (https://github.com/prospector-dev/prospector/)" @prospector . --profile .config/prospector.yaml --pylint-config-file .config/.pylintrc @printf "%s\n" "CONGRATULATIONS!!!" @printf "%s\n" "Not the slightest error or notice detected, that's .. amazing!" @printf "%s\n" "🥳 🎉 🎊 🤩 🕺 💃 👏" ``` Dès qu'une erreur est détectée, le script s'arrête. Vous ne verrez le message **CONGRATULATIONS**, exactement comme montré ci-dessus, que si les neuf vérifications réussissent. ## Plus de démos : les neuf outils, un par un {#more-demos-the-nine-tools-one-by-one} ### 1. Pylint, l'analyseur statique {#1-pylint} > > > [Extension VSCode](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint) > > Pylint est un analyseur de code statique pour Python 2 ou 3. La dernière version supporte Python 3.9.0 et supérieur. > > Pylint analyse votre code sans réellement l'exécuter. Il cherche les erreurs, applique un standard de codage, repère les code smells et peut suggérer comment le code pourrait être refactorisé. Commençons par le commencement : assurez-vous que votre code Python n'a pas d'erreurs de syntaxe — pas de mauvaise indentation, pas de `:` oublié à la fin d'une instruction de contrôle (comme un `if` ou un `for`). Je le lance comme ceci : `pylint . --rcfile .config/.pylintrc`. **.config/.pylintrc** ``` [MASTER] ; Pickle collected data for later comparisons. persistent=yes ; List of plugins (as comma-separated values of python modules names) to load, ; usually to register additional checkers. load-plugins= pylint.extensions.check_elif, pylint.extensions.bad_builtin, pylint.extensions.docparams, pylint.extensions.for_any_all, pylint.extensions.set_membership, pylint.extensions.code_style, pylint.extensions.overlapping_exceptions, pylint.extensions.typing, pylint.extensions.redefined_variable_type, pylint.extensions.comparison_placement, ; Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the ; number of processors available to use. jobs=0 ; When enabled, pylint would attempt to guess common misconfiguration and emit ; user-friendly hints instead of false-positive error messages. suggestion-mode=yes ; Minimum supported python version py-version = 3.13.0 ; Specify a score threshold to be exceeded before program exits with error. fail-under=9.50 [FORMAT] max-line-length=120 ``` ### 2. Autoflake, le nettoyage des imports inutilisés {#2-autoflake} > > > Autoflake supprime les imports et les variables inutilisés du code Python. Il s'appuie sur pyflakes pour cela. > > Par défaut, Autoflake ne supprime que les imports inutilisés des modules faisant partie de la bibliothèque standard. (D'autres modules peuvent avoir des effets de bord rendant leur suppression automatique risquée.) La suppression des variables inutilisées est également désactivée par défaut. > > Autoflake supprime aussi par défaut les instructions pass inutiles. Je le lance comme ceci : `autoflake --remove-unused-variables --remove-all-unused-import --recursive .` ### 3. isort, le tri des imports {#3-isort} > > > [Extension VSCode](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) > > `isort` trie vos imports, pour que vous n'ayez pas à le faire. > > isort est un utilitaire / une bibliothèque Python qui trie les imports par ordre alphabétique et les sépare automatiquement en sections et par type. Il fournit un utilitaire en ligne de commande, une bibliothèque Python et des plugins pour divers éditeurs afin de trier rapidement tous vos imports. Il nécessite Python 3.7+ pour fonctionner mais sait aussi formater du code Python 2. Je le lance comme ceci : `isort .` Note : j'ai aussi configuré mon VSCode avec ce réglage `"python.sortImports.args": ["--profile", "black"]` pour que les instructions d'import soient triées automatiquement pendant que je code. ### 4. vulture, la détection de code mort {#4-vulture} > > > Vulture trouve le code inutilisé dans les programmes Python. C'est utile pour nettoyer et trouver des erreurs dans de grandes bases de code. Si vous lancez Vulture à la fois sur votre bibliothèque et sur votre suite de tests, vous pouvez repérer le code non testé. > > À cause de la nature dynamique de Python, les analyseurs statiques comme Vulture risquent de manquer du code mort. Par ailleurs, du code appelé uniquement de manière implicite peut être signalé comme inutilisé. Malgré tout, Vulture peut être un outil très utile pour une meilleure qualité de code. Je le lance comme ceci : `vulture --min-confidence 100 .` > **Danger:** > > Attention avec Vulture : son algorithme détecte beaucoup de faux positifs, c'est pourquoi, dans mon processus d'automatisation, j'ai utilisé `--min-confidence 100` pour être sûr de n'avoir **que** du vrai code / de vraies variables inutilisés. Note : j'ai aussi configuré mon VSCode avec les réglages ci-dessous pour que, pendant que je code, VSCode me signale les éléments inutilisés et que je puisse réagir immédiatement. **.vscode/settings.json** ```json "python.analysis.diagnosticSeverityOverrides": { "reportUnusedClass": "information", "reportUnusedFunction": "information", "reportUnusedImport": "information", "reportUnusedVariable": "information" }, ``` ### 5. pydocstyle, la qualité des docstrings {#5-pydocstyle} > > > `pydocstyle` est un outil d'analyse statique qui vérifie le respect des conventions de docstrings Python. Cet outil vérifie la qualité de vos commentaires, comme les descriptions de vos fonctions et, par exemple, s'assure que si vous avez une fonction avec deux arguments, votre description explique les deux arguments (il y a un contrôle sur le nom et sur le type). Je le lance comme ceci : `pydocstyle --config=.config/.pydocstyle` **.config/.pydocstyle** ``` [pydocstyle] ignore = D100,D203,D205,D212,D213,D400,D404,D406,D407,D413,D415 match = .*\.py ``` > **Caution:** > > Le repository GitHub de pydocstyle a été archivé en novembre 2023 et n'est plus activement maintenu. La communauté recommande de migrer vers **Ruff** (voir la section « Sous le capot » ci-dessous), qui couvre des vérifications de docstrings équivalentes via son jeu de règles compatible `pydocstyle`. ### 6. mypy, le typage statique {#6-mypy} > > > Mypy est un vérificateur de types statique pour Python. > > Les vérificateurs de types aident à s'assurer que vous utilisez correctement les variables et les fonctions de votre code. Avec mypy, ajoutez des annotations de type (PEP 484) à vos programmes Python, et mypy vous avertira quand vous utiliserez ces types de façon incorrecte. > > Python est un langage dynamique : d'ordinaire, vous ne voyez les erreurs de votre code qu'au moment de l'exécuter. Mypy est un vérificateur statique, il trouve donc les bugs de vos programmes sans même les exécuter ! Je le lance comme ceci : `mypy --config-file .config/.mypy.ini .` **.config/.mypy.ini** ```ini [mypy] show_error_codes = true ; First we turn on *all the checks*, and then we turn off those that are too annoying. strict = True cache_dir = /tmp/mypy no_implicit_optional = true strict_equality = true warn_redundant_casts = true warn_unused_ignores = true python_version = 3.13 ``` ### 7. Pyright, l'autre vérificateur de types {#7-pyright} > > > Pyright est un vérificateur de types statique complet et basé sur les standards pour Python. Il est conçu pour la performance et peut être utilisé sur de grandes bases de code Python. Je l'utilise comme ceci : `pyright --project .config/pyright.json` **.config/pyright.json** ```json { "include": [ "." ], "pythonVersion": "3.13", "pythonPlatform": "Linux" } ``` ### 8. Black, le formatage automatique {#8-black} > > > [Extension VSCode](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter) > > En utilisant Black, vous acceptez de céder le contrôle sur les détails du formatage manuel. En échange, Black vous donne de la vitesse, du déterminisme et la liberté de ne plus subir les remarques de pycodestyle sur le formatage. Vous économiserez du temps et de l'énergie mentale pour des sujets plus importants. > > Black accélère la revue de code en produisant les diffs les plus petits possibles. Le code passé à Black a la même allure quel que soit le projet que vous lisez. Le formatage devient transparent au bout d'un moment et vous pouvez vous concentrer sur le contenu. Je l'utilise comme ceci : `black --config .config/black.toml .` **.config/black.toml** ```toml [tool.black] # The same as in prospector.yaml and .pylintrc line-length = 120 # We'll format the code for Python 3.13.0; we can add multiple versions (the ones supported by # the script, all versions separated by a comma like in ['py312', 'py313'] (run "black --help" to get them) target-version = ['py313'] ``` ### 9. prospector, l'inspection globale {#9-prospector} > > > Inspecte les fichiers source Python et fournit des informations sur le type et l'emplacement des classes, méthodes, etc. Je l'utilise comme ceci : `prospector . --profile .config/prospector.yaml --pylint-config-file .config/.pylintrc` **.config/prospector.yaml** ```yaml strictness: high test-warnings: true doc-warnings: false pycodestyle: options: max-line-length: 120 disable: - D100 - D203 - D205 - D212 - D213 - D400 - D404 - D406 - D407 - D413 - D415 bandit: run: true dodgy: run: true pyflakes: run: true mccabe: run: true ``` ## Sous le capot (passez votre chemin si vous voulez juste l'utiliser) {#under-the-hood-skip-this-if-you-just-want-to-use-it} ### En bonus - Ruff, un remplaçant potentiel pour plusieurs outils {#extra---ruff-a-potential-replacement-for-several-tools} > > > Ruff, un linter et formateur de code Python extrêmement rapide, écrit en Rust. > > [Extension VSCode](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) Pour l'instant, j'ai trop peu d'expérience avec Ruff, je préfère donc l'ajouter en **bonus**. La documentation indique qu'il est extrêmement rapide et je dois avouer que, la première fois que je l'ai lancé, je pensais que rien ne s'était passé (vraiment trop rapide). J'ai introduit une erreur volontaire dans mon code et elle a été correctement détectée : oui, l'outil est extrêmement rapide. D'après la documentation de Ruff, cet outil peut remplacer complètement Pylint (le linter), Autoflake (suppression des imports/variables inutilisés) et Black (l'outil de formatage), mais il ne remplace pas MyPy ni Pyright (doc). Je l'utilise comme ceci : `ruff format --cache-dir /tmp/ruff --config .config/pyproject.toml .` et `ruff check --cache-dir /tmp/ruff --config .config/pyproject.toml .` **.config/pyproject.toml** ```toml [tool.ruff] exclude=[] # The same as in prospector.yaml, .pylintrc, black.toml and pyproject.toml line-length = 120 indent-width = 4 ``` ## Conclusion {#conclusion} Neuf outils, une commande, un seul point d'échec — `make qa` transforme le « est-ce que j'ai pensé à tout vérifier ? » en une exécution déterministe unique, la vérification la plus rapide/fondamentale d'abord, qui s'arrête dès que quelque chose cloche. Le lancer à la main suppose quand même que vous y pensiez. Deux façons de l'automatiser : [Git - pre-commit-hooks](/blog/git-precommit) refuse le commit quand une vérification échoue, et [Dagger.io - Using dagger to automate your CI workflows](/blog/dagger-python) exécute exactement les mêmes étapes en local et dans votre CI. --- # Git - pre-commit-hooks > Vous détestez les pipelines CI/CD qui échouent ? Découvrez comment utiliser les hooks Git pre-commit, en particulier le framework pre-commit, pour appliquer vos standards de qualité de code en local avant de pusher. --- ![Git - pre-commit-hooks](/img/v2/clean_code.webp) **TL;DR** Évitez les pipelines CI/CD en échec en attrapant les erreurs avant de committer. Cet article explique comment utiliser les hooks Git pre-commit pour appliquer vos standards de qualité de code en local. Il propose un guide pratique pour installer et utiliser le framework `pre-commit`, un outil polyvalent capable de gérer des hooks multi-langages. Vous allez voir comment configurer des hooks pour un projet Python afin de détecter automatiquement des problèmes comme un formatage incohérent, et comment les installer et les lancer. L'article explique aussi où trouver d'autres hooks et quand il est légitime de les contourner. Vous aimez le code propre, non ? Et vous détestez recevoir un email de votre outil de versioning (par exemple Github ou GitLab) qui vous annonce que votre dernier commit n'est pas passé, que le formatage de votre code est mauvais ; et tout ça parce que vous avez laissé un espace de trop en fin de ligne ou utilisé des simples quotes au lieu de doubles (ou l'inverse), par exemple. Vous avez pushé vos modifications, vous êtes déjà passé à autre chose, peut-être un autre projet, et boum, deux heures après votre dernier commit (*parce que le serveur CI traitait plein de pipelines avant le vôtre*), boum, vous recevez un *Your last commit has failed, #gnagnagna*. Je hais ça autant que j'aime le code propre. Alors, que faire pour éviter ça ? La réponse est simple ! Avant chaque push, on devrait lancer les mêmes outils d'analyse de code que ceux exécutés dans la CI, c'est-à-dire `phplint`, `php-cs-fixer`, `phpcbf`, `phan`, `phpstan`, ... (pour PHP, tous regroupés dans l'[jakzal/phpqa Docker image](/blog/php-jakzal-phpqa)) ou `pylint`, `mypy`, `prospector`, `black`, `ruff`, ... (pour Python, voir [Python - Code Quality tools](/blog/python-qa)) ou `shellcheck` et `shellformat` (pour Bash sous Linux) ou ... On devrait, mais y pense-t-on chaque fois ? Malheureusement non. Voyons comment corriger ça. ## Le résultat : un commit reformaté avant même d'exister {#the-result-a-commit-reformatted-before-it-even-happens} Voici `pre-commit` en train d'attraper (et de corriger) une violation de formatage, sur un projet de démo que vous allez construire plus bas : ```bash $ pre-commit run --all-files ``` ```bash [INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks. [WARNING] repo `https://github.com/pre-commit/pre-commit-hooks` uses deprecated stage names (commit, push) which will be removed in a future version. Hint: often `pre-commit autoupdate --repo https://github.com/pre-commit/pre-commit-hooks` will fix this. if it does not -- consider reporting an issue to that repo. [INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks. [INFO] Once installed this environment will be reused. [INFO] This may take a few minutes... Check Yaml...........................................(no files to check)Skipped Fix End of Files.....................................(no files to check)Skipped Trim Trailing Whitespace.............................(no files to check)Skipped black................................................(no files to check)Skipped # git add . # pre-commit run --all-files Check Yaml...............................................................Passed Fix End of Files.........................................................Passed Trim Trailing Whitespace.................................................Passed black....................................................................Failed - hook id: black - files were modified by this hook reformatted main.py All done! ✨ 🍰 ✨ 1 file reformatted. ``` Vous avez vu la ligne **reformatted main.py** ? Une incohérence de quotes (`"double"` vs `'single'`) a été corrigée silencieusement par `black`, directement dans le stage local : ![Black a reformaté notre script](./images/black.webp) Pas de run CI, pas d'email deux heures plus tard — la violation ne quitte jamais votre machine. ## Pourquoi ça fonctionne {#why-it-works} Git propose deux étapes de hooks, `pre-` et `post-` ; un hook `pre-commit` s'exécute *avant* la création du commit et peut l'annuler complètement si quelque chose échoue. Parmi les outils bâtis sur ce principe ([husky](https://github.com/typicode/husky), [pre-commit](https://github.com/pre-commit/pre-commit), [grumphp](https://github.com/phpro/grumphp), [CaptainHook](https://github.com/captainhookphp/captainhook)), cet article utilise **pre-commit** : * Il est multi-langages — PHP, Python, Bash, tout ce dont le projet a besoin — un seul fichier de configuration. * Il lance les mêmes outils que votre CI (`black`, `pylint`, `phpstan`, ...), juste plus tôt et gratuitement, sans runner à attendre. * Une fois installé, il revérifie automatiquement à chaque `git commit` — rien à retenir, c'est imposé. ## Installation {#installation} On va créer un nouveau dossier temporaire, lancer `git init` pour initialiser un projet, puis créer une image Docker et lancer un container pour notre démo. D'abord, créez un dossier temporaire et placez-vous dedans, puis initialisez-le comme repository git (on va travailler hors ligne mais, oui, pour utiliser les hooks git pre-commit, il faut un projet git). ```bash $ mkdir /tmp/hooks && cd $_ $ git init ``` Il nous faut trois fichiers : un `Dockerfile` pour créer notre image Docker Python, un `compose.yaml` pour définir quelques paramètres et `main.py` comme exemple de script Python. **Dockerfile** ```docker # syntax=docker/dockerfile:1 FROM python:3.13-slim AS base RUN apt-get -y update \ && apt-get install --yes --no-install-recommends git \ && rm -rf /var/lib/apt/lists/* WORKDIR "/app/src" # Keep the container running ENTRYPOINT ["tail", "-f", "/dev/null"] ``` **compose.yaml** ```yaml services: app_python: build: . volumes: - .:/app/src ``` **main.py** ```python print("I'm your Python code") print('Who you, who are you?') ``` On va créer notre image Docker et un container avec cette seule commande : `docker compose up --detach --build`. Et, maintenant, on entre dans le container en lançant : `docker compose exec app_python /bin/sh`. Et on peut lancer notre script : ```bash $ python main.py I'm your Python code Who you, who are you? ``` ### Installer pre-commit {#installing-pre-commit} > Pour un projet Python, c'est vraiment facile, il suffit de lancer `pip install pre-commit`. ### Ajouter un fichier de configuration {#adding-a-configuration-file} Simple aussi, créez un fichier appelé `.pre-commit-config.yaml` avec ce contenu : **.pre-commit-config.yaml** ```yaml repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.3.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black rev: 24.10.0 hooks: - id: black ``` ### Déclencher le hook manuellement {#manually-fire-the-hook} `pre-commit` peut être déclenché manuellement mais il faut avoir des fichiers dans le stage local git. Dans cet article, on a créé quelques fichiers, lancez `git add .` simplement pour les mettre dans le stage local git. Maintenant, pour démarrer manuellement tous les contrôles définis dans le fichier yaml, lancez simplement `pre-commit run --all-files` — c'est exactement la commande et la sortie montrées en haut de cet article. Vous avez remarqué ma coquille ? ```python print("I'm your Python code") print('Who you, who are you?') ``` La première fois, j'ai utilisé des doubles quotes (dans le premier `print`) alors que j'ai utilisé des simples dans le second. J'ai donc (volontairement) créé une violation de code et l'outil `black` l'a vue et a reformaté `main.py`, exactement comme montré plus haut. ## Autres démos {#more-demos} ### Installer les hooks {#install-hooks} Ok, l'idée n'était pas de déclencher les hooks pre-commit à la main, non ? Lancez simplement `pre-commit install` et, à partir de maintenant, chaque fois que vous lancerez `git commit`, les contrôles `pre-commit` seront d'abord effectués et c'est seulement quand tous les contrôles réussissent (c'est-à-dire qu'ils renvoient tous un code de sortie `0`) que votre commit sera autorisé. ### Quelques hooks en plus {#a-few-more-hooks} Il existe énormément de hooks et vous pouvez même créer les vôtres : * [Code spell](https://github.com/codespell-project/codespell/blob/main/.pre-commit-config.yaml#L70), *Check code for common misspellings* * [Git leaks](https://github.com/gitleaks/gitleaks/blob/master/.pre-commit-hooks.yaml), *Find secrets with Gitleaks* * [Git lint](https://github.com/jorisroovers/gitlint), *Linting for your git commit messages* * [Markdown format](https://github.com/hukkin/mdformat/blob/master/.pre-commit-config.yaml), *CommonMark compliant Markdown formatter* * [Markdownlint](https://github.com/markdownlint/markdownlint/blob/main/.pre-commit-hooks.yaml), *Markdown lint tool* * [Ruff](https://github.com/astral-sh/ruff-pre-commit/blob/main/.pre-commit-hooks.yaml), *A pre-commit hook for Ruff.* * [Shell check](https://github.com/shellcheck-py/shellcheck-py?tab=readme-ov-file#as-a-pre-commit-hook), *python3/pip3 wrapper for installing shellcheck* * [Trufflehog](https://github.com/trufflesecurity/trufflehog/blob/main/.pre-commit-config.yaml), *Find, verify, and analyze leaked credentials* ### Une configuration Python 3.13 prête à l'emploi {#a-ready-made-python-313-config} Pour un projet Python 3.13, voici mon fichier `.pre-commit-config.yaml` : **.pre-commit-config.yaml** ```yaml default_language_version: python: python3.13 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-ast # Python linter - Simply check whether files parse as valid python. - id: check-docstring-first # No code before the initial docstring - id: check-json # JSON linter - Attempts to load all json files to verify syntax. - id: check-toml # TOML linter - Attempts to load all TOML files to verify syntax. - id: check-xml # XML linter - Attempts to load all XML files to verify syntax. - id: check-yaml # YAML linter - Attempts to load all yaml files to verify syntax. - id: check-merge-conflict # Check for files that contain merge conflict strings. - id: end-of-file-fixer # Makes sure files end in a newline and only a newline. - id: file-contents-sorter files: \.config/*.txt$ # Sort all lines in any .config/*.txt file (like cspell.txt f.i.) args: [--ignore-case] - id: no-commit-to-branch args: [--branch, main] # Don't allow to push directly on the main branch - id: requirements-txt-fixer # Sorts entries in requirements.txt - id: trailing-whitespace # Trims trailing whitespace. - repo: https://github.com/psf/black rev: 24.10.0 hooks: - id: black # Python black formatter - repo: local hooks: - id: isort # isort (should be installed using `pip install isort` or mentioned in the `requirements.txt` file) name: isort entry: isort language: system types: [python] require_serial: true args: ["--line-length 120"] - id: mypy # Mypy (should be installed using `pip install mypy` or mentioned in the `requirements.txt` file) name: mypy entry: mypy language: system types: [python] require_serial: true args: ["--config-file=.config/mypy.ini"] - id: pylint # Pylint (should be installed using `pip install pylint` or mentioned in the `requirements.txt` file) name: pylint entry: pylint language: system types: [python] require_serial: true args: ["--rcfile=.config/.pylintrc"] ``` ## Sous le capot (passez ce point si vous voulez juste l'utiliser) {#under-the-hood-skip-this-if-you-just-want-to-use-it} > **Note:** > > Si la mécanique vous intéresse, affichez simplement le fichier `.git/hooks/pre-commit`. L'instruction `pre-commit install` a configuré git pour exécuter un petit script Bash appelé `.git/hooks/pre-commit`. ### Chercher des hooks et écrire les vôtres {#search-for-hooks-and-write-your-own} Prenez le temps de parcourir pour en découvrir quelques-uns, ou cherchez sur avec des queries comme `context:global file:^\.pre-commit-hooks\.yaml$ "types: [python]"` par exemple ([lien direct](https://sourcegraph.com/search?q=context:global+file:%5E%5C.pre-commit-hooks%5C.yaml%24+%22types:+%5Bpython%5D%22\&patternType=keyword\&sm=0)). Comme illustré sur , vous pouvez ajouter des hooks locaux. Imaginez : vous avez déjà installé un outil comme `prospector` (pour Python) ou `phpstan` (pour PHP). Ces outils sont installés sur votre machine (vous pouvez donc les appeler en ligne de commande). Il suffit alors d'ajouter un nouveau hook comme ceci : **.pre-commit-config.yaml** ```yaml - repo: local hooks: - id: prospector name: prospector entry: prospector language: system ``` Vous pouvez aussi passer des arguments : **.pre-commit-config.yaml** ```yaml - repo: local hooks: - id: mypy # Mypy (should be installed using `pip install mypy`) name: mypy entry: mypy language: system types: [python] require_serial: true args: ["--config-file=.config/mypy.ini"] ``` ## Astuce : --no-verify {#tip---no-verify} Dans certaines situations, vous devez pusher vos modifications même s'il reste des violations de code. Disons que c'est votre dernière heure avant trois semaines de vacances et que vous travaillez seul sur une branch comme `feat-user-profile`. Vous voulez pusher vos modifications et profiter de la pause. Dans ce cas, vous pouvez ajouter le flag `--no-verify`, par exemple `git commit -m "wip: not yet finished" --no-verify`. Et là, les hooks pre-commit ne seront pas exécutés, donc vos modifications seront committées. > **Le flag --no-verify:** > > N'utilisez ce flag que si vous savez exactement ce que vous faites. Ce serait une très mauvaise idée de faire ça, par exemple, sur la branch `dev` si vous travaillez en équipe. ## Conclusion {#conclusion} Les emails de CI en échec de l'introduction disparaissent dès que les mêmes contrôles tournent en local, avant que le commit n'existe — `pre-commit` transforme « attendre que le serveur vous le dise » en « le savoir immédiatement ». Installez-le une fois (`pre-commit install`) et tous vos futurs commits sont couverts, dans tous les langages que votre projet mélange. Gardez `--no-verify` sous le coude pour la rare exception assumée, et consultez [Python - Code Quality tools](/blog/python-qa) pour les outils sur lesquels reposent les hooks de la démo de cet article. --- # Validez votre schéma OpenAPI face aux standards REST Belgif > Validez le schéma OpenAPI de votre FastAPI face aux standards REST Belgif avec Docker, avec des corrections pas à pas pour les erreurs de linting les plus courantes. --- ![Validez votre schéma OpenAPI face aux standards REST Belgif](/img/v2/belgif.webp) **TL;DR**Cet article est un guide pratique pour rendre vos applications FastAPI conformes aux standards REST Belgif, un ensemble de règles strictes imposé aux API du secteur public belge. Comme la génération par défaut du schéma OpenAPI de FastAPI entre souvent en conflit avec ces règles, l'auteur montre comment mettre en place le linter officiel Belgif dans un container Docker isolé pour repérer facilement les erreurs de conformité. L'article fournit aussi des solutions concrètes, pas à pas, et des fonctions Python utilitaires pour résoudre les violations les plus fréquentes — rétrograder la version d'OpenAPI, supprimer les propriétés `title` redondantes, corriger les conventions de nommage camelCase — afin que la documentation de votre API passe la validation sans accroc. Construire des API est relativement facile, mais construire des API *conformes* et *interopérables* est un tout autre défi. Si vous travaillez sur des projets pour le gouvernement belge ou le secteur public, vous avez probablement déjà croisé les **[standards REST Belgif](https://www.belgif.be/specification/rest/api-guide/)**. Ces directives complètes garantissent que les API des différentes institutions restent cohérentes, prévisibles et faciles à consommer. Les frameworks modernes comme **FastAPI** sont excellents pour construire rapidement des API et générer automatiquement la documentation OpenAPI, mais leur génération de schéma par défaut ne colle pas toujours parfaitement aux règles de linting strictes de Belgif. Dans cet article, nous allons voir comment utiliser le linter OpenAPI officiel de Belgif via Docker et comment valider votre application FastAPI face à quelques erreurs potentielles. Plus important encore, nous allons parcourir des solutions pratiques, pas à pas, pour résoudre les erreurs de linting les plus courantes causées par les comportements par défaut de FastAPI, afin que votre API obtienne un certificat de bonne santé. *Avant d'entrer dans le vif du sujet, si vous cherchez des conseils généraux sur la conception d'API robustes, jetez un œil à mon article précédent :* [API REST - How to write good APIs](/blog/php-api-tips). **Quick Jump:** [Le linter en action](#seeing-the-linter-run) | [Installation — Créer une application bidon](#installation--create-a-dummy-application) ## Le linter en action {#seeing-the-linter-run} Une fois le container `belgif-lint` en place (voir la section Installation ci-dessous), vérifier un fichier `openapi.json` tient en deux commandes : ```bash $ curl -s http://localhost:8000/openapi.json > openapi.json ``` ```bash $ docker compose run --rm belgif-lint ``` Cela produira quelque chose comme ceci : ```txt [INFO] Scanning for projects... [INFO] [INFO] ------------------< org.apache.maven:standalone-pom >------------------- [INFO] Building Maven Stub Project (No POM) 1 [INFO] --------------------------------[ pom ]--------------------------------- [INFO] [INFO] --- belgif-rest-guide-validator:3.1.0:validate-openapi (default-cli) @ standalone-pom --- [INFO] Validating following files:[/app/openapi.json] [INFO] Found kmodule: jar:file:/root/.m2/repository/io/github/belgif/rest/guide/validator/belgif-rest-guide-validator-rules/3.1.0/belgif-rest-guide-validator-rules-3.1.0.jar!/META-INF/kmodule.xml [INFO] Creating KieModule for artifact io.github.belgif.rest.guide.validator:belgif-rest-guide-validator-rules:3.1.0 [INFO] Artifact io.github.belgif.rest.guide.validator:belgif-rest-guide-validator-rules:3.1.0 has executable model [INFO] Start creation of KieBase: defaultKieBase [...] OpenApi validation summary: 30 violations and 0 ignored violations. [MANDATORY] [err-problem] Each error response of each operation SHOULD have a media type "application/problem+json" 2 occurrences: openapi.json ln 1 #/paths/items/{item_id}/get/responses/422 -- [Operation: GET /items/{item_id}] openapi.json ln 1 #/paths/items//get/responses/422 -- [Operation: GET /items/] [MANDATORY] [evo-object] In a request or response body, if any, you MUST always return a JSON object (and not e.g. an array) as a top level data structure to support future extensibility. 5 occurrences: openapi.json ln 1 #/paths//get/responses/200/content/application/json openapi.json ln 1 #/paths/items//get/responses/200/content/application/json openapi.json ln 1 #/paths/items/{item_id}/get/responses/200/content/application/json openapi.json ln 1 #/paths/items/{item_id}/get/responses/422/content/application/json openapi.json ln 1 #/paths/items//get/responses/422/content/application/json [MANDATORY] [oas-comp] Component names SHOULD use UpperCamelCase notation. For abbreviations as well, all letters except the first one should be lowercased. 1 occurrence: openapi.json ln 1 #/components/schemas/HTTPValidationError [MANDATORY] [oas-contra] OpenAPI 3.1 improves upon OpenAPI 3.0, but to avoid interoperability problems it SHOULD NOT be used yet because it is not yet widely supported by most tooling. 1 occurrence: # [MANDATORY] [oas-descr] The title property of a Schema MUST NOT be used. 10 occurrences: openapi.json ln 1 #/paths/items//get/parameters/0/schema openapi.json ln 1 #/components/schemas/ValidationError/properties/msg openapi.json ln 1 #/components/schemas/HTTPValidationError openapi.json ln 1 #/components/schemas/ValidationError/properties/type openapi.json ln 1 #/paths/items/{item_id}/get/parameters/1/schema openapi.json ln 1 #/components/schemas/ValidationError/properties/loc openapi.json ln 1 #/paths/items/{item_id}/get/parameters/0/schema openapi.json ln 1 #/components/schemas/ValidationError openapi.json ln 1 #/paths/items//get/parameters/1/schema openapi.json ln 1 #/components/schemas/HTTPValidationError/properties/detail [MANDATORY] [openapi-opid] A unique operationId MUST be specified on each operation. It SHOULD have a lowerCamelCase value following common programming naming conventions for method (function) names. 3 occurrences: openapi.json ln 1 #/paths/items//get -- OperationId not lowerCamelCase: read_items_items__get openapi.json ln 1 #/paths//get -- OperationId not lowerCamelCase: read_root__get openapi.json ln 1 #/paths/items/{item_id}/get -- OperationId not lowerCamelCase: read_item_items__item_id__get [MANDATORY] [path-param] Path parameters SHOULD use lowerCamelCase notation. 1 occurrence: openapi.json ln 1 #/paths/items/{item_id}/get/parameters/0 [MANDATORY] [prb-defaul] A default Problem response SHOULD be added to each operation with media type "application/problem+json" 3 occurrences: openapi.json ln 1 #/paths/items/{item_id}/get -- [Operation: GET /items/{item_id}] openapi.json ln 1 #/paths/items//get -- [Operation: GET /items/] openapi.json ln 1 #/paths//get -- [Operation: GET /] [MANDATORY] [uri-format] The server url should follow the format [/pathPrefix]/apiName/vMajorVersion optionally prefixed by https://hostname[:port] or http://localhost[:port]. The apiName must consist of one or more path segments in lowerCamelCase. 1 occurrence: #/servers/0 [MANDATORY] [uri-notat] Trailing slashes MUST NOT be used. 2 occurrences: openapi.json ln 1 #/paths/ openapi.json ln 1 #/paths/items/ [RECOMMENDED] [hlth-res] Each REST API SHOULD expose a GET /health operation which returns the availability of the API’s functionality for its clients. 1 occurrence: openapi.json ln 1 #/paths [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [...] ``` > **Référez-vous au site officiel:** > > À partir de maintenant, référez-vous à pour apprendre à gérer les erreurs signalées par l'outil. **Bonus - Se débarrasser des warnings internes de Belgif** En exécutant le linter Belgif, vous allez peut-être rencontrer de nombreux warnings comme ceux-ci : **warnings.txt** ```txt [WARNING] In an eval expression [ApiFunctions.existsPathWithPathParamAfter($path.getIdentifier(), parserResult)] : '$path' comes from previous pattern 'PathDefinition'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient. [WARNING] In an eval expression [ApiFunctions.isMediaTypeIncluded($contentType, Set.of("application/json"))] : '$contentType' comes from previous pattern 'String'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient. [WARNING] In an eval expression [isObjectType($responseSchema, parserResult)] : '$responseSchema' comes from previous pattern 'Schema'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient. [WARNING] In an eval expression [!hasItemsArray($responseSchema, parserResult)] : '$responseSchema' comes from previous pattern 'Schema'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient. [WARNING] In an eval expression [ApiFunctions.existsPathWithPathParamAfter($path.getIdentifier(), parserResult)] : '$path' comes from previous pattern 'PathDefinition'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient. [WARNING] In an eval expression [ApiFunctions.isMediaTypeIncluded($contentType, Set.of("application/json"))] : '$contentType' comes from previous pattern 'String'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient. [WARNING] In an eval expression [isObjectType($responseSchema, parserResult)] : '$responseSchema' comes from previous pattern 'Schema'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient. [WARNING] In an eval expression [hasItemsArray($responseSchema, parserResult)] : '$responseSchema' comes from previous pattern 'Schema'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient. [WARNING] In an eval expression [!hasCollectionResponse($responseSchema, parserResult)] : '$responseSchema' comes from previous pattern 'Schema'. 'parserResult' comes from previous pattern 'Parser$ParserResult'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient. [WARNING] In an eval expression [$schema != null] : '$schema' comes from previous pattern 'Schema'. Consider placing the constraint in the pattern and removing the eval if possible, as eval is not performance-efficient. [WARNING] More eval warnings will be suppressed... [WARNING] No correct line number found for: /servers/0 ``` Ces warnings concernent le linter lui-même (ils viennent en fait d'un outil appelé `Drools`) et donc **ils n'ont rien à voir avec votre code**. C'est juste de la pollution visuelle pour nous ; la seule chose que nous pouvons faire, c'est les masquer. Regardez le nouveau fichier ci-dessous : **compose.yaml** ```yaml services: app: build: . ports: - "8000:80" volumes: - ./main.py:/code/main.py # Belgif Validator Service belgif-lint: image: maven:3.9.9-eclipse-temurin-17-alpine volumes: - .:/app:ro - maven-cache:/root/.m2 working_dir: /app # 1. We override the entrypoint to use a shell (required for pipes) entrypoint: ["/bin/sh", "-c"] # 2. We use 'set +e' to not close the terminal if an error is met # Use style.color=always to keep ANSI colors while piping output to grep # Use 2>&1 to merge STDERR and STDOUT so line ordering is kept during the pipe command: > "set +e && mvn \ io.github.belgif.rest.guide.validator:belgif-rest-guide-validator-maven-plugin:3.1.0:validate-openapi \ -Dstyle.color=always \ -Drest-guide-validator.files=openapi.json \ -Drest-guide-validator.outputTypes=CONSOLE \ 2>&1 \ | grep -v 'eval expression' \ | grep -v 'More eval warnings will be suppressed' \ | grep -v 'comes from previous pattern' \ | grep -v 'No correct line number'" volumes: maven-cache: ``` En résumé, nous allons lancer une commande personnalisée où nous récupérons STDERR et STDOUT dans un seul stream de sortie, puis nous exécutons quelques commandes `grep` pour purger certains messages de la sortie (ceux que nous ne pouvons pas résoudre). ## Les standards Belgif {#belgif-standards} Les [standards REST Belgif](https://github.com/belgif/rest-guide-validator) sont définis sur et constituent *un effort collaboratif de plusieurs institutions gouvernementales belges, à l'origine sous l'égide du G-Cloud, avant de passer chez Belgif, le Belgian Interoperability Framework. Son objectif est d'améliorer la compatibilité entre les services RESTful proposés par les institutions publiques ou toute autre organisation adoptant ces directives.* Si vous voulez vérifier si votre API est conforme, vous pouvez utiliser le [belgif-rest-guide-validator](https://github.com/belgif/rest-guide-validator) comme documenté dans la section [Tools](https://www.belgif.be/specification/rest/api-guide/#openapi-tools). ## Installation — Créer une application bidon {#installation--create-a-dummy-application} Si vous n'en avez pas encore, cliquez simplement sur `Generate install script` ci-dessous et collez la ligne de commande dans un terminal, puis appuyez sur la touche `Enter` pour créer la structure du projet dans un dossier `/tmp/fastapi` sur votre disque. **Project: /tmp/fastapi** > **Note:** > > Now, please run 'docker compose up --build -d' to > create your API website. Wait a few and open your browser, surf to > to open your site and see your API OpenData documentation. **main.py** ```python from fastapi import FastAPI from typing import Union app = FastAPI() @app.get("/") def read_root(): return {"Hello": "World"} @app.get("/items/{item_id}") def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} @app.get("/items/") def read_items(skip: int = 0, limit: int = 10): return {"skip": skip, "limit": limit} ``` **compose.yaml** ```yaml services: app: build: . ports: - "8000:80" volumes: - ./main.py:/code/main.py ``` **Dockerfile** ```docker # Start from an official Python image. FROM python:3.11-slim # Set the working directory in the container. WORKDIR /code # Copy the requirements file and install dependencies. # We'll install fastapi and uvicorn (the server). RUN pip install --no-cache-dir 'fastapi[all]' # Copy the application code into the container. COPY ./main.py /code/main.py # Expose the port the app runs on. EXPOSE 80 # Command to run the application. CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] ``` Le code de `main.py` définit : * Un endpoint racine `/` qui retourne un simple JSON. * Un endpoint `/items/{item_id}` qui accepte un entier `item_id` comme paramètre de path et un paramètre de query optionnel `q` de type chaîne. * Un endpoint `/items/` qui accepte les paramètres de query `skip` et `limit` pour la pagination. Pour le lancer, exécutez simplement `docker compose up --build -d` dans votre terminal : ```bash $ docker compose up --build -d ``` Rendez-vous ensuite sur `http://localhost:`8000`/docs` pour voir votre application tourner. Vous pouvez la tester avec `curl` ou votre navigateur : 1. **Endpoint racine :** ```bash $ curl http://localhost:8000 ``` Vous devriez voir : `{"Hello":"World"}` 2. **Item avec paramètre de path et de query :** ```bash $ curl "http://localhost:8000/items/5?q=somequery" ``` Vous devriez voir : `{"item_id":5,"q":"somequery"}` 3. **Items avec pagination :** ```bash $ curl "http://localhost:8000/items/?skip=0&limit=20" ``` Vous devriez voir : `{"skip":0,"limit":20}` ## Ajouter le linter Belgif {#adding-the-belgif-linter} Le plus simple est d'utiliser un container Docker. Dans l'exemple de code ci-dessous, regardez la nouvelle version de `compose.yaml` : nous avons ajouté un service `belgif-lint` basé sur `maven`. **Project: /tmp/fastapi** > **Note:** > > Now, please run 'docker compose up --build -d' to > create your API website. Wait a few and open your browser, surf to > to open your site. **main.py** ```python from fastapi import FastAPI from typing import Union app = FastAPI() @app.get("/") def read_root(): return {"Hello": "World"} @app.get("/items/{item_id}") def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} @app.get("/items/") def read_items(skip: int = 0, limit: int = 10): return {"skip": skip, "limit": limit} ``` **compose.yaml** ```yaml services: app: build: . ports: - "8000:80" volumes: - ./main.py:/code/main.py # Belgif Validator Service belgif-lint: image: maven:3.9.9-eclipse-temurin-17-alpine volumes: - .:/app:ro - maven-cache:/root/.m2 working_dir: /app command: > mvn io.github.belgif.rest.guide.validator:belgif-rest-guide-validator-maven-plugin:3.1.0:validate-openapi -Drest-guide-validator.files=openapi.json -Drest-guide-validator.outputTypes=CONSOLE volumes: maven-cache: ``` **Dockerfile** ```docker # Start from an official Python image. FROM python:3.11-slim # Set the working directory in the container. WORKDIR /code # Copy the requirements file and install dependencies. # We'll install fastapi and uvicorn (the server). RUN pip install --no-cache-dir 'fastapi[all]' # Copy the application code into the container. COPY ./main.py /code/main.py # Expose the port the app runs on. EXPOSE 80 # Command to run the application. CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] ``` Une fois que vous avez relancé `docker compose up --build -d` pour créer le container `belgif-lint`, vous êtes prêt à lancer la vérification montrée en début d'article — il vous faut juste d'abord un fichier `openapi.json` sur le disque, ce que produit exactement la première commande là-haut. ## Astuces FastAPI {#fastapi-tips} > **Mon expérience personnelle:** > > En intégrant belgif dans un projet FastAPI, j'ai rencontré plusieurs erreurs de linting qui ont demandé du dépannage manuel. Le guide ci-dessous décrit la solution que j'ai mise au point pour les résoudre. **Notez que cette approche repose sur mes propres constats et peut différer des bonnes pratiques standard.** ### oas-tags - Gérer les tags {#oas-tags---managing-tags} Vous obtiendrez l'erreur `[MANDATORY] [oas-tags] Each tag used on an operation SHOULD also be declared in the top level tags list of the OpenAPI document, with an optional description.` lorsque vous utilisez un ou plusieurs tags dans vos endpoints et que ces tags ne sont pas déclarés. Pour résoudre ça, vous devez utiliser l'attribut `openapi_tags` comme illustré ci-dessous : **tags.py** ```python # Define the metadata for your tags tags_metadata: list[dict[str, str]] = [ { "name": "Retrieving data", "description": "Operations related to fetching and querying data.", }, { "name": "Healthcheck", "description": "Endpoints to monitor the health status of the application.", }, ] app = FastAPI( # ... openapi_tags=tags_metadata, # belgif [oas-tags] # ... ) ``` ### oas-contra - N'utilisez pas encore OpenAPI 3.1 {#oas-contra---dont-use-openapi-31-yet} Si vous obtenez `[MANDATORY] [oas-contra] OpenAPI 3.1 improves upon OpenAPI 3.0, but to avoid interoperability problems it SHOULD NOT be used yet because it is not yet widely supported by most tooling.`, vous pouvez régler cette erreur en rétrogradant la version d'OpenAPI utilisée par FastAPI comme ceci : ```python app = FastAPI( # ... openapi_version="3.0.2", # belgif [oas-contra] ) ``` ### oas-descr - La propriété title doit être supprimée {#oas-descr---title-property-has-to-be-removed} Vous obtiendrez `[MANDATORY] [oas-descr] The title property of a Schema MUST NOT be used.` quand FastAPI génère à la fois une propriété `title` et une propriété `description` pour vos objets. La solution est d'ajouter une fonction utilitaire et de supprimer le `title` (puisque `description` est toujours généré) : Code partiel : ```python if "components" in openapi_schema and "schemas" in openapi_schema["components"]: for schema in openapi_schema["components"]["schemas"].values(): _remove_titles(schema) ``` Voir l'utilitaire fourni plus loin dans l'article. ### oas-comp - Les noms de composants doivent utiliser la notation UpperCamelCase {#oas-comp---component-names-should-use-uppercamelcase-notation} Si vous obtenez l'erreur `[MANDATORY] [oas-comp] Component names SHOULD use UpperCamelCase notation. For abbreviations as well, all letters except the first one should be lowercased.`, vous devrez ajouter une fonction de renommage pour transformer par exemple `HTTPValidationError` en `HttpValidationError`. Cela peut se faire par exemple comme ceci (code partiel) : ```python def _fix_schema_names(openapi_schema: dict[str, Any]) -> None: """ Renames schemas that violate Belgif's naming conventions. Specifically targets 'HTTPValidationError' (abbreviations must be camel-cased like 'Http', not 'HTTP'). """ components: Any = openapi_schema.get("components", {}) schemas: Any = components.get("schemas", {}) # Fix: HTTPValidationError -> HttpValidationError if "HTTPValidationError" in schemas: # 1. Move the definition to the new key schemas["HttpValidationError"] = schemas.pop("HTTPValidationError") # 2. Update all references ($ref) in the entire document to point to the new key _update_refs( openapi_schema, "#/components/schemas/HTTPValidationError", "#/components/schemas/HttpValidationError" ) ``` Voir l'utilitaire fourni plus loin dans l'article. ### openapi-opid - lowerCamelCase pour operationId {#openapi-opid---lowercamelcase-for-operationid} Le message `[MANDATORY] [openapi-opid] A unique operationId MUST be specified on each operation. It SHOULD have a lowerCamelCase value following common programming naming conventions for method (function) names.` vous signale l'utilisation d'une mauvaise syntaxe. Code partiel : ```python def _fix_operation_ids(openapi_schema: dict[str, Any]) -> None: """ Iterates over all paths and converts snake_case operationIds to camelCase. """ paths: Any = openapi_schema.get("paths", {}) for path_item in paths.values(): for operation in path_item.values(): if isinstance(operation, dict) and "operationId" in operation: old_id: str = cast(str, operation["operationId"]) operation["operationId"] = _to_camel_case(old_id) ``` ### L'utilitaire openapi.py {#the-openapipy-helper} L'utilitaire fourni ci-dessous peut vous aider à résoudre les erreurs `oas-descr` et `oas-comp`. Le fichier `main.py` montre comment appeler cet utilitaire. **main.py** ```python from helpers.openapi import configure_custom_openapi # Your code... app = FastAPI( # ... ) # Call the helper and pass the FastAPI object configure_custom_openapi(app) ``` **helpers/openapi.py** ```python """ OpenAPI Configuration Helper. This module isolates the logic required to customize the generated OpenAPI (Swagger) schema (i.e. the "openapi.json" file). It specifically handles: 1. Removal of redundant 'title' fields (Rule: oas-descr). 2. Renaming schemas to strict CamelCase (e.g. HTTPValidationError -> HttpValidationError). 3. Converting Operation IDs to camelCase (Rule: openapi-opid). This allows 'main.py' to remain clean while satisfying strict linters like Belgif. """ import re from typing import Any, cast from fastapi import FastAPI from fastapi.openapi.utils import get_openapi # Define a recursive type for JSON schemas to fix strict typing issues. JsonNode = dict[str, Any] | list[Any] | Any def _remove_titles(schema: Any) -> None: """ Recursively removes the 'title' key from a JSON-like schema dictionary. """ if isinstance(schema, dict): # Explicit cast to silence linter warnings about unknown keys s_dict: dict[str, Any] = cast(dict[str, Any], schema) s_dict.pop("title", None) for value in s_dict.values(): _remove_titles(value) elif isinstance(schema, list): for item in schema: _remove_titles(item) def _to_camel_case(snake_str: str) -> str: """Converts snake_case to lowerCamelCase.""" components: list[str] = snake_str.split("_") # We capitalize the first letter of each component except the first one return components[0] + "".join(x.title() for x in components[1:]) def _update_refs(item: Any, old_ref: str, new_ref: str) -> None: """ Recursively updates $ref strings in the schema. Used when a schema component is renamed. """ if isinstance(item, dict): s_dict: dict[str, Any] = cast(dict[str, Any], item) # If this node is a reference to the old name, update it if s_dict.get("$ref") == old_ref: s_dict["$ref"] = new_ref return for value in s_dict.values(): _update_refs(value, old_ref, new_ref) elif isinstance(item, list): for sub_item in item: _update_refs(sub_item, old_ref, new_ref) def _fix_schema_names(openapi_schema: dict[str, Any]) -> None: """ Renames schemas that violate Belgif's naming conventions. Specifically targets 'HTTPValidationError' (abbreviations must be camel-cased like 'Http', not 'HTTP'). """ components: Any = openapi_schema.get("components", {}) schemas: Any = components.get("schemas", {}) # Fix: HTTPValidationError -> HttpValidationError if "HTTPValidationError" in schemas: # 1. Move the definition to the new key schemas["HttpValidationError"] = schemas.pop("HTTPValidationError") # 2. Update all references ($ref) in the entire document to point to the new key _update_refs( openapi_schema, "#/components/schemas/HTTPValidationError", "#/components/schemas/HttpValidationError" ) def _fix_operation_ids(openapi_schema: dict[str, Any]) -> None: """ Iterates over all paths and converts snake_case operationIds to camelCase. """ paths: Any = openapi_schema.get("paths", {}) for path_item in paths.values(): for operation in path_item.values(): if isinstance(operation, dict) and "operationId" in operation: old_id: str = cast(str, operation["operationId"]) operation["operationId"] = _to_camel_case(old_id) def configure_custom_openapi(app: FastAPI) -> None: """ Overrides the default OpenAPI generator to apply Belgif compliance fixes. Args: app: The FastAPI application instance to configure. """ def custom_openapi() -> dict[str, Any]: if app.openapi_schema: return app.openapi_schema # 1. Generate the default OpenAPI schema openapi_schema: dict[str, Any] = get_openapi( title=app.title, version=app.version, description=app.description, openapi_version=app.openapi_version, routes=app.routes, tags=app.openapi_tags, ) # 2. Fix 1: Remove Titles (Clutter reduction) (belgif [oas-descr]) if "components" in openapi_schema and "schemas" in openapi_schema["components"]: for schema in openapi_schema["components"]["schemas"].values(): _remove_titles(schema) # 3. Fix 2: Rename Schemas (Naming conventions) (belgif [oas-comp]) _fix_schema_names(openapi_schema) # 4. Fix 3: Operation IDs (snake_case -> camelCase) (belgif [openapi-opid]) _fix_operation_ids(openapi_schema) app.openapi_schema = openapi_schema return app.openapi_schema # Assign the custom function to the app instance app.openapi = custom_openapi # type: ignore[method-assign] ``` ## Conclusion {#conclusion} FastAPI vous offre gratuitement un schéma OpenAPI fonctionnel, mais « fonctionnel » et « conforme Belgif » sont deux niveaux d'exigence différents — le linter ci-dessus est ce qui comble l'écart, et les corrections de cet article couvrent les violations que les réglages par défaut de FastAPI déclenchent le plus souvent (les tags, la version d'OpenAPI, les propriétés `title`, les conventions de nommage). Lancez `belgif-lint` avant de livrer, pas après qu'un relecteur l'ait signalé. Si vous êtes encore en train de concevoir l'API elle-même plutôt que de la valider, [API REST - How to write good APIs](/blog/php-api-tips) est l'article à lire en premier. ---