Supprimer une fonction obsolète d'un code source est, en théorie, l'une des choses les plus simples qu'un développeur puisse faire : supprimer la définition, vérifier qu'elle n'est plus utilisée, puis valider les modifications. En pratique, pour toute fonction suffisamment ancienne pour être considérée comme obsolète, c'est précisément à l'étape de vérification que le processus se complique. La fonction peut être appelée par du code écrit il y a des années par un membre de l'équipe qui ne la possède plus, dans un dépôt rarement mis à jour, ou encore par un langage ou un framework qui n'appartient pas à l'équipe actuelle. Elle peut être invoquée indirectement via un wrapper, par réflexion, ou par un mécanisme de répartition d'exécution qui n'apparaît dans aucun graphe d'appels statique. Elle peut être référencée dans du code généré, dans des tests, ou dans un fichier de configuration qui la déclenche par son nom. Le développeur qui la marque comme obsolète et celui qui la supprime finalement n'ont souvent aucun moyen de le savoir sans un outil capable de dresser un inventaire complet des fonctions appelantes, quel que soit le dépôt.
Identifiez tous les appelants avant de supprimer quoi que ce soit.
SMART TS XL construit un graphe d'appels interlangages qui identifie chaque appelant de chaque fonction avant que vous n'effectuiez une modification.
Cliquez iciLe coût d'une telle erreur est immédiat et concret. La suppression d'une fonction sans identification complète de l'appelant entraîne des défaillances d'exécution dans les systèmes qui en dépendent encore. Dans une architecture monolithique à déploiement unique, la surface de défaillance est limitée. Dans un système distribué avec plusieurs services, chacun déployé indépendamment, les défaillances se propagent en cascade : le service fournissant la fonction est mis à jour, mais pas les consommateurs, et des dysfonctionnements apparaissent en production, dans des systèmes pouvant appartenir à différentes équipes. Dans un environnement mainframe où les programmes COBOL appellent des blocs utilitaires partagés par leur nom, la défaillance peut ne se manifester qu'à l'exécution d'un traitement par lots spécifique, hebdomadaire ou mensuel, rendant la référence manquante invisible lors des cycles de test habituels. Dans le contexte plus large de la gestion du code obsolète , les risques s'accumulent avec le temps : un code obsolète supprimé de manière incomplète est plus dangereux qu'un code obsolète laissé en place, car la suppression crée l'illusion d'une suppression complète tandis que les appelants restants continuent d'exécuter une définition qui n'existe plus.
Cet article est un guide pratique pour la découverte des appelants avant la suppression de fonctions : ce qu’exige un inventaire complet des appelants, pourquoi les outils que les développeurs utilisent en premier lieu sont structurellement insuffisants, comment différents types de relations d’appel nécessitent différentes approches d’analyse et à quoi ressemble une véritable énumération des appelants inter-systèmes dans les bases de code à l’échelle de l’entreprise qui mélangent langages, plateformes et référentiels.
Pourquoi la recherche de l'appelant est plus difficile qu'il n'y paraît
La méthode de recherche des appelants la plus simple est familière à tous les développeurs : un clic droit sur le nom d’une fonction dans un EDI, la sélection de « Rechercher toutes les références » ou « Afficher la hiérarchie des appels », puis l’examen des résultats. Cette méthode fonctionne correctement dans le cadre d’un projet unique ouvert dans une seule instance d’EDI. Dès que le code source s’étend au-delà de ce cadre, les résultats deviennent incomplets, et ces incomplétude ne sont pas visibles dans l’affichage. L’EDI n’indique pas les appelants manquants, car il n’a pas indexé les dépôts qui les contiennent. Le développeur voit alors un ensemble de résultats apparemment complet et poursuit son travail en conséquence.
Voici le problème structurel de la découverte des appelants à grande échelle : les outils les plus utilisés par les développeurs sont limités par leur périmètre d’indexation. Or, dans les grands systèmes distribués et multilingues, ce périmètre ne couvre qu’une fraction des endroits où une fonction donnée peut être appelée. La confiance du développeur dans l’exhaustivité des résultats est inversement proportionnelle à l’exhaustivité réelle de la recherche. Dans un petit code source monolingue, la hiérarchie des appels de l’IDE est parfaitement fiable. En revanche, dans un système d’entreprise couvrant plusieurs dépôts, langages et environnements de déploiement, elle est systématiquement trompeuse. Comme l’illustrent l’analyse de l’ entropie du code et du risque de refactorisation , les modules hérités peuvent dépendre d’interfaces obsolètes tandis que les services plus récents appellent encore des routines initialement conçues pour des environnements antérieurs. Ces relations d’appels intersystèmes sont précisément celles que la recherche limitée à l’IDE ne peut pas détecter.
Pour comprendre les raisons précises des échecs de la recherche de l'appelant, il est nécessaire d'examiner chaque type d'appel principal : appels directs, appels indirects, appels interlingues et répartition dynamique. Chaque type d'appel échoue pour des raisons différentes et requiert des techniques d'analyse spécifiques pour être correctement résolu.
Appels directs au-delà des limites du référentiel
Les appels directs sont le type d'appel le plus simple : une fonction appelle explicitement une autre par son nom. Au sein d'un même dépôt, les IDE les gèrent correctement. Entre dépôts différents, l'analyse échoue car l'indexation de l'IDE ne couvre pas la frontière. Si le dépôt A définit une fonction utilitaire partagée et que les dépôts B, C et D l'importent et l'appellent, l'IDE de chacun de ces dépôts ne verra que les appels situés dans sa propre portée indexée.
Ce modèle d'appel multi-dépôts est la norme plutôt que l'exception dans les architectures de microservices, où les bibliothèques partagées sont publiées sous forme de packages et utilisées par des dizaines de services. Le responsable de la maintenance d'une bibliothèque qui rend obsolète une fonction dans le package partagé doit savoir quels services l'utilisent encore. Son IDE ignore tout des consommateurs. Le gestionnaire de packages sait quels services dépendent du package, mais pas quelle fonction spécifique chaque service appelle au sein de ce package. Faire correspondre « ce service utilise la version X de ce package » à « ce service appelle cette fonction obsolète spécifique » nécessite d'indexer le code source de chaque consommateur et de résoudre l'appel à la définition de la fonction correspondante.
Appels indirects : wrappers, délégués et façades
Une fonction ne peut pas être appelée directement par ses utilisateurs. Elle peut être appelée via une fonction enveloppe qui fournit des fonctionnalités supplémentaires de journalisation, de gestion des erreurs ou de transformation des paramètres. Elle peut être assignée à un délégué ou à un pointeur de fonction et invoquée par l'intermédiaire de ce délégué. Elle peut être enregistrée dans un registre de services ou un framework de plugins et appelée par son nom via un mécanisme de répartition. Dans chacun de ces cas, une recherche directe d'appels à la fonction obsolète renvoie un résultat incomplet, car les appelants réels appellent la fonction enveloppe ou le répartiteur, et non la fonction obsolète elle-même.
L'invocation via un wrapper est particulièrement fréquente dans les grands projets où les problématiques transversales telles que la journalisation, l'autorisation et la logique de nouvelle tentative sont intégrées aux fonctions principales grâce à des modèles de conception basés sur des wrappers. Une fonction obsolète encapsulée par un utilitaire de journalisation est en réalité appelée par chaque appelant du wrapper, et non par le code contenant le nom de la fonction obsolète. Identifier ces appelants nécessite de parcourir le graphe d'appels du wrapper : l'utilitaire de journalisation appelle la fonction obsolète, et chaque appelant de l'utilitaire de journalisation est donc un appelant indirect de la fonction obsolète. Ce parcours récursif du graphe d'appels est ce qui distingue un inventaire complet des appelants d'une recherche de références superficielle.
Prenons l'exemple représentatif suivant en Java, où une méthode obsolète est accessible via une couche de délégation :
Java
// Deprecated in the core service
@Deprecated
public BillingResult calculateLegacyFee(Account account) {
// original implementation
}
// Facade that delegates; not visible as a direct caller in a simple reference search
public BillingResult computeFee(Account account) {
return calculateLegacyFee(account); // indirect caller
}
// Actual consumer; calls computeFee, unaware of the underlying deprecated method
public void processMonthlyBilling(List<Account> accounts) {
accounts.forEach(a -> computeFee(a)); // two hops from the deprecated function
}
Une recherche « Trouver toutes les références » pour calculateLegacyFee Retours computeFee comme seul appelant. Il ne renvoie pas processMonthlyBilling, qui est le véritable consommateur du comportement obsolète. Un inventaire complet des appelants nécessite de parcourir le graphe d'appels en amont. computeFee identifier tous les chemins qui invoquent finalement la méthode obsolète.
Invocations interlingue
Les appels interlangages constituent le domaine où les outils standard de découverte d'appelants échouent le plus. Lorsqu'un service Java invoque un programme COBOL par son nom via une couche intermédiaire, lorsqu'un script Python appelle une procédure stockée encapsulant une fonction obsolète, ou lorsqu'une tâche JCL invoque un programme par son PROGNAME qui appelle en interne un paragraphe obsolète, aucune de ces relations n'apparaît dans le graphe d'appels d'un langage donné. Les outils de chaque langage ne perçoivent que leur propre côté de l'appel.
Dans les environnements mainframe, les appels inter-langages sont structurels et omniprésents. Un flux de travaux JCL spécifie le nom du programme COBOL qu'il exécute. Ce programme COBOL appelle des paragraphes et des sous-programmes par leur nom. Les paragraphes utilitaires définis dans les bibliothèques de copies sont partagés par de nombreux programmes. Lorsqu'un paragraphe d'une bibliothèque de copies est obsolète, identifier tous les appelants nécessite de comprendre les relations d'appel COBOL (quels programmes incluent la bibliothèque de copies et lesquels appellent le paragraphe), les relations d'invocation JCL (quels travaux invoquent ces programmes) et les interfaces inter-langages (Java ou SQL interagissant avec ces programmes). Aucun outil unique ne couvre l'ensemble de ces relations. Comme l'a montré l'analyse statique des systèmes hérités , les outils d'analyse statique conçus pour les environnements modernes ne peuvent pas appréhender pleinement la manière dont les programmes hérités sont déclenchés, appelés et interconnectés lorsque les relations d'appel s'étendent simultanément sur le JCL, le COBOL et les interfaces inter-systèmes.
Dispatch et réflexion dynamiques
Certains appelants invoquent une fonction non pas par son nom littéral dans le code source, mais par un mécanisme qui résout la fonction lors de l'exécution : la réflexion en Java ou .NET, getattr/__call__ en Python, liaison tardive en COBOL via CALL identifier, la répartition dynamique via le polymorphisme, ou l'invocation par chaîne de caractères dans les frameworks de plugins et les systèmes pilotés par configuration. Ces appelants ne contiennent pas le nom de la fonction obsolète sous une forme que l'analyse statique puisse détecter de manière fiable.
Un fichier de configuration spécifiant le nom d'une fonction sous forme de chaîne de caractères, chargé à l'exécution et utilisé pour invoquer la fonction par réflexion, est un appelant qui n'apparaît nulle part dans l'analyse du code source. Un framework de plugins découvrant et invoquant des gestionnaires enregistrés par interface est un appelant qui apparaît dans le graphe d'appels uniquement comme un appel au mécanisme de répartition, et non comme un appel à un gestionnaire spécifique. L'identification de ces appelants requiert une combinaison d'analyse statique pour détecter les schémas de répartition dynamiques, de traçage à l'exécution pour observer les invocations réelles et d'inspection manuelle de la logique de configuration et d'enregistrement qui détermine les fonctions appelées. Comme indiqué dans l'analyse statique de code obfusqué et généré , lorsque les chemins d'exécution ne sont pas exprimés directement dans le code source, l'analyse statique doit reconstruire les chemins probables à partir de schémas structurels plutôt que de références textuelles directes. Ces reconstructions nécessitent une analyse du mécanisme de répartition lui-même, prenant en compte le langage.
Les outils que les développeurs utilisent en premier et leurs limites
Les développeurs utilisent une séquence d'outils bien définie pour identifier l'appelant, et chacun d'eux cesse, tout aussi prévisiblement, de fournir des résultats fiables à un moment précis. Il est important de comprendre cette séquence, car les résultats de chaque outil semblent complets même lorsqu'ils ne le sont pas.
Hiérarchie des appels IDE : Fiable au sein d’un même projet
Les fonctionnalités d'analyse de la hiérarchie des appels dans les environnements de développement intégrés (IDE) constituent la première étape la plus naturelle. IntelliJ IDEA, Visual Studio, VS Code et Eclipse proposent tous une fonction permettant de « trouver tous les appelants » ou d'« afficher la hiérarchie des appels » qui énumère récursivement les appelants d'une fonction sélectionnée, dans la portée indexée du projet ou de l'espace de travail courant. Pour une fonction utilisée exclusivement dans un seul dépôt et un seul langage, ces fonctionnalités sont précises et suffisantes.
La limitation est explicite dans la définition du périmètre : « dans le périmètre indexé ». Les appels provenant d’autres dépôts, d’autres environnements d’exécution ou de services dépendant de ce code via un gestionnaire de paquets plutôt que par une référence directe au projet sont hors périmètre. L’IDE n’indique pas les éléments non indexés. Le développeur reçoit un ensemble de résultats sans savoir combien de dépôts supplémentaires n’ont pas été indexés, combien d’entre eux utilisent la fonction obsolète, ni si le résultat « zéro appelant » signifie réellement zéro appelant ou seulement « zéro appelant dans la partie du système accessible par cet outil ».
grep et la recherche textuelle : vaste mais structurellement aveugle
Lorsque la recherche dans l'IDE est jugée incomplète, l'étape suivante consiste généralement en une recherche textuelle : `grep` dans les répertoires sources disponibles, ou recherche sur la plateforme via la recherche de code de GitHub ou GitLab. Cette méthode élargit considérablement le champ de recherche et permet de trouver les fonctions appelantes dans d'autres dépôts, si ceux-ci sont accessibles. Le problème structurel est que la recherche textuelle trouve des chaînes de caractères, et non les appels. Elle renvoie toutes les occurrences du nom de la fonction, y compris les commentaires qui la mentionnent, les chaînes de documentation, les messages de journalisation qui la nomment à des fins de débogage, et les chaînes littérales contenant le nom de la fonction sans l'appeler. Elle manque également les fonctions appelantes lorsque le nom de la fonction diffère de la chaîne de recherche : les appels via des alias, les correspondances partielles en COBOL (où les noms peuvent être abrégés) ou les invocations dynamiques (où le nom est assemblé à l'exécution).
Les résultats de la recherche textuelle nécessitent un filtrage manuel pour distinguer les sites d'appels réels, les références documentaires et les faux positifs dus à des collisions de chaînes de caractères. Dans un système de grande envergure, ce filtrage représente un effort considérable et ne garantit pas l'exhaustivité des résultats : si un appelant a été omis en raison d'un nom différent, les résultats filtrés ne contiennent aucune indication de cette omission.
Avertissements du compilateur et @Deprecated Annotations
Les langages et chaînes d'outils modernes offrent des mécanismes d'annotation de dépréciation qui génèrent des avertissements lors de l'appel de fonctions obsolètes. C'est le cas notamment de Java. @Deprecated annotation combinée avec -Xlint:deprecation génère des avertissements à la compilation aux points d'appel. C# [Obsolete] L'attribut génère des avertissements lors de la compilation. La convention Go consistant à nommer les fonctions obsolètes et à les documenter dans godoc ne produit pas d'avertissements automatiquement. Ces mécanismes sont utiles, mais présentent une limite : ils ne fonctionnent que pour les fonctions compilées sur le même code source que celui où l'obsolescence est signalée.
Un appelant qui utilise une version plus ancienne de la bibliothèque, antérieure à la @Deprecated Une annotation ne génère aucun avertissement. Un appelant utilisant un artefact binaire plutôt que de compiler à partir du code source ne reçoit aucun avertissement. Un appelant dans un autre langage, via une interface interlangage, ne reçoit aucun avertissement. Surtout, les avertissements produits lors de la compilation sont locaux au compilateur : ils concernent les appels qu'il détecte, et non ceux provenant d'autres dépôts compilés séparément. Utiliser les avertissements du compilateur comme unique mécanisme de détection des appelants dans un système multiservices ne détecte pas tous les appelants compilés indépendamment, ce qui est pourtant la norme dans les architectures de microservices.
Outils d'analyse statique : meilleurs, mais limités dans leur portée
Les outils d'analyse statique dédiés offrent une énumération des appelants plus précise que les IDE pour leur langage cible et peuvent souvent indexer plusieurs bases de code au-delà des limites des dépôts. Ils construisent des graphes d'appels corrects au lieu de se fier à la correspondance textuelle, gèrent mieux les alias et les appels indirects que la recherche des IDE, et peuvent être exécutés dans des pipelines d'intégration continue pour détecter les nouveaux appelants dès leur ajout. Ils constituent l'approche monolingue la plus performante disponible.
La limitation réside dans la même limite de portée qui restreint les EDI, mais cette fois au niveau de l'outil : un outil d'analyse statique Java n'indexe pas les programmes COBOL, un analyseur COBOL n'indexe pas les services Java, et aucun des deux n'indexe les flux de travaux JCL. Dans un système où la fonction obsolète est un utilitaire COBOL appelé depuis des programmes COBOL eux-mêmes invoqués par des travaux JCL et dont les données de sortie sont consommées par des services Java, chaque outil d'analyse statique ne perçoit qu'un fragment de la relation d'appel. Comme l'illustre l'analyse des techniques essentielles de refactorisation , l'identification de tous les chemins d'exécution vers une section de code donnée, y compris les cas d'erreur rares et les branches de repli, exige une cartographie complète du graphe d'appels, impossible à réaliser par les outils monolangages au-delà des frontières linguistiques.
Ce qu'implique réellement un inventaire complet des appelants
L'inventaire complet des appelants d'une fonction obsolète dans un système d'entreprise ne se résume pas à un résultat de recherche. Il s'agit d'une énumération structurée de tous les chemins d'exécution permettant d'accéder à cette fonction, y compris les chemins directs, indirects, interlangages et à répartition dynamique. La construction de cette énumération requiert plusieurs fonctionnalités qu'aucun outil standard ne fournit.
Un graphe d'appels interlangage unifié. Ce graphe doit couvrir tous les langages du système. Un appel d'une procédure JCL vers un programme COBOL, un appel du programme COBOL vers un paragraphe utilitaire partagé, et un appel d'un service Java vers le même programme COBOL via une interface middleware doivent tous être des nœuds et des arêtes de ce graphe. La fonction obsolète est un nœud de ce graphe, et l'énumération des appelants correspond au parcours de toutes les arêtes entrantes, directes et transitives, quel que soit leur langage d'origine.
Parcours récursif de l'intégralité du graphe d'appels. Les appelants directs ne constituent que le premier niveau. Un inventaire complet nécessite de remonter le graphe d'appels en passant par les appelants indirects, les fonctions d'encapsulation et les couches de façade, jusqu'à atteindre les fonctions sans appelant, qui sont les véritables points d'entrée des chaînes d'appels. Toute fonction d'un chemin aboutissant à la fonction obsolète est un appelant au sens pertinent : la suppression de la fonction obsolète interrompra tous les chemins la traversant.
Indexation inter-dépôts. Le graphe d'appels doit inclure le code de chaque dépôt susceptible d'appeler la fonction, y compris les dépôts qui en dépendent via une bibliothèque ou un paquet partagé. Cela nécessite l'indexation simultanée de tous les dépôts et la résolution des relations d'importation inter-dépôts afin de relier les appels d'un dépôt aux définitions d'un autre.
Détection des schémas d'invocation indirecte. L'analyse doit identifier les appels effectués par réflexion, répartition dynamique, pointeurs de fonction, délégués et invocation par chaîne de caractères dans les fichiers de configuration. Ces appels nécessitent une détection par schéma plutôt qu'une résolution directe des limites d'appel : il s'agit de trouver les mécanismes de répartition dynamique dans le code et de déterminer les fonctions auxquelles ils peuvent faire appel et dans quelles conditions.
Il est important de distinguer les appelants actifs des appelants de test ou inactifs. Tous les appelants n'exigent pas la même réponse. Un appelant présent uniquement dans un environnement de test pour la fonction obsolète doit être supprimé lors du nettoyage, et non migré. Un appelant dans du code identifié comme inactif par analyse d'utilisation ne constitue pas un obstacle à la suppression de la fonction. Comprendre ces distinctions nécessite de combiner l'énumération des appelants avec des informations sur les chemins d'exécution réellement actifs. Comme détaillé dans l'analyse statique de la détection du code inactif , du code inaccessible et des fonctions inutilisées peuvent persister pendant des années dans des systèmes critiques en raison d'une documentation incomplète ou d'incertitudes concernant les dépendances historiques. L'inventaire des appelants d'une fonction obsolète doit donc faire la distinction entre les appelants actifs et les appelants inactifs.
Le processus de dépréciation et de mise hors service : une approche structurée
Considérer la suppression des fonctions obsolètes comme un événement ponctuel plutôt que comme un processus structuré est à l'origine de la plupart des échecs de détection des appelants. L'approche correcte consiste à considérer la suppression comme l'étape finale d'un processus en plusieurs phases qui débute bien avant toute suppression de code.
Phase 1 : Marquer et mesurer
La première étape consiste à annoter la fonction comme obsolète en utilisant le mécanisme intégré du langage (@Deprecated en Java, [Obsolete] en C#, #[deprecated] En Rust (ou son équivalent), on indexe le code source de la fonction et on établit un nombre de référence d'appels. Ce nombre de référence ne résulte pas d'une recherche unique ; il est obtenu en indexant l'ensemble du code source connu susceptible d'appeler la fonction et en comptabilisant les résultats. Ce nombre de référence remplit deux objectifs : il quantifie la portée de la migration et sert de point de repère pour mesurer la progression de la migration des appelants.
Les données de référence doivent être organisées par type d'appelant et par emplacement :
| Catégorie d'appelant | que vous avez | Priorité | Propriétaire |
|---|---|---|---|
| Appelants directs dans le même dépôt | N | Haute | Équipe actuelle |
| Appels directs dans les services aux personnes dépendantes | N | Haute | propriétaires de services |
| Appels via des fonctions d'encapsulation | N | Moyenne | propriétaires d'emballages |
| Appelants dans le code généré ou le framework | N | Moyenne | Équipe Framework |
| Appelants dans le code de test uniquement | N | Low | Équipe actuelle |
| Appelants en code mort | N | Nettoyage uniquement | Équipe actuelle |
Phase 2 : Notification et migration
Avec un inventaire complet des appelants, la migration devient un effort organisé plutôt que réactif. Chaque responsable d'appelant est informé des emplacements d'appel précis : non pas « vous appelez peut-être cette fonction », mais « vous appelez cette fonction à la ligne 247 de BillingService.java, ligne 82 de AccountProcessor.java, et dans le test d'intégration à la ligne 14 de BillingServiceTest.java« Ce niveau de précision est ce que l’inventaire de l’appelant rend possible et ce que les avertissements de dépréciation génériques ne peuvent pas fournir. »
Il est essentiel de fournir une procédure de migration en même temps que la notification de dépréciation. Cette dernière doit inclure la documentation de la fonction de remplacement, une description des différences de comportement entre les anciennes et nouvelles implémentations et, si la modification est importante, un exemple de code illustrant l'avant et l'après. Pour les utilisateurs travaillant sur le code d'autres équipes, le calendrier de migration doit être négocié explicitement plutôt qu'annoncé unilatéralement, car ces équipes ont leurs propres priorités et engagements de livraison. Comme illustré dans le contexte de la refactorisation de bases de données entre systèmes dépendants , l'intégration progressive des utilisateurs de la nouvelle structure avant la dépréciation de l'ancienne est la méthode qui permet d'éviter que les changements incompatibles ne soient perçus comme des incidents inattendus.
Phase 3 : Surveiller le nombre d'appelants
Entre la mesure initiale et la date de suppression prévue, le nombre d'appelants doit être surveillé en continu. À chaque migration d'un appelant vers la fonction de remplacement, ce nombre diminue. La suppression est déclenchée lorsque le nombre d'appelants actifs atteint zéro (les appelants de test uniquement et ceux contenant du code mort peuvent être supprimés simultanément avec la fonction elle-même). Cette surveillance continue nécessite l'exécution régulière du recensement des appelants dans le cadre du pipeline d'intégration continue, et non un inventaire ponctuel qui devient obsolète avec les modifications du code.
La surveillance détecte également les nouveaux appels ajoutés pendant la période de dépréciation. Dans les grandes organisations, il est fréquent que du nouveau code appelant une fonction obsolète soit écrit pendant la migration, soit parce que le développeur ignorait la dépréciation, soit parce qu'une revue de code l'a manquée, soit parce qu'un générateur de code automatisé produit du code appelant la fonction obsolète. La détection des appels au niveau de l'intégration continue pour la fonction obsolète, configurée pour échouer sur les nouveaux points d'appel, empêche le nombre d'appels d'augmenter pendant la migration.
Phase 4 : Vérifier l'intégralité avant le retrait
Juste avant de supprimer la fonction, l'énumération des appelants doit être exécutée une dernière fois sur l'ensemble des bases de code connues. Cette vérification finale constitue une sécurité supplémentaire : elle confirme que le nombre d'appelants actifs a atteint zéro et identifie les ajouts tardifs non détectés par la surveillance de l'intégration continue. À ce stade, l'inventaire doit également vérifier l'absence d'appelants dynamiques : fichiers de configuration référençant la fonction par une chaîne de caractères, enregistrements par réflexion et tout autre mécanisme d'invocation indirecte identifié lors de l'analyse initiale.
La vérification doit s'étendre au graphe de dépendances de toutes les bibliothèques partagées ou de tous les packages exposant la fonction obsolète. Si la fonction fait partie d'une API publique utilisée par des tiers, le calendrier de suppression doit tenir compte des consommateurs externes qui ne sont pas identifiables par l'analyse du code interne. Pour les systèmes internes, la vérification couvre l'intégralité du code source indexé. Pour les API publiques, la vérification couvre l'ensemble des consommateurs connus, ainsi qu'une période de transition définie durant laquelle les consommateurs externes doivent migrer.
Comment la découverte de l'appelant fonctionne différemment dans les environnements hérités et les environnements mainframe
Les défis décrits ci-dessus s'appliquent à tout système logiciel de grande envergure, mais ils sont particulièrement aigus dans les environnements mainframe et hérités, car les relations d'appel dans ces environnements sont exprimées par des mécanismes que les outils modernes de découverte d'appelants n'ont pas été conçus pour analyser.
Dans les environnements COBOL, les fonctions sont appelées par des instructions CALL qui peuvent référencer la cible par une chaîne littérale, par un élément de données contenant le nom du programme, ou par un pointeur de procédure. Le cas de la chaîne littérale est résolu par une analyse statique ; le cas de l’élément de données nécessite une analyse du flux de données pour déterminer la valeur que l’élément de données peut contenir au moment de l’appel ; et le cas du pointeur de procédure requiert de suivre la manière dont le pointeur est affecté. Chacun de ces mécanismes d’appel apparaît différemment dans le code source et nécessite une analyse différente pour être résolu.
Dans les environnements JCL, les programmes sont appelés par leur nom dans des instructions EXEC PGM=. Le nom du programme est une chaîne de caractères qui correspond à un module compilé dans une bibliothèque de chargement. Le traçage des appelants d'un programme COBOL via JCL nécessite l'analyse syntaxique du JCL pour extraire les noms des programmes, leur correspondance avec les programmes COBOL compilés qui les implémentent, et la détermination des paragraphes COBOL, au sein de ces programmes, qui appellent l'utilitaire obsolète. Cette résolution en plusieurs étapes dépasse totalement le cadre d'un analyseur COBOL ou d'un analyseur JCL fonctionnant isolément.
Les copybooks partagés constituent un cas particulièrement important dans les environnements COBOL. Un paragraphe obsolète défini dans un copybook peut être inclus dans de nombreux programmes via des instructions COPY. Ce paragraphe n'est pas dupliqué physiquement dans chaque programme ; il est inclus à la compilation. Une analyse qui compte les occurrences du nom du paragraphe dans les fichiers sources sans résoudre les inclusions de copybooks entraînera à la fois un surcomptage (en trouvant la définition du paragraphe dans le copybook lui-même) et un sous-comptage (en ignorant le fait que chaque programme incluant le copybook a accès au paragraphe). Une identification correcte des appelants nécessite de comprendre quels programmes incluent quels copybooks et quels paragraphes de ces copybooks ils appellent. La relation entre les références codées en dur et leurs consommateurs en aval illustre pourquoi la résolution de ces relations d'invocation au niveau du programme est essentielle avant toute modification structurelle : ce qui semble être une simple référence de chaîne peut être le seul mécanisme par lequel des dizaines de programmes accèdent à des fonctionnalités critiques.
Comment SMART TS XL Constitue l'inventaire complet des appelants
SMART TS XL Ce système construit un graphe d'appels unifié pour chaque langage, plateforme et référentiel de l'environnement indexé. Les programmes COBOL, les flux de travaux JCL, les services Java, les applications .NET, les procédures stockées SQL, les scripts Python et autres artefacts sources sont analysés par des méthodes spécifiques à chaque langage et intégrés dans un graphe de références croisées commun. Chaque fonction, paragraphe, procédure, méthode et unité de programme constitue un nœud de ce graphe. Chaque relation d'appel, qu'il s'agisse d'une instruction COBOL CALL, d'un appel de méthode Java, d'un JCL EXEC PGM ou d'un SQL EXEC, est représentée par une arête typée. Le graphe représente la topologie complète des appels du système, et non une vue partielle par langage.
Lorsqu'une fonction est marquée pour suppression, SMART TS XLL'énumération des appelants parcourt le graphe d'appels entrant depuis le nœud de la fonction cible, recensant chaque appelant à chaque niveau de la hiérarchie d'appels. Ce parcours est récursif et suit le graphe à travers les fonctions d'encapsulation, les couches de façade et les utilitaires intermédiaires jusqu'à atteindre les fonctions sans appelant, qui représentent les véritables points d'entrée des chaînes d'appels. Les résultats sont organisés par langage, par dépôt, par type d'appelant et par profondeur d'appel, offrant ainsi à l'équipe un inventaire structuré qui distingue les appelants directs des appelants indirects et les appelants actifs des appelants de code mort.
La fonctionnalité d'analyse d'impact de la plateforme étend cette analyse à un rapport structuré d'impact des changements : elle indique non seulement les fonctions qui appellent la fonction obsolète, mais aussi les programmes, services, traitements par lots et procédures JCL affectés à chaque niveau de la chaîne de dépendances. Ce rapport est l'élément essentiel qui rend opérationnel le processus de suppression des composants obsolètes : il nomme les responsables, identifie les appels spécifiques et quantifie l'étendue de la migration nécessaire avant que la suppression puisse être effectuée en toute sécurité. Comme expliqué en détail dans l' analyse d'impact pour la gestion des changements en entreprise , la capacité à recenser les composants affectés avant d'apporter une modification structurelle est une condition fondamentale au bon fonctionnement des systèmes d'entreprise complexes et interconnectés.
SMART TS XL Ce système prend également en charge la phase de surveillance continue du processus de dépréciation. Grâce à la mise à jour constante du graphe de références croisées lors de l'indexation des modifications du code source, le nombre d'appels à une fonction dépréciée est toujours à jour. L'intégration au pipeline d'intégration continue permet aux vérifications automatisées de signaler les nouveaux appels à des fonctions dépréciées, imposant ainsi la discipline de migration dès l'introduction de nouveau code, plutôt que de constater les violations a posteriori. Cette combinaison d'énumération initiale, de conseils de migration et de surveillance continue couvre l'intégralité du cycle de vie d'une fonction dépréciée, de l'annotation à sa suppression sécurisée.
Suppression de fonction sans regret
La différence entre une suppression de fonction réussie et une suppression entraînant des pannes en production réside presque toujours dans l'exhaustivité de la découverte des appelants. La suppression en elle-même est simple : il suffit de supprimer la définition et de déployer. C'est la préparation qui pose problème, et sa qualité dépend de l'inventaire des appelants sur lequel elle repose.
Dans les systèmes où le graphe d'appels est peu profond, monolingue et contenu dans un seul dépôt, la hiérarchie des appels de l'IDE et les avertissements du compilateur constituent une préparation suffisante. En revanche, dans les systèmes où le graphe d'appels s'étend sur plusieurs langages, plusieurs dépôts, plusieurs plateformes et potentiellement plusieurs décennies de code, ces outils ne couvrent qu'une petite fraction, souvent inconnue, de la surface d'appel réelle. C'est dans l'écart entre ce qu'ils retournent et ce qui appelle réellement la fonction que naissent les défaillances en production.
L'énumération structurée des appelants, conçue spécifiquement pour être interlangage et interdépôt, ne constitue pas une simple amélioration du processus de suppression de fonctions pour les développeurs. Elle est indispensable pour garantir la sécurité de ce processus dans tout système suffisamment complexe pour avoir accumulé les relations d'appels intersystèmes que les fonctions obsolètes des bases de code d'entreprise entretiennent couramment. Chaque fonction obsolète supprimée sans un inventaire complet de ses appelants représente une version susceptible de générer un nombre inconnu d'erreurs d'exécution, en attente du chemin d'exécution spécifique qui atteint la définition manquante. L'objectif de la découverte structurée des appelants est précisément d'éliminer cette incertitude.