Comment détecter le code mort dans une grande codebase avant qu'il ne vous ralentisse
Le code mort s'annonce rarement. Il ne casse pas le build. Il ne fait pas échouer les tests. Il reste tranquillement dans le repository, avec l'air assez important pour que personne n'ose le supprimer.
Puis la codebase grandit autour de lui.
Un module déprécié exporte encore des types publics. Un ancien service n'est plus appelé en production, mais trois packages importent toujours ses helpers. Un feature flag a été retiré du produit il y a deux ans, mais l'implémentation existe encore derrière une condition toujours fausse. Les nouveaux développeurs le lisent, les reviewers le préservent, et les refactorings le contournent.
Le code mort n'est pas seulement du désordre. Dans une grande codebase, il ralentit l'onboarding, brouille l'ownership, augmente le temps de test et rend l'architecture plus difficile à comprendre.
La partie difficile n'est pas de supprimer du code. La partie difficile est de savoir ce qui peut être supprimé sans risque.
Qu'est-ce que le code mort ?
Le code mort est du code qui n'a plus de chemin significatif vers une exécution ou une utilisation réelle.
Il peut prendre plusieurs formes :
- Fonctions qui ne sont jamais appelées
- Composants qui ne sont plus rendus
- Routes API qu'aucun client n'utilise
- Branches de feature flag qui ne peuvent plus s'activer
- Packages qu'aucune application n'importe
- Adaptateurs de base de données pour des systèmes retirés
- Scripts CLI que personne n'exécute
- Types et utilitaires qui ne supportent que d'autre code mort
Une partie du code mort est évidente. Une fonction privée sans référence est généralement sûre à supprimer. Le reste est plus difficile. Les exports publics, les modules chargés dynamiquement, les conventions de framework et les consommateurs d'API externes peuvent donner l'impression qu'un code est inutilisé alors qu'il est encore important.
C'est pourquoi un nettoyage sûr du code mort nécessite plusieurs signaux.
Pourquoi le code mort survit
Le code mort survit parce que la suppression semble risquée.
Ajouter du code est récompensé. Supprimer du code est questionné. Si un développeur retire un module et que quelque chose casse, l'échec est visible. S'il laisse le code mort en place, le coût est diffus et différé.
Les équipes manquent aussi de confiance. Une recherche de références peut ne rien montrer, mais qu'en est-il des imports dynamiques ? D'un job planifié ? D'un client qui appelle encore un ancien endpoint ?
Enfin, le code mort se cache souvent derrière des frontières d'architecture faibles. Si tout importe tout, il devient difficile de savoir si un module est réellement inutilisé ou seulement accessible indirectement à travers un chemin de dépendances emmêlé. C'est une des raisons pour lesquelles la gestion des dépendances dans un monorepo est si importante.
Signal 1 : les références statiques
Commencez par le signal le plus simple : le nombre de références.
Pour les projets TypeScript, votre éditeur, le language server TypeScript, ESLint, ts-prune, knip ou des outils similaires peuvent identifier les exports qui semblent inutilisés.
Cela attrape beaucoup de cas faciles :
export function formatLegacyInvoiceDate(date: Date) {
return oldFormatter(date);
}
Si cette fonction est exportée mais jamais importée, elle devient candidate à la suppression.
Mais les références statiques ont des limites. Elles peuvent manquer les usages dynamiques :
const handler = await import(`./handlers/${event.type}`);
Elles peuvent aussi mal comprendre les conventions de framework où les fichiers sont découverts par chemin, pas par import. Les routes Next.js, les providers NestJS, les fichiers de migration et les entrypoints CLI peuvent être utilisés sans chaîne de référence normale.
Traitez donc l'analyse statique comme un générateur de candidats, pas comme un verdict final.
Signal 2 : les graphes de dépendances
Le nombre de références répond à la question « ce symbole est-il importé ? ». Les graphes de dépendances répondent à une question plus large : « comment ce module se connecte-t-il au reste du système ? ».
C'est crucial dans les grandes applications. Un fichier peut avoir des références, mais seulement depuis un package lui-même inutilisé. Un utilitaire peut sembler vivant parce qu'un service déprécié l'importe. Un package peut exister dans le workspace sans être accessible depuis une application déployable.
La visualisation des dépendances aide à identifier les îlots isolés :
- Packages sans dépendances entrantes
- Modules connectés uniquement à des features dépréciées
- Clusters circulaires qui ne supportent que d'anciens comportements
- Utilitaires partagés devenus des fourre-tout accidentels
ReposLens est utile ici parce qu'il montre le graphe d'architecture réel plutôt que seulement des warnings au niveau fichier. Si un module est déconnecté des chemins qui mènent aux entrypoints de production, il devient un candidat de nettoyage plus solide.
Cela aide aussi à éviter les suppressions accidentelles. Si un module est encore connecté à un chemin critique, le graphe le rend visible avant la PR de nettoyage.
Signal 3 : l'usage runtime
L'analyse statique vous dit ce qui pourrait être utilisé. Les données runtime vous disent ce qui est réellement utilisé.
Pour les routes, jobs, commandes et branches de features, ajoutez une instrumentation légère avant de supprimer quoi que ce soit de risqué.
Suivez :
- Les hits d'endpoints API
- Les exécutions de jobs planifiés
- L'usage des commandes CLI
- L'exposition des feature flags
- Les invocations de handlers d'événements
- Les warnings de module pour les chemins legacy suspects
Par exemple, si vous suspectez une route API d'être inutilisée, loggez son usage pendant deux semaines :
logger.info("legacy_endpoint_called", {
route: "/api/v1/export",
userId,
});
Si elle ne reçoit aucun trafic pendant une période représentative, vous avez une preuve plus forte. Si elle reçoit du trafic d'un seul client, vous pouvez migrer ce client avant de supprimer l'endpoint.
La preuve runtime est particulièrement importante pour les API publiques et les jobs en arrière-plan. Ne supprimez jamais un comportement accessible de l'extérieur uniquement sur la base d'un grep.
Signal 4 : ownership et contexte métier
Certains codes semblent morts parce que l'équipe qui les lit ne sait pas qui les possède.
Avant de supprimer un module suspect, demandez :
- Quelle surface produit utilisait cela ?
- Quelle équipe en était propriétaire ?
- Existe-t-il un client, contrat ou migration qui en dépend encore ?
- Est-ce référencé dans la documentation, les runbooks, dashboards ou scripts support ?
- Un remplacement est-il déjà en production ?
C'est là que les Architecture Decision Records aident. Si l'équipe a documenté pourquoi un module existe, le nettoyage repose moins sur des suppositions. Si aucun owner ou ADR n'existe, c'est un signal, mais pas une preuve automatique.
Un workflow sûr pour supprimer le code mort
Le processus le plus sûr est progressif.
1. Marquer les candidats
Créez une liste de modules suspectés avec les preuves :
module: packages/legacy-export
références statiques: aucune depuis apps/web ou apps/api
usage runtime: aucun hit endpoint en 30 jours
owner: aucun owner actif
remplacement: apps/api/src/report
recommandation: supprimer en deux PRs
Cela transforme la suppression en décision reviewable plutôt qu'en surprise.
2. Retirer les références d'abord
Si un module déprécié a encore quelques callers, migrez ces callers en premier. Gardez la PR de nettoyage focalisée.
Les petites PRs sont plus faciles à relire et moins susceptibles de cacher des changements de comportement. Elles fonctionnent aussi mieux avec des quality gates de PR automatisés, parce que les échecs pointent vers un changement plus étroit.
3. Supprimer le module
Quand les références et l'usage runtime ont disparu, supprimez le module et exécutez toute la suite de tests.
Vérifiez aussi :
- La sortie de build
- La vérification de types
- Les clients générés
- Les images Docker
- Les scripts CI
- La configuration de déploiement
- Les liens de documentation
Le code mort laisse souvent des traces hors du dossier source.
4. Surveiller après le merge
Pour les suppressions risquées, surveillez les logs et erreurs après le déploiement. Si le chemin supprimé était vraiment inutilisé, il ne se passe rien. Ce silence est le meilleur résultat.
Si quelque chose casse, la PR doit être assez petite pour être comprise et revert rapidement.
Le code mort dans les monorepos
Les monorepos rendent le code mort à la fois plus facile et plus difficile à détecter.
Plus facile, parce que tous les packages vivent au même endroit et les graphes de dépendances peuvent montrer tout le système. Plus difficile, parce que les packages internes restent souvent « disponibles » longtemps après qu'aucune application n'en a besoin.
Cherchez les packages de workspace qui :
- ne sont dépendance d'aucune application
- ne dépendent que d'autres packages dépréciés
- n'ont pas de commits récents hors formatage ou mises à jour de dépendances
- publient des artefacts qu'aucun déploiement ne consomme
- existent seulement parce qu'un autre ancien package les importe encore
Dans un monorepo, nettoyer le code mort est souvent un problème de graphe, pas seulement un problème de fichiers. Vous ne supprimez pas seulement des fonctions inutilisées ; vous élaguez des branches inaccessibles de l'architecture.
Ce qu'il ne faut pas supprimer trop vite
Certains codes demandent une prudence supplémentaire :
- Endpoints d'API publique
- Migrations de base de données
- Journaux d'audit et exports de conformité
- Flux de facturation ou d'abonnement
- Callbacks d'authentification
- Webhooks
- Scripts utilisés par le support ou les opérations
- Code chargé par convention plutôt que par import
Pour ces zones, exigez des preuves plus fortes : données runtime, confirmation d'owner et plan de dépréciation progressif.
Rendre le nettoyage continu
Le nettoyage du code mort fonctionne mieux quand il est continu, pas héroïque.
Ajoutez une habitude de maintenance récurrente :
- lancer la détection d'exports inutilisés chaque mois
- revoir les îlots du graphe de dépendances après les releases majeures
- supprimer les feature flags peu après les décisions de rollout
- exiger un owner pour les modules dépréciés de longue durée
- ajouter les tâches de nettoyage dans la même epic que les remplacements
Le but n'est pas de rendre la codebase parfaitement minimale. Le but est de la garder assez lisible pour que le système actif reste visible.
Quand le code mort disparaît, l'architecture devient plus facile à voir. L'onboarding accélère. Les refactorings deviennent moins intimidants. Et chaque futur développeur passe plus de temps à comprendre le système qui existe, pas les systèmes qui existaient avant.
Articles similaires
Architecture Decision Records : comment ne plus perdre le contexte technique
Découvrez comment les Architecture Decision Records aident les équipes à préserver le contexte technique, réduire la dérive architecturale et faciliter les refactorings.
Continuer la lectureMicroservices vs Monolithe : Comment réellement visualiser votre architecture
Monolithe ou microservices ? La vraie question est : pouvez-vous voir ce que vous avez réellement ? Apprenez à visualiser, comparer et décider en confiance.
Continuer la lectureComment détecter les dépendances circulaires dans votre projet TypeScript (et les corriger)
Découvrez 3 méthodes pratiques pour détecter les dépendances circulaires en TypeScript : madge CLI, ESLint import/no-cycle, et les checks PR automatiques avec ReposLens.
Continuer la lecture