Complexité cyclomatique : mesurer sans bloquer votre équipe

Résumé

La complexité cyclomatique mesure le nombre de chemins indépendants dans une fonction : comptez les décisions (if, elif, case, while, for, &&, ||, ternaires) et ajoutez 1. Le seuil universellement cité est 10, mais les outils divergent (ESLint monte à 20, Microsoft à 25). Ce qui compte vraiment : une fonction avec un bon score peut rester illisible si elle imbriquée; le vrai coût est le temps d'onboarding d'un développeur junior qui devra la tracer. Mesurez avant de bloquer; gâtez à 10-15 sur le code nouveau, examinez à 20, et traitez 50+ comme une dette technique, pas un rejet de PR.

Ingénieur à un bureau double-écran la nuit, un écran affichant un réseau enchevêtré de lignes de branchement brillantes sur un éditeur de code

La complexité cyclomatique compte le nombre de chemins indépendants dans une fonction. Une fonction sans branche affiche 1. Ajoutez un if, ça passe à 2. Ajoutez un switch à quatre cas, ça monte à 6. Ce chiffre vous dit combien de cas de test vous faut couvrir chaque chemin, et il corrèle avec le temps qu'il faut à quelqu'un d'extérieur pour tenir la fonction dans sa tête.

C'est toute la métrique. Ce qui compte, c'est ce que les équipes en font, et où elles se trompent.

Ce que la complexité cyclomatique compte réellement

McCabe l'a définie en 1976 en termes de théorie des graphes : arêtes moins nœuds plus deux. En pratique, vous n'avez pas besoin du graphe. Comptez les points de décision : if, else if, case, while, for, catch, &&, ||, ternaires. Ajoutez 1. C'est votre score.

def prix(commande):
    if commande.est_vip:           # +1
        if commande.total > 100:   # +1
            return commande.total * 0.8
        return commande.total * 0.9
    elif commande.a_coupon:        # +1
        return commande.total * 0.95
    return commande.total

Ce score est 4. Pas énorme. Mais c'est déjà trois niveaux d'embranchement pour une fonction qui a commencé par « appliquer une remise ». C'est le schéma qui vaut le coup de noter : la complexité s'accumule un elif à la fois, et personne n'est le vilain.

D'où vient le seuil des 10 (et pourquoi les outils divergent)

Le chiffre que tout le monde cite : 10. Il vient de la NIST Special Publication 500-235, où McCabe et Watson écrivent que des limites au-delà de 10 devraient être réservées aux équipes avec du personnel expérimenté, une conception formelle et un plan de test exhaustif. Autrement dit : 10 est le défaut, pas une loi.

Les outils n'en conviennent pas, et c'est utile à savoir avant de configurer une barrière :

Quatre sources, quatre chiffres. Si votre CI gate rate le test à 10 et celui d'un collègue à 25, aucun de vous n'a tort. Vous mesurez juste face à des tolérances au risque différentes. La documentation Microsoft détaille le raisonnement NIST si vous voulez la source plutôt que le résumé.

Ce qu'on fixerait vraiment : 10-15 comme ligne d'avertissement sur le code nouveau, 20 comme point où une PR reçoit un second regard, 50 comme un arrêt net. Sous 10, ne gaspillez pas du temps de review à en discuter.

Le piège : un bon score ne signifie pas une fonction lisible

C'est là que les équipes se brûlent. Une fonction peut afficher 6 et être vraiment difficile à lire, et une fonction peut afficher 14 et être triviale.

Prenez un switch plat avec dix cas, chacun retournant une constante. C'est une complexité de 10, et la plupart des ingénieurs le lisent en quinze secondes parce que le motif est évident : une chose entre, une chose sort, pas d'état porté entre branches. Maintenant prenez une fonction avec trois if imbriqués et une boucle qui mutate une variable partagée. Ça peut afficher 6, et ça prendra un ingénieur senior cinq minutes pour tracer, parce que vous devez tenir toute la pile d'appels dans votre tête pour savoir quelle branche vous êtes réellement.

La complexité cyclomatique mesure les chemins. Elle ne mesure pas la profondeur d'imbrication, la portée des variables, ou la distance entre une condition et son effet dans le fichier. Deux fonctions avec le même score peuvent être une lecture complètement différente.

On a vu des équipes traiter « sous 10 » comme un substitut pour « révisionnable », pousser une PR parce que le linter était vert, puis regarder un développeur junior perdre tout un après-midi dans la même fonction trois semaines après. Le score a passé. La lecture n'a pas été plus facile.

Complexité cyclomatique vs. Complexité cognitive : deux questions différentes

