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.
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.totalCe 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 :
NIST SP 500-235 : 10
ESLint
complexityrule : 20Microsoft CA1502 : 25
Steve McConnell, Code Complete : 0-5 OK, 6-10 à surveiller, 10+ à refactoriser
Références Carnegie Mellon : 1-10 simple, 11-20 plus dur à tester, 20+ difficile à comprendre, 50+ unmaintainable
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.

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.

Comment la bloquer sans arrêter votre équipe
Mesurez avant de bloquer. Choisissez l'un :
Python :
radon cc -a -s .montre chaque fonction avec une note de lettre;radon cc -n c .filtre à C-grade et pire.JavaScript / TypeScript : ESLint's
complexityrule, fixé avec['warn', { max: 15 }]pour commencer.Go :
gocyclo, pointez-le sur un package et il imprime chaque fonction au-dessus d'un seuil.Java / C# : SonarQube ou PMD, généralement déjà câblé dans CI si votre équipe en utilise 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.

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.