Logo ConstructiCat
CodeBust.
Parcourir la section ▾

Évitement du refactoring.

Les assistants IA restructurent rarement le code existant : ils greffent de nouvelles branches, des paramètres drapeaux et des couches d'enveloppe sur ce qui existe déjà, si bien que fonctions et conditionnelles ne cessent de grossir pendant que le refactoring — le retravail qui garde une base de code saine — disparaît sans bruit.

##Signes et symptômes

Le relecteur voit un diff composé presque entièrement d'ajouts à du code existant. Le modèle a répondu au prompt en empilant une branche, un drapeau ou une couche de plus sur la fonction qu'on lui a désignée, au lieu de remodeler cette fonction pour que le changement y trouve sa place. Signes révélateurs :

  • Diffs composés uniquement d'ajouts : aucune ligne déplacée, renommée ou supprimée ; la fonction touchée ne fait que s'allonger.
  • Paramètres drapeaux : isWholesale, skipValidation, legacy = false ajoutés à une signature existante, chacun modifiant le comportement au fond du corps.
  • Prolifération de branches : chaque nouvelle exigence devient un else if ou un case de plus dans la même fonction au lieu d'une table de correspondance, d'une stratégie ou d'un appel polymorphe.
  • Une enveloppe par-dessus : processOrderV2 appelle processOrder et corrige son résultat au lieu de le modifier ; l'ancien et le nouveau chemin cohabitent.
  • Des métriques qui montent au même endroit : la complexité cognitive, la longueur et le nombre de paramètres des mêmes fonctions augmentent de commit en commit — un point chaud qui grossit.
  • Des drapeaux morts jamais supprimés : des paramètres que tous les appelants passent désormais de la même façon, conservés "au cas où".
// Chaque demande a ajouté un paramètre et une branche ; rien n'a jamais été restructuré.
function priceOrder(
  items: Item[],
  discount?: number,
  isWholesale = false,
  applyVat = true,
  couponCode?: string,
) {
  let total = items.reduce((s, i) => s + i.price * i.qty, 0);
  if (discount) total = total * (1 - discount);
  if (isWholesale) {
    if (total > 1000) total = total * 0.9;
    else if (total > 500) total = total * 0.95;
  }
  if (couponCode === "SPRING") total = total - 10;
  else if (couponCode === "VIP") total = total * 0.85;
  if (applyVat) total = total * 1.2;
  return total;
}

Chaque demande a ajouté un paramètre et une branche, et chaque diff n'a touché que la fin de la fonction. Rien n'est dupliqué, donc un détecteur de clones reste muet ; l'odeur se manifeste par la croissance de la complexité et de la taille.

##Causes du problème

Pourquoi les modèles le produisent

  • Biais additif de la prédiction du prochain token. Un LLM complète le prompt qu'il a sous les yeux. Ajouter une branche ou un paramètre à l'endroit du changement est la suite localement la plus probable et la moins risquée ; remodeler la fonction exige de garder en contexte tout le module et ses appelants et de prédire une modification cohérente à l'échelle globale, ce qui est plus difficile et n'est pas ce que récompense le prompt immédiat.
  • Contexte du dépôt trop mince. Les assistants de code chargent rarement tous les appelants et tous les tests. Sans voir qui dépend d'une fonction, le modèle ne peut pas savoir s'il est sûr de la restructurer ; il laisse donc la structure telle quelle et ajoute un if là où il voit. OX Security décrit l'IA comme "une armée de développeurs juniors talentueux mais inexpérimentés" — productifs localement, sans regard sur l'architecture.
  • Le refactoring est risqué et non récompensé. Restructurer touche du code que le modèle n'a pas été chargé de modifier et peut casser des appelants et des tests. Un assistant complaisant, du genre "fais juste que ça marche", minimise le rayon d'impact en ne touchant pas au code qui fonctionne — exactement le comportement qu'OX a mesuré comme "évitement des refactorings" (Avoidance of Refactors) dans 80 à 90 % du code généré par IA : l'IA "excelle à générer du nouveau code mais ne revient jamais améliorer l'architecture".
  • Générer ne coûte rien, supprimer fait peur. Produire 40 nouvelles lignes ne coûte rien au modèle ; le convaincre de supprimer et de remodeler 40 lignes existantes va contre son instinct de conservation.

