Un refactoring de code legacy réussi ne ressemble jamais à une conversation ChatGPT heureuse du premier coup. Il ressemble à une suite de petits diffs, chacun validé par un test, chacun réversible. La vraie question n’est pas « ChatGPT peut-il refactorer mon code ». C’est : suis-je capable de transformer mon code en petites unités testables avant de lui demander quoi que ce soit.

Les LLM génératifs n’accélèrent pas le refactoring legacy parce qu’ils produisent du meilleur code. Ils l’accélèrent uniquement chez les équipes qui ont déjà la discipline de tests, de revue et de découpage en petits incréments. Sur un repository sans tests, ChatGPT est un accélérateur de dette, pas de qualité.

Ce que le refactoring legacy avec ChatGPT change vraiment

Refactorer du code legacy, c’est modifier sa structure sans modifier son comportement observable. Le legacy ajoute deux contraintes : on ne connaît pas toujours ce comportement, et les tests manquent souvent. ChatGPT ne résout ni l’un ni l’autre. Il accélère la phase de transformation une fois que le terrain est préparé.

Une étude arXiv publiée en 2024 a analysé 715 interactions de refactoring issues d’un corpus de 29 778 prompts et réponses entre développeurs et ChatGPT. Le résultat qui saute aux yeux : les refactorings qui aboutissent en production ne viennent presque jamais d’un prompt unique. Ils viennent d’une négociation en plusieurs tours, où le développeur recadre, rejette, redemande avec un contexte enrichi. Le prompt initial sert à amorcer, pas à résoudre.

Cela change la façon de penser l’outil. ChatGPT n’est pas un refactorer. C’est un interlocuteur qui propose des transformations que l’humain accepte, rejette ou amende. La décision reste humaine. La vitesse de génération ne remplace pas la lenteur de la validation.

Quand ChatGPT accélère, quand il ralentit

Code avec tests solides, fonctions pures, responsabilités claires : ChatGPT va vite sur les renommages, les extractions de fonctions, les modernisations de syntaxe (var vers const, callbacks vers async/await, class components vers hooks React). Transformations mécaniques, rien de subtil.

Legacy typique (fonctions de 400 lignes, état global implicite, import circulaires, effets de bord cachés dans les getters) : il propose des refactorings plausibles qui cassent des invariants invisibles dans le snippet soumis. Plus le contexte manquant est large, plus la proposition est séduisante et fausse.

Résume la fonction en trois phrases et liste ses effets de bord. Si tu n’y arrives pas, commence par là.

Préparer le terrain avant le premier prompt

La préparation détermine 80 % du résultat. Elle tient en quatre gestes.

Lire le code concerné et tracer ses dépendances entrantes et sortantes. Quels modules l’appellent, quels modules il appelle, quelles données il lit, quelles données il mute. Un schéma manuel sur papier suffit. Sans cette cartographie, toute proposition de refactoring est une supposition.

Identifier les tests existants et mesurer la couverture sur le périmètre visé. Si la couverture est inférieure à 60 % sur les chemins critiques, le premier travail n’est pas de refactorer, c’est d’ajouter des tests caractérisation. Ces tests ne valident pas que le code fait ce qu’il devrait faire, ils gèlent ce qu’il fait aujourd’hui, bug compris.

Lister les erreurs connues, les workarounds, les commentaires cryptiques. Le legacy contient toujours des hacks qui paraissent absurdes mais résolvent un cas client précis. Un refactoring qui « nettoie » ces hacks sans les comprendre réintroduit des bugs que des customers ont payé cher à faire corriger.

Fixer le périmètre. Un fichier, une function, un module. Pas plus. Les développeurs qui arrivent avec un prompt « refactorise tout ce repository » reçoivent une transformation théâtrale et inutilisable. Ceux qui arrivent avec « refactorise cette function de 80 lignes en extrayant les trois responsabilités que j’ai identifiées » reçoivent quelque chose d’applicable.

Les prompts qui produisent du code applicable

Un prompt de refactoring efficace contient cinq éléments : le code source intégral, l’intention explicite, les invariants à préserver, les contraintes techniques et le format de sortie attendu. L’absence d’un seul de ces éléments dégrade la qualité de la réponse.

Prompt de diagnostic initial

Avant de demander une transformation, demande une analyse. L’objectif est de forcer le modèle à expliciter ce qu’il comprend du code, ce qui permet de repérer ses hypothèses fausses avant qu’elles ne contaminent le refactoring.

Analyse cette function sans la modifier. Identifie :
- Ses responsabilités (combien, lesquelles)
- Ses dépendances externes (import, state global, I/O)
- Ses effets de bord
- Les patterns de code problématiques (duplication, couplage, magic numbers)
- Les refactorings possibles, classés par risque