SonarSource a introduit la Cognitive Complexity en 2017 précisément pour combler ce fossé. La complexité cyclomatique répond à « combien de chemins cette fonction a-t-elle ». La complexité cognitive répond à « comme c'est dur de tenir cette fonction dans sa tête », et elle fait ça en pénalisant l'imbrication plus que la structure plate.

Une instruction switch bouge à peine le score cognitif. Un if imbriqué trois niveaux profonds dans une boucle le fait monter vite, parce que chaque niveau d'imbrication ajouté aggrave le coût mental du niveau précédent. La propre analyse de SonarSource détaille les règles de scoring si vous voulez l'implémenter vous-même, et la plupart des linters qui supportent la complexité cyclomatique supportent maintenant la complexité cognitive comme règle séparée et supplémentaire.

Pulez la complexité cognitive si votre équipe est assez petite pour que tout le monde connaisse déjà où vivent les fonctions énervantes. Activez-la dès que vous avez plus de deux personnes qui n'ont pas écrit le code qu'elles révisent, parce que c'est exactement le fossé qu'elle construit pour attraper.

Cork board with red string connecting index cards in a branching decision-tree pattern

Ce que les outils de review IA font de ce chiffre (et ce qu'ils loupent)

GitHub Copilot code review, CodeRabbit, Qodo et Greptile surfacent tous les signaux de complexité sur une PR à un certain degré. Certains signalent une fonction qui a franchi un seuil. D'autres résument « cette PR augmente la complexité sur trois fichiers ». Presque aucun ne vous dit pourquoi ça compte pour la personne qui ouvrira le fichier six mois après sans contexte.

C'est le vrai fossé. Un avertissement de complexité sur une PR c'est un chiffre dans un commentaire. Ça ne dit pas à un relecteur si la branche ajoutée y appartient, si elle duplique de la logique trois fichiers plus loin, ou si le bon fix c'est une clause de garde versus un full pass d'extract-method. Les outils IA sont bons pour compter. Ils ne sont pas encore bons pour expliquer la forme du désordre.

Ce qui comble réellement ce fossé en pratique, c'est d'appairer le chiffre avec une question qu'un humain doit encore répondre : est-ce que cette fonction fait une chose, ou est-ce qu'elle fait trois choses enrobées dans des if ? Aucun linter ne répond ça pour vous. Il juste vous dit où regarder.

Pourquoi ça compte plus sur un repo de 100K LOC qu'un side project

Sur une base de code qu'vous avez écrite seul, la complexité c'est un problème de mémoire que vous avez déjà résolu. Vous connaissez les dix fonctions épineuses par nom, vous savez pourquoi elles sont épineuses, et vous contournez sans penser. C'est pas la situation où la plupart de ce public se trouve.

Sur un repo partagé avec cinq à cinquante ingénieurs, personne ne tient la carte entière. Une fonction affichant 22 que l'auteur original comprenait parfaitement devient, six mois après, une fonction qu'un ingénieur différent doit reconstruire de zéro, généralement sous une deadline. Vous l'avez déjà fait : grep le nom de la fonction, Ctrl+F le fichier, git blame les lignes suspectes, puis message quelqu'un qui a quitté l'équipe il y a huit mois.

C'est aussi où le chiffre de complexité commence à interagir avec la recherche. Une fonction à haute complexité est plus difficile à résumer correctement, ce qui signifie qu'elle est plus difficile pour un collègue ou un outil de code-search de décrire précisément en une phrase. Demandez « ce que fait cette fonction » sur une fonction complexity-4 et vous obtenez une réponse propre. Demandez la même chose sur une fonction complexity-22 avec quatre branches imbriquées et l'honnête réponse c'est « ça dépend quel chemin vous demandez ». Cette ambiguïté c'est exactement ce qui ralentit un nouveau recruté le jour un, et c'est pourquoi la complexité vaut la peine d'être tracée au niveau du repo, pas juste comme avertissement lint par PR.

Le vrai coût que personne ne met dans la métrique : le temps d'onboarding

Voilà la partie qui n'apparaît pas sur un dashboard. Un développeur junior qui rejoint l'équipe n'expérimente pas « complexité cyclomatique de 23 ». Il expérimente : j'ai ouvert ce fichier, je ne sais pas quelle branche s'exécute quand, et j'ai lu depuis quarante minutes.

On a mesuré ça vaguement sur quelques cycles d'onboarding : les fonctions avec un score de complexité supérieur à 15 prenaient les nouveaux environ trois à quatre fois plus longtemps à expliquer correctement dans une séance que les fonctions affichant sous 8. Pas parce que les fonctions à haute complexité faisaient plus, mais parce que tracer quelle branche s'exécute sous quelle condition prend du temps de lecture réel et séquentiel, et personne ne lit une condition imbriquée correctement à la première lecture.

C'est là que la métrique gagne ses galons pour les équipes qui onboardent souvent. C'est pas vraiment un chiffre de qualité de code. C'est un substitut pour « combien de minutes va coûter la personne suivante qui ne l'a pas écrite ». Tracez-la de cette manière et la conversation sur le seuil devient beaucoup moins abstraite.

Senior engineer pointing at a laptop screen while a junior engineer takes notes during a pairing session

Comment la bloquer sans arrêter votre équipe

Mesurez avant de bloquer. Choisissez l'un :

Exécutez-le une fois sur tout le repo avant d'activer l'application. Vous obtiendrez une baseline, et probablement quelques fonctions dans la plage 40+ qui datent d'avant que quiconque actuellement dans l'équipe soit arrivé. Ne bloquez pas rétroactivement sur celles-là; ça juste apprend aux gens à contourner le linter.

Bloquez le code nouveau à 10-15. Signalez quoi que ce soit dépassant 20 pour un second relecteur, pas un rejet automatique; certaines de ces fonctions, comme le switch plat, vont bien. Traitez n'importe quoi au-delà de 50 comme un ticket dette technique, pas un commentaire sur la PR de quelqu'un.

Hands typing on a mechanical keyboard in front of a blurred pull request review interface with red and green diff bars

La limite tient que si la chaîne d'outils l'exécute de la même façon chaque fois. Une règle qui est waivée pour une deadline une fois est waivee pour toujours.

Vous pourchassez le score ou la fonction ?

Une équipe qui bloque dur à 10 et expédie des fonctions plates, ennuyeuses, faciles à lire est en bonne forme. Une équipe qui bloque dur à 10 et commence à fractionner les fonctions en quatre plus petites qui s'appellent mutuellement dans une chaîne que personne ne peut tracer sans trois onglets ouverts a fait le nombre mieux et la base de code pire.

La complexité cyclomatique est un détecteur de fumée, pas un extincteur. Elle vous dit où regarder. Elle ne vous dit pas quoi faire une fois que vous êtes là-bas, et traiter le score comme le but au lieu de la lisibilité qu'il est supposé proxyer c'est comment les équipes finissent avec un dashboard vert et un repo qui prend encore trois semaines à un nouveau recruté pour sentir utile.

Questions fréquentes

Quel est le bon seuil de complexité cyclomatique pour mon équipe ?
Il n'y a pas un « bon » seuil universel. NIST recommande 10, ESLint par défaut 20, Microsoft 25. Ce qui fonctionne : 10-15 comme avertissement sur le code nouveau, 20 pour un second regard, 50+ comme dette technique. Mesurez d'abord votre codebase existant pour établir une baseline, puis ne bloquez pas rétroactivement le code ancien.
La complexité cyclomatique mesure-t-elle vraiment la lisibilité ?
Non. Elle mesure les chemins de code, pas la lisibilité. Une fonction avec 10 cas switch plats peut afficher complexity=10 et rester lisible. Une fonction avec trois if imbriqués peut afficher complexity=6 et être difficile. C'est pourquoi cognitive complexity (qui pénalise l'imbrication) complète mieux ce que vous essayez vraiment de tracker : la charge mentale pour le lecteur suivant.
Comment les outils IA de review de code utilisent-ils la complexité cyclomatique ?
Ils la surfacent pour signaler des pics sur une PR (« cette fonction a jumped de 6 à 18 »). Mais ils expliquent rarement *pourquoi* ça importe ou si la branche ajoutée est justifiée. Le chiffre est un détecteur de fumée : il vous dit où regarder, pas quoi en faire.
Faut-il forcer une limite basse de complexité partout dans le code ?
Non. Forcez sur le code nouveau (10-15), examinez attentivement à 20, mais ne rejetez pas une PR juste parce que le score est haut si la fonction est simple et n'a que beaucoup de branchements. Un gating trop dur enseigne aux équipes à contourner le linter ou à diviser des fonctions de façon stupide (deep call stacks illisibles).
Quelle est la différence entre cyclomatic complexity et cognitive complexity ?
Cyclomatic complexity compte les chemins indépendants (if/elif/case/switch). Cognitive complexity pénalise aussi l'imbrication. Une fonction avec 10 cas plats affiche complexity=10 mais cognitive=5. Utilisez cognitive complexity si vous avez plus de deux développeurs qui ne connaissent pas tous le code.
Comment mesurer la complexité cyclomatique sur un codebase existant ?
Utilisez radon pour Python (radon cc -a .), ESLint pour JS/TS, gocyclo pour Go, SonarQube pour Java/C#. Lancez d'abord sur l'ensemble du repo pour établir une baseline, puis activez le gating progressivement sur le code nouveau seulement, sans bloquer le code hérité.