Pourquoi c'est nuisible

  • Le refactoring disparaît de l'historique. L'analyse 2025 de GitClear portant sur 211 millions de lignes modifiées a montré que les lignes associées au refactoring ("déplacées") sont passées de 25 % des lignes modifiées en 2021 à moins de 10 % en 2024, tandis que les lignes copiées-collées passaient de 8,3 % à 12,3 % et, en 2024, dépassaient pour la première fois les lignes déplacées.
  • La complexité s'accumule. Chaque drapeau greffé double les combinaisons que la fonction doit gérer et que les tests doivent couvrir ; chaque else if supplémentaire rend le changement suivant plus risqué — si bien que le prompt suivant ajoute lui aussi au lieu de restructurer.
  • Accumulation de dette technique. Le refactoring est le retravail qui contient l'entropie ; le supprimer, c'est créer une dette qui n'est jamais remboursée. La base de code grossit plus vite qu'elle ne s'améliore.
  • Charge de revue. Les relecteurs reçoivent des diffs qui ne font qu'ajouter des lignes à des fonctions déjà longues ; juger comment une nouvelle branche interagit avec la douzaine de branches au-dessus est plus difficile que relire une fonction remodelée pour absorber le changement.

##Traitement

Tactiques de prompt / de revue

  • Rendez d'abord le changement facile. Le conseil de Kent Beck — "pour chaque changement souhaité, rendez le changement facile (attention : cela peut être difficile), puis faites le changement facile" — se traduit directement en prompt : "Avant d'ajouter la règle de coupon, restructure priceOrder pour que la nouvelle règle s'ajoute en une ligne ; garde un comportement identique et lance les tests. Ensuite, ajoute la fonctionnalité."
  • Interdisez les nouveaux paramètres drapeaux. "N'ajoute pas de paramètres booléens ou de mode aux fonctions existantes ; si le comportement doit varier, propose d'abord une restructuration."
  • Fixez un budget de complexité, pas seulement d'exactitude. Exigez que l'assistant exécute les règles ESLint complexity, max-params et max-lines-per-function ainsi que la règle de complexité cognitive de Sonar, et qu'il donne les valeurs avant/après pour chaque fonction touchée ; toute hausse doit être justifiée.
  • Demandez le refactoring comme une étape distincte. Générer et restructurer sont deux tâches différentes : d'abord "fais que ça marche", puis un changement séparé qui préserve le comportement — "restructure maintenant cette fonction pour que les branches deviennent des données ou des fonctions séparées" —, avec des tests au vert avant et après.
  • Surveillez les points chauds. Suivez fréquence de modification × complexité (Code Maat ou simplement l'historique git) pour les fichiers qui grossissent à chaque commit écrit par l'IA, et planifiez leur restructuration avant que la prochaine fonctionnalité n'y arrive.

Le refactoring proprement dit — Décomposer une condition et Extraire une méthode donnent un nom à chaque branche ; Remplacer un paramètre par des méthodes explicites ou Introduire un objet paramètre remplacent les paramètres drapeaux ; et Remplacer le conditionnel par du polymorphisme (ou une table de correspondance) prend le relais quand les branches choisissent un comportement selon un type ou un code.

// Après : chaque règle est une petite fonction ; priceOrder ne fait que les composer.
type PriceRule = (total: number) => number;

const subtotalOf = (items: Item[]) =>
  items.reduce((s, i) => s + i.price * i.qty, 0);

const VAT_RATE = 1.2;
const discount = (rate: number): PriceRule => (t) => t * (1 - rate);
const wholesale: PriceRule = (t) => (t > 1000 ? t * 0.9 : t > 500 ? t * 0.95 : t);
const COUPONS: Record<string, PriceRule> = {
  SPRING: (t) => t - 10,
  VIP: (t) => t * 0.85,
};
const vat: PriceRule = (t) => t * VAT_RATE;

function priceOrder(items: Item[], rules: PriceRule[]): number {
  return rules.reduce((total, rule) => rule(total), subtotalOf(items));
}

// L'appelant indique quelles règles s'appliquent, dans l'ordre :
// priceOrder(items, [discount(0.1), wholesale, COUPONS.SPRING, vat]);

Une nouvelle règle de prix est désormais une entrée de plus, pas un paramètre et une branche de plus, et chaque règle se teste isolément.

À ne pas confondre avec Duplication au lieu d'extraction : là, le modèle copie une logique existante dans un nouveau bloc quasi identique, et les détecteurs de clones le repèrent. Ici, rien n'est copié — le modèle continue de faire grossir la même fonction à coups de branches, de drapeaux et de couches, et ce sont les métriques de complexité et de taille qui le révèlent.

##Détecté par

  • SonarQube / SonarSource S3776 — La complexité cognitive des fonctions ne devrait pas être trop élevée — signale les fonctions qui continuent d'absorber de nouvelles branches et de nouveaux drapeaux
  • ESLint complexity — Seuil de complexité cyclomatique par fonction (option max)
  • ESLint max-lines-per-function — Limite de longueur de fonction — repère les fonctions qui ne font que grossir
  • ESLint max-params — Limite du nombre de paramètres — repère les drapeaux et options greffés sur une signature existante
  • ESLint max-depth — Limite de profondeur d'imbrication — repère les échelles if/else empilées dans d'anciennes branches
  • Code Maat entity-churn — Fréquence de modification par fichier d'après l'historique git — combinée à la complexité, révèle les points chauds qui grossissent de commit en commit