Ne propose aucun code. Je veux une analyse en prose.

[coller le code ici]

Ce prompt a l’avantage de ne rien coûter en exécution : tu lis l’analyse, tu valides qu’elle correspond à ton modèle mental, puis tu décides du refactoring. Si l’analyse est à côté, le reste le sera aussi. Ce même principe de diagnostic préalable s’applique aux outils d’IDE modernes, où la comparaison entre Claude Code et Cursor IDE révèle les mêmes limites sur la compréhension du contexte long.

Prompt pour refactorer une function

Refactorise la function suivante selon ces règles :
- Applique Extract Function pour chaque responsabilité distincte
- Les functions extraites doivent être pures (pas de state, pas d'I/O)
- Préserve exactement le comportement observable (même sortie, mêmes exceptions)
- Conserve les const et import existants sauf demande contraire
- N'ajoute aucune dépendance

Sortie attendue :
1. Le code refactorisé complet
2. Un diff unifié vs l'original
3. La liste des invariants que tu as préservés
4. Les cas limites que tu n'as pas vérifiés

[coller la function]

Le format de sortie forcé est ce qui différencie un prompt amateur d’un prompt de production. Sans la section « invariants préservés », le modèle ne se rend pas compte qu’il a modifié le comportement. L’obliger à lister ce qu’il a gardé intact le force à le vérifier.

Prompt pour séparer logic et data

Ce code mélange logique métier et accès aux data. Sépare-les en deux modules :
- Un module data.ts qui exporte uniquement des const et des types
- Un module logic.ts qui importe data.ts et contient les functions

Règles :
- Zéro logique dans data.ts, zéro const de configuration dans logic.ts
- Les types partagés restent dans data.ts
- Les import doivent être explicites, pas de re-export

[coller le code]

Cette séparation paraît mineure. Sur un module de 600 lignes où 30 const de configuration traînent au milieu de 20 functions qui les utilisent implicitement, c’est la transformation qui débloque tous les refactorings suivants.

Prompt pour moderniser const et import

Modernise ce code selon les règles suivantes :
- Remplace var par const (ou let si réassigné)
- Regroupe les import par origine : builtin, externe, interne
- Supprime les import inutilisés
- Extrait les magic numbers et strings en const nommées en haut du module

Ne change AUCUNE logic. Ne renomme AUCUNE function publique.
Produis un diff unifié.

[coller le code]

La dernière ligne est ce qui évite les dérives. Sans elle, le modèle en profite pour « améliorer » le nommage, ce qui casse tous les appelants en dehors du snippet.

Prompt pour proposer plusieurs refactorings

Demander plusieurs options force la comparaison et évite la décision imposée par une proposition unique.

Propose 3 refactorings différents pour ce code, avec pour chacun :
- Le nom du pattern appliqué (Extract Function, Replace Conditional,
  Introduce Parameter Object, etc.)
- Le diff complet
- Les avantages et inconvénients
- Le niveau de risque (faible, moyen, élevé)

Ne choisis pas à ma place. Présente les 3.

[coller le code]

Patterns de refactoring qui survivent à la production

Quatre patterns couvrent l’écrasante majorité des besoins sur du code legacy. Les connaître évite de se laisser embarquer dans des transformations exotiques que ChatGPT adore proposer.

Extract Function. Prendre une portion de code cohérente à l’intérieur d’une function trop longue et l’isoler dans sa propre function. C’est le refactoring le plus rentable : il améliore la lisibilité, rend le code testable unitairement, et prépare les transformations suivantes. Règle : si une portion de code mérite un commentaire pour expliquer ce qu’elle fait, elle mérite d’être extraite dans une function dont le nom remplace ce commentaire.

Extract Data. Sortir les données (const de configuration, mappings, tables) du code qui les utilise. Le bénéfice immédiat est la testabilité. Le bénéfice caché est qu’on se rend compte qu’une partie de ces data est morte, dupliquée ou contradictoire. ChatGPT repère bien les duplications textuelles, mal les contradictions sémantiques.

Introduce Const. Remplacer les magic numbers et strings par des const nommées. Sur du legacy, c’est parfois le seul refactoring qui tient en production parce qu’il ne change rien à l’exécution. Il révèle aussi les incohérences : trois occurrences de 0.15 dans trois fichiers différents, qui doivent-elles être le même nombre.

Réorganisation des import et du module. Regrouper les import par catégorie (builtin Node, dépendances externes, modules internes), supprimer les inutilisés, éclater un module monolithique en sous-modules cohérents. ChatGPT fait ce travail correctement sur des modules courts. Sur des fichiers de plus de 500 lignes, il oublie des import ou en invente.

Refactorer une function sans casser le comportement

La démarche tient en cinq étapes, chacune validée avant de passer à la suivante.

Figer le comportement actuel par des tests de caractérisation. Pour chaque cas d’entrée identifié, enregistrer la sortie produite aujourd’hui et écrire un test qui l’attend. Ces tests ne jugent pas la correction, ils gèlent l’existant.

Lire la function et identifier ses blocs logiques. Un bloc par responsabilité. Si tu identifies plus de cinq blocs, la function est trop grosse pour être refactorée en une passe, découpe le chantier.

Extraire un bloc, un seul. Relancer la suite de tests. Si elle casse, revert. Si elle passe, commit. Ce cycle doit durer moins de dix minutes par itération. ChatGPT entre ici : tu lui demandes d’extraire un bloc précis que tu as identifié, pas de refactorer toute la function.

Comparer la sortie avant et après sur des cas réels (pas seulement les tests). Les tests couvrent ce que tu as pensé à tester. La comparaison sur données de production révèle ce que tu n’avais pas pensé à tester.

Passer au bloc suivant. La patience sur ce cycle est ce qui distingue un refactoring qui tient d’un refactoring qui explose deux semaines plus tard. La même discipline s’applique d’ailleurs aux corrections techniques plus larges : corriger un score CLS mobile dégradé demande la même approche par petits incréments vérifiés.

Séparer logic, data et state dans un code legacy

C’est le refactoring le plus transformateur et le plus sous-estimé. Un module legacy typique mélange trois choses : la logique métier (les règles), les data (les configurations), et le state (ce qui change entre deux appels). Tant que les trois cohabitent, impossible de tester sereinement, impossible d’évoluer sans régression.

Le signal qu’il faut séparer : tu n’arrives pas à écrire un test unitaire sans mocker cinq choses. Le mock est le symptôme d’un couplage. ChatGPT aide à identifier les mélanges : on lui soumet un module et on lui demande de classer chaque ligne en logic, data ou state. Le résultat est rarement parfait, mais il révèle les zones grises qui méritent discussion.

La séparation se fait progressivement. On commence par sortir les data (plus facile, moins risqué). On sort ensuite le state (plus délicat, parce que le state a souvent des invariants implicites). On laisse la logic dans le module d’origine, elle migrera naturellement vers une forme plus pure une fois que les deux autres préoccupations ont disparu.

Gérer const, import et structure des modules

Nettoyage des import : tâche la plus mécanique déléguée à ChatGPT. Trois règles tiennent. Groupes par origine (builtin, externe, interne), zéro inutilisé, ordre alphabétique dans chaque groupe. Cosmétique en apparence, ça réduit drastiquement les conflits de merge sur un repo multi-équipes.

Une const utilisée à un seul endroit reste locale à sa function. La déplacer en haut du module crée une illusion de partage.

Tests avant et après : ce qui sépare un refactoring d’un pari

Sans tests, ChatGPT est un générateur de bugs sophistiqué. Avec tests, c’est un accélérateur. Cette dépendance n’est pas négociable.

Type de testRôle dans le refactoringQuand l’écrire
CaractérisationGèle le comportement actuelAvant toute transformation
UnitaireValide les functions extraitesPendant l’extraction
IntégrationVérifie les interactions entre modulesAprès extraction, avant commit
Non-régressionCompare sorties avant/après sur données réellesAvant mise en production

Les tests de caractérisation sont les plus importants et les plus négligés. Ils ne vérifient rien de « correct ». Ils capturent l’entrée, la sortie, et les exceptions levées aujourd’hui. Leur rôle est de sonner l’alarme si le refactoring change quoi que ce soit d’observable. ChatGPT aide à les générer : on lui soumet la function et un jeu de données d’entrée, on lui demande de produire la suite d’assertions attendues sur la sortie actuelle. On vérifie ensuite manuellement quelques cas.

Les tests d’intégration doivent tourner sur des data réalistes, pas sur des mocks génériques. Beaucoup d’équipes ont été surprises de voir passer des tests mockés et casser la prod : le mock ne reproduit que ce qu’on a pensé à y mettre.

Checklist de validation rapide avant d’accepter un refactoring proposé : la suite complète passe sans modification des tests, le diff est lisible en moins de 10 minutes, aucune nouvelle dépendance n’est introduite, aucun commentaire n’a été supprimé sans justification, le comportement sur au moins trois cas de production réels est identique.

Refactoring manuel, ChatGPT seul, ChatGPT encadré

Trois approches coexistent, avec des profils de risque radicalement différents.

Le refactoring manuel reste la référence sur les zones critiques (paiement, sécurité, calculs financiers). Lent, fastidieux, mais la personne qui code comprend ce qu’elle transforme. Le taux de régression est corrélé à l’expérience du développeur, pas à la vitesse de frappe.

ChatGPT seul, sans supervision, produit du code qui ressemble à du bon code. Sur du greenfield, ça tient. Sur du legacy, ça casse. Le mode « accepte toutes les propositions » est une stratégie de dette technique garantie. À éviter en production sur tout ce qui a plus de deux ans.

ChatGPT encadré par tests et revue humaine est le mode qui produit des gains mesurables. Le développeur garde la responsabilité du découpage, de la validation et de l’acceptation. ChatGPT accélère la phase de génération. Les tests jouent le rôle de filet. La revue humaine capture les erreurs sémantiques que les tests ne voient pas. Ce mode demande de la discipline, mais c’est le seul qui soit défendable en production.

Risques, limites et garde-fous

Les hallucinations sur les API sont le risque le plus courant. ChatGPT invente des méthodes qui n’existent pas, des options qui n’ont jamais été implémentées, des signatures de functions qui ont changé il y a trois versions. Le garde-fou est la compilation ou le typage strict. Sans TypeScript, sans typage Python strict, ces erreurs passent à travers la relecture humaine.

Les refactorings trop ambitieux sont le deuxième piège. Le modèle accepte volontiers une demande « refactorise ce module de 1200 lignes » et produit quelque chose qui paraît propre et qui ne compile pas. La règle : un refactoring accepté en une passe doit tenir en moins de 100 lignes de diff. Au-delà, on découpe.

La perte de contexte entre deux prompts est structurelle. ChatGPT n’a pas de mémoire persistante du repository. Chaque nouveau prompt repart de zéro sur ce qui n’est pas dans le contexte soumis. Le garde-fou est de maintenir un document court (un paragraphe) qui résume les conventions du projet et qu’on colle en préambule de chaque session. Les bonnes équipes gèrent cette mémoire contextuelle comme elles géreraient l’impact SEO d’une redirection 301 : avec une documentation explicite de chaque décision.

L’oubli des cas limites est systématique. ChatGPT optimise pour le cas nominal. Le cas où la liste est vide, où la date est dans un format exotique, où le client a un apostrophe dans son nom : oubliés huit fois sur dix. Les tests les capturent si on a pensé à les écrire.

Les mauvaises abstractions sont le risque le plus insidieux. Le modèle aime introduire des couches de factory et des patterns surdimensionnés pour un besoin simple. La règle YAGNI reste valide : on ne factorise qu’après avoir vu le motif apparaître trois fois. Refuser une proposition qui introduit une abstraction pour un cas unique, c’est protéger le lecteur de code futur.

Questions fréquentes

ChatGPT peut-il refactorer du code legacy en autonomie complète ?

Non, et personne ne devrait le configurer ainsi en production. Les propositions sont plausibles mais contiennent régulièrement des erreurs sémantiques que seul un humain qui connaît le contexte métier peut repérer. Les équipes qui tentent l’automatisation complète sur du legacy produisent des régressions silencieuses qui se révèlent en incident. L’autonomie est défendable sur du code très couvert en tests, avec revue systématique des diffs générés.

Faut-il refactorer tout le repository en une fois ?

Jamais. Les refactorings de grande ampleur soumis en bloc échouent pour trois raisons : les tests existants ne couvrent pas tous les chemins, les conflits de merge explosent pendant le chantier, et la revue humaine devient impossible. La stratégie qui marche : identifier les modules les plus douloureux, les refactorer un par un sur plusieurs sprints, chaque transformation tenant dans une pull request de moins de 500 lignes.

Comment choisir entre plusieurs propositions de refactoring ?

Trois critères : la réversibilité (peut-on revenir en arrière en un commit), l’alignement avec les conventions existantes du projet, et le volume de code touché hors périmètre. Une proposition qui introduit un pattern étranger au reste du code crée de la dissonance cognitive pour les mainteneurs. Une proposition qui modifie trois fichiers périphériques pour un refactoring localisé signale un couplage caché qu’il faut comprendre avant d’accepter.

Les patterns proposés par ChatGPT sont-ils fiables sur du code Python legacy ?

La fiabilité dépend du périmètre. Sur des transformations syntaxiques (f-strings, type hints, dataclasses), très fiable. Sur des transformations structurelles (refonte d’une classe, extraction de modules, migration vers des frameworks récents), la fiabilité chute. Les bibliothèques Python évoluent vite et le modèle propose parfois des API obsolètes depuis deux versions majeures. Vérifier contre la documentation officielle avant d’accepter reste une étape non négociable, au même titre que vérifier la vitesse d’un site avec l’outil adapté avant de conclure à une optimisation réussie.

Quiz personnalisé

Votre recommandation sur refactoring code legacy avec chatgpt

Trois questions rapides pour savoir exactement ce qui s'applique dans votre situation.

Q1 Quel est votre rôle dans la situation ?
Q2 Quel type de situation ?
Q3 Quelle est votre priorité ?