Duplication au lieu d'extraction.
Les assistants IA ont tendance à coller une copie fraîche et légèrement modifiée d'une logique existante au lieu de réutiliser ou d'extraire une fonction partagée, gonflant le code dupliqué tandis que la refactorisation disparaît discrètement.
##Signs and Symptoms
Un relecteur reconnaît ce smell quand un diff rédigé par IA ajoute un bloc qui existe déjà quelque part dans le dépôt, légèrement renommé, au lieu d'appeler la fonction qui est déjà là. La forme révélatrice est deux corps quasi identiques ou plus qui ne diffèrent que par un nom (user/order), un endpoint ou une constante — le genre de copie qu'un humain aurait factorisée.
Signaux courants :
- De nouveaux handlers/services qui répètent verbatim la même validation, le même retry, le même mapping d'erreurs ou le même boilerplate de fetch.
- Des chaînes magiques et littéraux de configuration re-saisis dans chaque nouveau bloc plutôt que référencés depuis un seul endroit.
- Chaque tour de l'agent réimplémente une logique que le tour précédent avait déjà écrite (l'agent n'a aucune mémoire qu'un helper existe).
git logmontre des commits IA consécutifs ajoutant des lignes mais presque aucune ligne « déplacée »/renommée — des ajouts sans consolidation.
// L'IA a ajouté ceci pour la nouvelle route — mais getUser existe déjà 30 lignes plus haut
async function getOrder(id: string) {
const res = await fetch(`${API}/orders/${id}`, { headers: authHeaders() });
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const json = await res.json();
if (!json?.data) throw new Error("Malformed response");
return json.data;
}
async function getUser(id: string) { // <-- corps identique à 95 %
const res = await fetch(`${API}/users/${id}`, { headers: authHeaders() });
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const json = await res.json();
if (!json?.data) throw new Error("Malformed response");
return json.data;
}
C'est le smell classique Code dupliqué, mais produit systématiquement plutôt que par accident.
##Reasons for the Problem
Pourquoi les modèles le produisent
- Localité du token suivant. Un LLM complète l'étendue courante à partir du texte le plus similaire de sa fenêtre de contexte. Reproduire un bloc proche et connu-bon est la continuation à plus forte probabilité ; « s'arrêter, aller définir un helper, puis l'appeler » est un détour plus long, à plus faible probabilité, qui s'étend sur plusieurs fichiers.
- Aucun modèle à l'échelle du dépôt. L'assistant sait rarement qu'un helper approprié existe déjà hors de son contexte. Sans récupération, il ne peut pas réutiliser ce qu'il ne voit pas, alors il régénère. Les praticiens décrivent des agents qui « recréent une logique similaire à partir de zéro pour chaque nouvelle tâche », et Eno Reyes (Factory) présente la qualité du code elle-même comme le principal prédicteur du fait que l'IA aide ou nuit à une base de code.
- Amnésie par tour + portée du prompt. Chaque requête est traitée comme une unité autonome (« fais marcher ceci »), donc l'agent sur-ajuste une solution étroite à usage unique au lieu de généraliser. OX Security nomme cela sur-spécification (vue dans 80–90 % du code IA) et évitement des refactorisations (80–90 %) : le modèle émet du code fonctionnel pour le prompt immédiat mais ne consolide jamais.
- Complaisance / moindre friction. Les modèles optimisent pour une sortie qui satisfait visiblement la demande sans toucher à des fichiers non liés. Dupliquer est « sûr » et local ; refactoriser risque de casser quelque chose que le modèle ne voit pas, alors il l'évite.
- L'outillage rend cela bon marché. Comme le note GitClear, le tab-pour-accepter rend l'insertion d'un bloc frais quasi gratuite, supprimant la friction qui poussait autrefois les développeurs vers la réutilisation.
Pourquoi c'est nuisible
- Érosion mesurée de la qualité. L'analyse 2025 de GitClear (211 M de lignes modifiées, 2020–2024) a constaté que les lignes copiées-collées sont passées de 8,3 % à 12,3 % tandis que les lignes « déplacées » (refactorisées) ont chuté d'environ 24 % à 9,5 % — 2024 a été la première année où les lignes copiées-collées ont dépassé les lignes déplacées, et les blocs de 5+ lignes dupliquées ont bondi d'environ 8×.
- Dette d'exactitude — « bugs déjà-vu ». OX Security a trouvé cela dans 70–80 % du code IA : un bug corrigé dans un clone survit silencieusement dans tous les autres, donc des défauts identiques se reproduisent et chacun nécessite une correction redondante. La duplication transforme un bug en N bugs.
- Maintenabilité & charge de revue. Chaque changement de comportement doit être trouvé et édité à de nombreux endroits ; les diffs gonflent de blocs quasi identiques que les relecteurs doivent comparer ligne par ligne pour confirmer qu'ils sont réellement identiques (et pas subtilement, dangereusement différents).
- Surface de sécurité. Un pattern vulnérable (vérification d'authentification manquante, entrée non échappée) se propage dans chaque copie, et un durcissement ultérieur peut manquer des clones — la duplication multiplie la surface de correctif.
##Treatment
Tactiques de revue & de prompting
- Dites au modèle où chercher d'abord : « Avant d'écrire du nouveau code, cherche dans le dépôt un helper existant qui fait ceci et réutilise-le ; ne duplique pas. » Collez le module pertinent pour qu'il soit dans le contexte.
- Exigez la consolidation explicitement : « Si deux blocs ne diffèrent que par des paramètres, extrais une seule fonction paramétrée (Extraire une fonction) et appelle-la depuis les deux. »
- Faites-lui exécuter les outils : « Exécute le linter / jscpd / la vérification de duplication et résous tout clone qu'il signale avant de rendre le diff. » Les agents se corrigent bien quand on leur donne un signal déterministe.
- En revue, traitez un bloc d'apparence collée comme une invitation à grep : cherchez une ligne distinctive ; si elle existe déjà, renvoyez-le pour extraction.
Le refactoring — Extraire une fonction + paramétrer
// après : une seule source de vérité, appelée depuis les deux
async function getResource<T>(path: string): Promise<T> {
const res = await fetch(`${API}/${path}`, { headers: authHeaders() });
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const json = await res.json();
if (!json?.data) throw new Error("Malformed response");
return json.data as T;
}
const getOrder = (id: string) => getResource<Order>(`orders/${id}`);
const getUser = (id: string) => getResource<User>(`users/${id}`);
Désormais, une correction de la gestion d'erreurs ou de l'analyse des réponses atterrit une seule fois. Lorsque les doublons diffèrent par une étape plutôt que par une valeur, préférez Former une méthode patron (Template Method) ou passez l'étape variable en callback. Les manœuvres canoniques ici sont Extraire une fonction, Extraire une variable/constante (pour les littéraux re-saisis) et Remonter une méthode lorsque les clones vivent dans des classes sœurs — toutes des remèdes standard au Code dupliqué.
Garde-fou, pas seulement nettoyage : branchez un détecteur de clones dans la CI avec un seuil bas afin que le prochain doublon généré par IA fasse échouer le build au lieu d'être fusionné. Associez-le à une règle maison d'une ligne dans CLAUDE.md/AGENTS.md (« réutilise les helpers existants ; ne colle jamais un quasi-doublon ») pour que la contrainte soit présente dans le contexte du modèle à chaque tour.
##Detected by
- jscpd copy/paste duplication (configurable min-tokens / min-lines threshold; supports --threshold gate in CI)
- PMD CPD (Copy/Paste Detector) — token-based clone detection across 30+ languages
- SonarQube / SonarSource S4144 — Methods should not have identical implementations
- SonarQube / SonarSource S1192 — String literals should not be duplicated
- SonarQube Duplicated blocks / duplicated_lines_density metric (built-in clone engine, fails quality gate over threshold)
- eslint-plugin-sonarjs sonarjs/no-identical-functions
- eslint-plugin-sonarjs sonarjs/no-duplicate-string