Alle Aufrufer einer veralteten Funktion finden

Wie man alle Aufrufer einer veralteten Funktion findet, bevor man sie entfernt

Das Entfernen einer veralteten Funktion aus dem Code ist konzeptionell eine der einfachsten Aufgaben für Entwickler: Definition löschen, prüfen, ob sie noch verwendet wird, und die Änderungen committen. In der Praxis scheitert der Prozess jedoch bei Funktionen, die lange genug existieren, um als veraltet zu gelten, genau an diesem Schritt: der Prüfung, ob sie noch verwendet wird. Die Funktion kann von Code aufgerufen werden, der vor Jahren von einem ehemaligen Teammitglied geschrieben wurde, in einem Repository, das nur selten aktualisiert wird, oder von einer Sprache oder einem Framework, das nicht zum aktuellen Team gehört. Sie kann indirekt über einen Wrapper, per Reflection oder über einen Laufzeitmechanismus aufgerufen werden, der in keinem statischen Aufrufgraphen erscheint. Sie kann in generiertem Code, im Testgerüst oder in einer Konfigurationsdatei referenziert werden, die sie namentlich auslöst. Der Entwickler, der die Funktion als veraltet markiert, und derjenige, der sie schließlich entfernt, haben ohne ein Tool, das eine vollständige, repositoryübergreifende Aufrufliste erstellen kann, möglicherweise keine Möglichkeit, all dies zu erfahren.

Ermitteln Sie jeden Anrufer, bevor Sie etwas löschen.

SMART TS XL Erstellt einen sprachübergreifenden Aufrufgraphen, der jeden Aufrufer einer beliebigen Funktion identifiziert, bevor Sie eine Änderung vornehmen.

Mehr Info

Die Folgen eines Fehlers sind unmittelbar und konkret. Eine Funktion, die mit unvollständiger Aufrufererkennung entfernt wird, verursacht Laufzeitfehler in den Systemen, die weiterhin von ihr abhängen. In einem monolithischen System mit einer einzigen Bereitstellung ist die Fehlerfläche begrenzt. In einem verteilten System mit mehreren, unabhängig voneinander bereitgestellten Diensten breiten sich die Fehler kaskadenartig aus: Der Dienst, der die Funktion bereitgestellt hat, wird aktualisiert, die Nutzer jedoch nicht, und es kommt zur Laufzeit zu Fehlern in Produktionssystemen, die möglicherweise von verschiedenen Teams verwaltet werden. In einer Mainframe-Umgebung, in der COBOL-Programme gemeinsam genutzte Hilfsfunktionen namentlich aufrufen, kann sich der Fehler erst bei der Ausführung eines bestimmten Batch-Jobs manifestieren, der wöchentlich oder monatlich ausgeführt werden kann. Dadurch bleibt die fehlende Referenz während normaler Testzyklen unentdeckt. Betrachtet man den breiteren Kontext der Verwaltung veralteten Codes , so verstärken sich die Risiken mit der Zeit: Unvollständig entfernter veralteter Code ist gefährlicher als verbleibender veralteter Code, da die Entfernung den Anschein von Vollständigkeit erweckt, während die verbleibenden Aufrufer weiterhin auf eine Definition zugreifen, die nicht mehr existiert.

Dieser Artikel ist ein praktischer Leitfaden zur Ermittlung von Aufrufern vor der Funktionsentfernung: Was ein vollständiges Aufruferinventar erfordert, warum die Werkzeuge, zu denen Entwickler zuerst greifen, strukturell unzureichend sind, wie unterschiedliche Arten von Aufrufbeziehungen unterschiedliche Analyseansätze erfordern und wie eine echte systemübergreifende Aufruferaufzählung in Codebasen im Unternehmensmaßstab aussieht, die Sprachen, Plattformen und Repositories mischen.

Warum die Anruferidentifizierung schwieriger ist als sie aussieht

Die einfachste Methode zur Funktionsaufruferkennung ist jedem Entwickler vertraut: Rechtsklick auf einen Funktionsnamen in der IDE, Auswahl von „Alle Verweise suchen“ oder „Aufrufhierarchie anzeigen“ und Überprüfung der Ergebnisse. Innerhalb eines einzelnen Projekts, das in einer einzelnen IDE-Instanz geladen ist, funktioniert dies zuverlässig. Sobald die Codebasis jedoch größer wird, sind die Ergebnisse unvollständig, und zwar auf eine Weise, die in der Ausgabe nicht sichtbar ist. Die IDE zeigt nicht an, welche Aufrufer nicht gefunden wurden, da die entsprechenden Repositories nicht indiziert wurden. Der Entwickler sieht ein scheinbar vollständiges Ergebnis und arbeitet entsprechend weiter.

Dies ist das strukturelle Problem der Aufrufererkennung im großen Maßstab: Die von Entwicklern am häufigsten verwendeten Tools sind durch ihren Indexierungsbereich begrenzt. In großen, verteilten, mehrsprachigen Systemen deckt dieser Bereich jedoch nur einen Bruchteil der Stellen ab, an denen eine bestimmte Funktion aufgerufen werden könnte. Das Vertrauen des Entwicklers in die Vollständigkeit der Ergebnisse ist umgekehrt proportional zur tatsächlichen Vollständigkeit der Suche. In einer kleinen, einsprachigen Codebasis ist die Aufrufhierarchie der IDE tatsächlich zuverlässig. In einem Unternehmenssystem, das sich über mehrere Repositories, Sprachen und Bereitstellungsumgebungen erstreckt, ist sie systematisch irreführend. Betrachtet man die Codeentropie und das Refactoring-Risiko , so können Legacy-Module von veralteten Schnittstellen abhängen, während neuere Dienste noch Routinen aufrufen, die ursprünglich für ältere Umgebungen entwickelt wurden. Genau diese systemübergreifenden Aufrufbeziehungen kann die auf die IDE beschränkte Suche nicht erfassen.

Um die genauen Gründe für das Fehlschlagen der Anrufererkennung zu verstehen, muss jeder wichtige Anruftyp untersucht werden: direkte Anrufe, indirekte Anrufe, sprachübergreifende Aufrufe und dynamische Anrufverteilung. Jeder dieser Typen schlägt aus unterschiedlichen Gründen fehl und erfordert daher unterschiedliche Analysemethoden für eine korrekte Lösung.

Direkte Aufrufe über Repository-Grenzen hinweg

Direkte Funktionsaufrufe sind der einfachste Aufruftyp: Eine Funktion ruft eine andere explizit über ihren Namen auf. Innerhalb eines einzelnen Repositorys verarbeiten IDEs diese zuverlässig. Über Repository-Grenzen hinweg schlägt die Analyse fehl, da die Indizierung der IDE die Grenze nicht abdeckt. Wenn Repository A eine gemeinsam genutzte Hilfsfunktion definiert und die Repositories B, C und D diese jeweils importieren und aufrufen, sieht die IDE jedes dieser Repositories nur die Aufrufe innerhalb ihres eigenen indizierten Bereichs.

Dieses Aufrufmuster über mehrere Repositories ist in Microservice-Architekturen, in denen gemeinsam genutzte Bibliotheken als Pakete veröffentlicht und von Dutzenden von Diensten verwendet werden, eher die Regel als die Ausnahme. Der Bibliotheksentwickler, der eine Funktion im gemeinsamen Paket als veraltet markiert, muss wissen, welche Dienste diese Funktion noch aufrufen. Seine IDE weiß nichts über die Nutzer. Der Paketmanager weiß zwar, welche Dienste vom Paket abhängen, aber nicht, welche spezifische Funktion innerhalb des Pakets jeder Dienst aufruft. Die Zuordnung von „Dieser Dienst verwendet Version X dieses Pakets“ zu „Dieser Dienst ruft diese spezifische, veraltete Funktion auf“ erfordert die Indizierung des Quellcodes jedes Nutzers und die Auflösung des Aufrufs bis zur Definition der spezifischen Funktion.

Indirekte Aufrufe: Wrapper, Delegierte und Fassaden

Eine Funktion darf nicht direkt von ihren Nutzern aufgerufen werden. Sie kann über eine Wrapper-Funktion aufgerufen werden, die zusätzliche Protokollierung, Fehlerbehandlung oder Parametertransformation bereitstellt. Sie kann einem Delegaten oder einem Funktionszeiger zugewiesen und über den Delegaten aufgerufen werden. Sie kann in einer Dienstregistrierung oder einem Plugin-Framework registriert und über einen Dispatch-Mechanismus aufgerufen werden. In all diesen Fällen liefert eine direkte Suche nach Aufrufen der veralteten Funktion ein unvollständiges Ergebnis, da die eigentlichen Aufrufer den Wrapper oder den Dispatcher und nicht die veraltete Funktion selbst aufrufen.

Aufrufe über Wrapper sind besonders häufig in großen Codebasen, in denen übergreifende Funktionen wie Protokollierung, Autorisierung und Wiederholungslogik mithilfe von Wrapper-Mustern um Kernfunktionen herum implementiert sind. Eine veraltete Funktion, die von einem Protokollierungsprogramm umschlossen wird, wird effektiv von jedem Aufrufer des Wrappers aufgerufen, nicht von Code, der den Namen der veralteten Funktion enthält. Um diese Aufrufer zu identifizieren, muss der Aufruf des Wrappers verfolgt werden: Das Protokollierungsprogramm ruft die veraltete Funktion auf, und jeder Aufrufer des Protokollierungsprogramms ist somit ein indirekter Aufrufer der veralteten Funktion. Diese rekursive Durchlaufung des Aufrufgraphen unterscheidet eine vollständige Aufruferanalyse von einer oberflächlichen Referenzsuche.

Betrachten wir ein typisches Java-Beispiel, in dem über eine Delegationsschicht auf eine veraltete Methode zugegriffen wird:

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
}

Eine Suche nach „Alle Referenzen finden“ calculateLegacyFee Rückgabe computeFee als einziger Anrufer. Es meldet sich nicht zurück. processMonthlyBilling, der eigentliche Nutzer des veralteten Verhaltens ist. Eine vollständige Anruferliste erfordert das Durchlaufen des Anrufdiagramms stromaufwärts durch computeFee um jeden Pfad zu identifizieren, der letztendlich die veraltete Methode aufruft.

Sprachübergreifende Aufrufe

Bei sprachübergreifenden Aufrufen versagen Standardwerkzeuge zur Aufrufererkennung am stärksten. Wenn ein Java-Dienst über eine Middleware-Schicht ein COBOL-Programm per Namen aufruft, ein Python-Skript eine gespeicherte Prozedur aufruft, die eine veraltete Funktion umschließt, oder ein JCL-Job ein Programm über seinen PROGNAME aufruft, das intern einen veralteten Paragraphen aufruft, erscheint keine dieser Beziehungen im Aufrufdiagramm einer einzelnen Sprache. Die Werkzeuge jeder Sprache sehen nur ihre eigene Seite des Aufrufs.

In Mainframe-Umgebungen sind sprachübergreifende Aufrufe strukturell verankert und allgegenwärtig. Ein JCL-Jobstream gibt den Namen des ausgeführten COBOL-Programms an. Dieses COBOL-Programm ruft Paragraphen und Unterprogramme namentlich auf. In Kopierbibliotheken definierte Hilfsparagraphen werden von vielen Programmen gemeinsam genutzt. Wird ein Paragraph in einer Kopierbibliothek als veraltet markiert, erfordert die Ermittlung aller Aufrufer das Verständnis der COBOL-Aufrufbeziehungen (welche Programme die Kopierbibliothek einbinden und welche den Paragraphen aufrufen), der JCL-Aufrufbeziehungen (welche Jobs diese Programme aufrufen) sowie aller sprachübergreifenden Schnittstellen (Java oder SQL, die mit diesen Programmen interagieren). Kein einzelnes Tool kann all diese Beziehungen abdecken. Wie die Analyse statischer Analysen auf Legacy-Systemen zeigt , können für moderne Umgebungen entwickelte statische Analysetools nicht das vollständige Bild davon erfassen, wie Legacy-Programme ausgelöst, aufgerufen und miteinander verbunden werden, wenn die Aufrufbeziehungen gleichzeitig JCL, COBOL und systemübergreifende Schnittstellen umfassen.

Dynamische Dispatch- und Reflektionsfunktion

Manche Aufrufer rufen eine Funktion nicht über ihren wörtlichen Namen im Quellcode auf, sondern über einen Mechanismus, der die Funktion zur Laufzeit auflöst: Reflektion in Java oder .NET. getattr/__call__ in Python, späte Bindung in COBOL durch CALL identifierDie dynamische Funktionsaufrufung erfolgt durch Polymorphismus oder durch Aufruf per Zeichenkette in Plugin-Frameworks und konfigurationsgesteuerten Systemen. Diese Aufrufer enthalten den Namen der veralteten Funktion in keiner Form, die durch statische Analyse zuverlässig erkannt werden kann.

Eine Konfigurationsdatei, die einen Funktionsnamen als Zeichenkette angibt, zur Laufzeit geladen und zur Funktionsaufrufung per Reflektion verwendet wird, ist ein Aufrufer, der in keiner Quellcodeanalyse auftaucht. Ein Plugin-Framework, das registrierte Handler über seine Schnittstelle erkennt und aufruft, ist ebenfalls ein Aufrufer, der im Aufrufgraphen nur als Aufruf des Dispatch-Mechanismus erscheint, nicht als Aufruf eines spezifischen Handlers. Die Identifizierung dieser Aufrufer erfordert eine Kombination aus statischer Analyse zur Erkennung dynamischer Dispatch-Muster, Laufzeit-Tracing zur Beobachtung tatsächlicher Aufrufe und manueller Überprüfung der Konfigurations- und Registrierungslogik, die bestimmt, welche Funktionen aufgerufen werden. Wie bereits bei der Untersuchung der statischen Analyse von verschleiertem und generiertem Code erläutert , muss die statische Analyse, wenn Ausführungspfade nicht direkt im Quellcode ausgedrückt sind, die wahrscheinlichen Pfade anhand von Strukturmustern anstatt direkter Textreferenzen rekonstruieren. Diese Rekonstruktionen erfordern eine sprachspezifische Analyse des Dispatch-Mechanismus selbst.

Die Tools, zu denen Entwickler zuerst greifen, und wo sie aufhören.

Es gibt eine vorhersehbare Abfolge von Tools, die Entwickler bei der Anruferermittlung einsetzen, und einen ebenso vorhersehbaren Punkt, an dem jedes Tool keine zuverlässigen Ergebnisse mehr liefert. Das Verständnis dieser Abfolge ist wichtig, da die Ausgabe jedes Tools vollständig erscheint, selbst wenn sie es nicht ist.

IDE-Aufrufhierarchie: Zuverlässig innerhalb eines Projekts

Die Aufrufhierarchie-Funktionen der IDEs sind der naheliegendste erste Schritt. IntelliJ IDEA, Visual Studio, VS Code und Eclipse bieten alle eine Funktion zum Suchen aller Aufrufer oder zum Anzeigen der Aufrufhierarchie, die die Aufrufer einer ausgewählten Funktion rekursiv innerhalb des indizierten Bereichs des aktuellen Projekts oder Arbeitsbereichs auflistet. Für eine Funktion, die ausschließlich in einem Repository und einer Sprache verwendet wird, sind diese Funktionen präzise und ausreichend.

Die Einschränkung ist in der Definition explizit festgelegt: „innerhalb des indizierten Bereichs“. Aufrufer in anderen Repositories, in anderen Laufzeitumgebungen oder in Diensten, die über einen Paketmanager und nicht durch direkte Projektreferenzen auf diesen Code zugreifen, liegen außerhalb des Bereichs. Die IDE zeigt nicht an, was nicht durchsucht wurde. Der Entwickler erhält ein Ergebnis-Set und hat keinen Einblick in die Anzahl der nicht indizierten Repositories, die Anzahl derer, die die veraltete Funktion verwenden, oder ob das Ergebnis „keine Aufrufer“ tatsächlich keine Aufrufer bedeutet oder lediglich „keine Aufrufer innerhalb des Teils des Systems, den dieses Tool erfassen kann“.

grep und Textsuche: Umfassend, aber strukturell blind

Wenn die Suche in der IDE als unvollständig vermutet wird, ist der nächste Schritt üblicherweise die Textsuche: Man durchsucht die verfügbaren Quellverzeichnisse mit grep oder nutzt die Plattformsuche über GitHub oder GitLab. Dadurch wird der Suchbereich erheblich erweitert und Aufrufer in anderen Repositories gefunden, sofern diese zugänglich sind. Das strukturelle Problem besteht darin, dass die Textsuche Zeichenketten, nicht aber Funktionsaufrufe findet. Sie liefert jedes Vorkommen des Funktionsnamens, einschließlich Kommentaren, die die Funktion erwähnen, Dokumentationszeichenketten, Logmeldungen, die die Funktion zu Debugging-Zwecken nennen, und Zeichenkettenliterale, die den Funktionsnamen enthalten, ihn aber nicht aufrufen. Außerdem werden Aufrufer übersehen, bei denen der Funktionsname von der Suchzeichenkette abweicht: Aufrufer durch Aliase, durch teilweise übereinstimmende Namen in COBOL, wo Namen abgekürzt sein können, oder durch dynamische Aufrufe, bei denen der Name zur Laufzeit zusammengesetzt wird.

Die Ergebnisse der Textsuche müssen manuell gefiltert werden, um festzustellen, welche Einträge tatsächliche Anruferadressen, welche Dokumentationsverweise und welche Fehlalarme aufgrund von Zeichenkettenkonflikten sind. In großen Systemen ist diese Filterung selbst sehr aufwendig, und der Filterprozess kann die Vollständigkeit nicht gewährleisten: Wurde ein Anrufer aufgrund eines abweichenden Namens übersehen, enthält das gefilterte Ergebnis keinen Hinweis darauf.

Compilerwarnungen und @Deprecated Anmerkungen

Moderne Programmiersprachen und Toolchains bieten Mechanismen zur Kennzeichnung veralteter Funktionen, die Warnungen ausgeben, wenn diese aufgerufen werden. Javas @Deprecated Annotation kombiniert mit -Xlint:deprecation erzeugt Kompilierzeitwarnungen an Aufrufstellen. C# [Obsolete] Das Attribut erzeugt Warnungen zur Kompilierzeit. Die Go-Konvention, veraltete Funktionen zu benennen und in godoc zu dokumentieren, erzeugt keine automatischen Warnungen. Diese Mechanismen sind zwar wertvoll, aber insofern eingeschränkt, als sie nur für Aufrufer funktionieren, die gegen dieselbe Codebasis kompilieren, in der die Veraltung annotiert ist.

Ein Aufrufer, der eine ältere Version der Bibliothek verwendet, die vor der @Deprecated Eine Annotation führt zu keiner Warnung. Ein Aufrufer, der ein Binärartefakt verwendet, anstatt aus dem Quellcode zu kompilieren, erhält keine Warnung. Ein Aufrufer in einer anderen Sprache, der über eine sprachübergreifende Schnittstelle aufruft, erhält ebenfalls keine Warnung. Entscheidend ist jedoch, dass die während der Kompilierung erzeugten Warnungen nur für den Compiler selbst gelten: Sie warnen vor den von ihm erkannten Aufrufen, nicht vor Aufrufen in anderen, separat kompilierten Repositories. Die alleinige Verwendung von Compilerwarnungen zur Aufrufererkennung in einem Multi-Service-System führt dazu, dass alle Aufrufer übersehen werden, die unabhängig kompilieren – was in Microservice-Architekturen der Normalfall ist.

Werkzeuge zur statischen Analyse: Besser, aber in ihrem Anwendungsbereich begrenzt

Speziell entwickelte statische Analysetools bieten eine präzisere Aufrufer-Enumeration als IDEs für die jeweilige Zielsprache und können, sofern sie für die Indizierung mehrerer Codebasen konfiguriert sind, oft Repository-Grenzen überschreiten. Sie erstellen korrekte Aufrufgraphen anstatt sich auf Textvergleiche zu verlassen, verarbeiten Aliase und indirekte Aufrufe besser als die IDE-Suche und können in CI-Pipelines ausgeführt werden, um neu hinzugefügte Aufrufer zu erfassen. Sie stellen den leistungsfähigsten verfügbaren Ansatz für eine einzelne Sprache dar.

Die Einschränkung entspricht derjenigen von IDEs, betrifft aber nun die Werkzeugebene: Ein Java-Tool zur statischen Codeanalyse indiziert keine COBOL-Programme, ein COBOL-Analysator keine Java-Dienste und auch keine JCL-Jobstreams. In einem System, in dem die veraltete Funktion ein COBOL-Hilfsprogramm ist, das von COBOL-Programmen aufgerufen wird, welche wiederum von JCL-Jobs gestartet werden und deren Datenausgabe von Java-Diensten genutzt wird, sieht jedes Tool zur statischen Codeanalyse nur einen Teil der Aufrufbeziehung. Wie im Kontext grundlegender Refactoring-Techniken betrachtet , erfordert die Identifizierung aller Ausführungspfade zu einem bestimmten Codeabschnitt, einschließlich seltener Fehlerzustände und Ausweichzweige, eine vollständige Zuordnung des Aufrufgraphen, die sprachübergreifend mit Tools für einzelne Sprachen nicht erstellt werden kann.

Was eine vollständige Anruferliste tatsächlich erfordert

Eine vollständige Aufrufliste für eine veraltete Funktion in einem Unternehmenssystem ist kein Suchergebnis. Es handelt sich um eine strukturierte Aufzählung aller Ausführungspfade, über die die veraltete Funktion erreicht werden kann, einschließlich direkter, indirekter, sprachübergreifender und dynamisch aufgerufener Pfade. Die Erstellung dieser Aufzählung erfordert mehrere Funktionen, die kein einzelnes Standardtool bietet.

Ein einheitlicher, sprachübergreifender Aufrufgraph. Dieser Aufrufgraph muss alle Sprachen des Systems umfassen. Ein Aufruf einer JCL-Prozedur an ein COBOL-Programm, ein Aufruf eines gemeinsam genutzten Hilfsprogramms und ein Aufruf desselben COBOL-Programms durch einen Java-Dienst über eine Middleware-Schnittstelle müssen jeweils Knoten und Kanten im selben Graphen darstellen. Die veraltete Funktion ist ein Knoten in diesem Graphen, und die Aufrufer-Enumeration durchläuft alle eingehenden Kanten, sowohl direkte als auch transitive, unabhängig von ihrer Ursprungssprache.

Rekursive Traversierung des gesamten Aufrufgraphen. Direkte Aufrufer bilden nur die erste Ebene. Eine vollständige Erfassung erfordert die Verfolgung des Aufrufgraphen stromaufwärts über indirekte Aufrufer, Wrapper-Funktionen und Fassadenschichten, bis die Traversierung Funktionen erreicht, die selbst keine Aufrufer haben – die eigentlichen Einstiegspunkte der Aufrufketten. Jede Funktion auf einem Pfad, der bei der veralteten Funktion endet, ist in diesem Sinne ein Aufrufer: Das Entfernen der veralteten Funktion führt dazu, dass jeder Pfad, der über sie verläuft, ungültig wird.

Repositoryübergreifende Indizierung. Der Aufrufgraph muss Code aus jedem Repository enthalten, das die Funktion potenziell aufrufen könnte, einschließlich Repositories, die über eine gemeinsam genutzte Bibliothek oder ein Paket von der Funktion abhängen. Dies erfordert die gleichzeitige Indizierung aller Repositories und die Auflösung von Repository-übergreifenden Importbeziehungen, um Aufrufe in einem Repository mit Definitionen in einem anderen zu verknüpfen.

Erkennung indirekter Aufrufmuster. Die Analyse muss Aufrufe identifizieren, die über Reflektion, dynamische Dispatch, Funktionszeiger, Delegaten und zeichenkettenbasierte Aufrufe in Konfigurationsdateien erfolgen. Dies erfordert eine musterbasierte Erkennung anstelle einer direkten Auflösung der Aufrufgrenzen: Die Mechanismen der dynamischen Dispatch im Code müssen gefunden und ermittelt werden, welche Funktionen unter welchen Bedingungen aufgerufen werden können.

Unterscheidung zwischen aktiven Aufrufern und Aufrufern, die nur in Tests verwendet werden oder nicht mehr benötigt werden. Nicht alle Aufrufer erfordern dieselbe Reaktion. Ein Aufrufer, der nur in einer Testumgebung für die veraltete Funktion selbst existiert, muss im Rahmen der Bereinigung entfernt und nicht migriert werden. Ein Aufrufer in Code, der selbst durch Nutzungsanalyse als toter Code identifiziert wurde, ist kein Hindernis für die Funktionsentfernung. Um diese Unterscheidungen zu verstehen, muss die Aufruferliste mit Informationen darüber kombiniert werden, welche Codepfade tatsächlich aktiv sind. Wie in der Untersuchung der Erkennung von totem Code durch statische Analyse detailliert beschrieben , können unerreichbarer Code und ungenutzte Funktionen aufgrund unvollständiger Dokumentation oder Unsicherheit über historische Abhängigkeiten jahrelang in unternehmenskritischen Systemen bestehen bleiben. Daher muss die Aufruferliste für eine veraltete Funktion zwischen aktiven und nicht mehr benötigten Aufrufern unterscheiden.

Der Prozess von der Abschaffung bis zur Entfernung: Ein strukturierter Ansatz

Die Entfernung veralteter Funktionen als einmaliges Ereignis statt als strukturierter Prozess zu behandeln, ist die häufigste Ursache für Fehler bei der Aufrufererkennung. Der korrekte Ansatz sieht die Entfernung als letzten Schritt eines mehrphasigen Prozesses, der lange vor der eigentlichen Codelöschung beginnt.

Phase 1: Markieren und Messen

Der erste Schritt besteht darin, die Funktion mithilfe des in die Sprache integrierten Mechanismus als veraltet zu kennzeichnen (@Deprecated in Java, [Obsolete] in C#, #[deprecated] in Rust (oder einer entsprechenden Äquivalentsprache) und Ermittlung einer Basisanzahl von Aufrufern. Diese Basisanzahl basiert nicht auf einer einzelnen Suche, sondern auf der Indizierung aller bekannten Codebasen, die die Funktion aufrufen könnten, und der anschließenden Zählung der Ergebnisse. Die Basisanzahl dient zwei Zwecken: Sie quantifiziert den Umfang der Migration und bietet einen Referenzwert, anhand dessen der Fortschritt während der Migration der Aufrufer gemessen werden kann.

Die Basislinie sollte nach Anrufertyp und Standort organisiert sein:

AnruferkategorieZu ZählenPrioritätEigentümer
Direkte Aufrufer im selben RepositoryNHochAktuelles Team
Direkte Anrufer in abhängigen DienstenNHochDienstleistungsinhaber
Anrufer über Wrapper-FunktionenNMediumBesitzer von Verpackungen
Aufrufer im generierten oder Framework-CodeNMediumFramework-Team
Aufrufer im TestcodeNNiedrigAktuelles Team
Anrufer im CodeNNur AufräumarbeitenAktuelles Team

Phase 2: Benachrichtigen und Migrieren

Mit einer vollständigen Anruferliste wird die Migration zu einem organisierten und nicht zu einem reaktiven Prozess. Jeder Anruferinhaber wird über die genauen Aufrufpositionen benachrichtigt: nicht „Sie rufen möglicherweise diese Funktion auf“, sondern „Sie rufen diese Funktion in Zeile 247 auf“. BillingService.java, Zeile 82 von AccountProcessor.javaund im Integrationstest in Zeile 14 von BillingServiceTest.java„Diese Detailgenauigkeit wird durch das Aufruferinventar ermöglicht und kann durch allgemeine Veraltungswarnungen nicht erreicht werden.“

Die Bereitstellung eines Migrationspfads zusammen mit der Benachrichtigung ist unerlässlich. Die Abschaffungsmitteilung sollte die Dokumentation der Ersatzfunktion, eine Beschreibung etwaiger Verhaltensunterschiede zwischen der alten und der neuen Implementierung sowie – bei komplexeren Änderungen – ein Codebeispiel, das den Zustand vor und nach der Änderung veranschaulicht, enthalten. Für Aufrufer in den Codebasen anderer Teams sollte der Zeitplan für die Migration explizit verhandelt und nicht einseitig angekündigt werden, da diese Teams ihre eigenen Prioritäten und Lieferverpflichtungen haben. Wie im Kontext der Datenbank-Refaktorisierung in abhängigen Systemen gezeigt wurde , verhindert die schrittweise Einführung der neuen Struktur vor der Abschaffung der alten Struktur, dass inkompatible Änderungen unerwartet auftreten.

Phase 3: Anruferanzahl überwachen

Zwischen der Basismessung und dem geplanten Entfernungstermin sollte die Anzahl der Aufrufer kontinuierlich überwacht werden. Jedes Mal, wenn ein Aufrufer zur Ersatzfunktion migriert wird, verringert sich die Anzahl. Die Entfernung erfolgt, sobald die Anzahl aktiver Aufrufer null erreicht (Aufrufer, die nur zu Testzwecken verwendet werden oder nicht mehr benötigt werden, können gleichzeitig mit der Funktion selbst entfernt werden). Die kontinuierliche Überwachung erfordert die regelmäßige Ausführung der Aufrufer-Enumeration als Teil der CI-Pipeline und nicht die Verwendung einer einmaligen Bestandsaufnahme, die mit Codeänderungen veraltet.

Die Überwachung erfasst auch neue Aufrufer, die während der Deprecation-Phase hinzugefügt werden. In großen Organisationen kommt es häufig vor, dass während des Migrationszeitraums neuer Code geschrieben wird, der eine veraltete Funktion aufruft. Dies kann daran liegen, dass der Entwickler die Veraltung nicht kannte, dass sie bei einer Code-Review übersehen wurde oder dass ein automatisierter Codegenerator Code erzeugt, der die veraltete Funktion aufruft. Die CI-basierte Aufrufererkennung für die veraltete Funktion, die so konfiguriert ist, dass sie bei neuen Aufrufstellen fehlschlägt, verhindert, dass die Anzahl der Aufrufer während der Migration ansteigt.

Phase 4: Vollständigkeit vor der Entfernung prüfen

Unmittelbar vor dem Entfernen der Funktion sollte die Aufrufer-Enumeration ein letztes Mal für alle bekannten Codebasen durchgeführt werden. Diese abschließende Prüfung dient als Sicherheitsmaßnahme: Sie bestätigt, dass die Anzahl der aktiven Aufrufer null erreicht hat und identifiziert alle später hinzugefügten Funktionen, die von der CI-Überwachung nicht erfasst wurden. Die Bestandsaufnahme sollte an dieser Stelle auch das Fehlen dynamischer Aufrufer überprüfen: Konfigurationsdateien, die die Funktion per Zeichenkette referenzieren, reflexionsbasierte Registrierungen und alle anderen indirekten Aufrufmechanismen, die während der ersten Analyse identifiziert wurden.

Die Überprüfung sollte sich auf den Abhängigkeitsgraphen aller gemeinsam genutzten Bibliotheken oder Pakete erstrecken, die die veraltete Funktion bereitstellen. Ist die Funktion Teil einer öffentlichen API, die von externen Parteien genutzt wird, muss der Zeitplan für die Entfernung auch externe Nutzer berücksichtigen, die möglicherweise nicht durch interne Codeanalyse erreichbar sind. Bei internen Systemen umfasst die Überprüfung jede indizierte Codebasis. Bei öffentlich veröffentlichten APIs umfasst die Überprüfung die bekannten Nutzer sowie einen definierten Zeitraum, in dem externe Nutzer migrieren müssen.

Wie die Anrufererkennung in Legacy- und Mainframe-Umgebungen unterschiedlich funktioniert

Die oben beschriebenen Herausforderungen gelten für jedes große Softwaresystem, sind aber in Mainframe- und Legacy-Umgebungen besonders akut, da die Aufrufbeziehungen in diesen Umgebungen durch Mechanismen ausgedrückt werden, für deren Analyse moderne Aufrufer-Erkennungswerkzeuge nicht konzipiert wurden.

In COBOL-Umgebungen werden Funktionen über CALL-Anweisungen aufgerufen, die das Ziel entweder über eine Zeichenkette, ein Datenelement mit dem Programmnamen oder einen Prozedurzeiger referenzieren können. Der Fall der Zeichenkette lässt sich statisch analysieren; im Fall des Datenelements ist eine Datenflussanalyse erforderlich, um dessen Wert zum Zeitpunkt des Aufrufs zu bestimmen; und im Fall des Prozedurzeigers muss die Zuweisung des Zeigers nachverfolgt werden. Jeder dieser Aufrufmechanismen erscheint im Quellcode unterschiedlich und erfordert eine andere Analyse zur Auflösung.

In JCL-Umgebungen werden Programme über ihren Namen in EXEC PGM= Anweisungen aufgerufen. Der Programmname ist eine Zeichenkette, die einem kompilierten Modul in einer Ladebibliothek zugeordnet ist. Um die Aufrufer eines COBOL-Programms über JCL zu verfolgen, muss die JCL analysiert werden, um die Programmnamen zu extrahieren, diese Namen den kompilierten COBOL-Programmen zuzuordnen, die sie implementieren, und zu ermitteln, welche COBOL-Abschnitte innerhalb dieser Programme die veraltete Hilfsfunktion aufrufen. Diese mehrstufige Analyse liegt vollständig außerhalb des Anwendungsbereichs eines isoliert arbeitenden COBOL- oder JCL-Analysators.

Gemeinsam genutzte Copybooks sind in COBOL-Umgebungen besonders wichtig. Ein veralteter Absatz, der in einem Copybook definiert ist, kann über COPY-Anweisungen in vielen Programmen eingebunden werden. Der Absatz wird nicht physisch in jedem Programm dupliziert, sondern zur Kompilierzeit eingebunden. Eine Analyse, die die Vorkommen des Absatznamens in Quelldateien zählt, ohne die Einbindung in Copybooks zu berücksichtigen, führt sowohl zu einer Überzählung (da die Absatzdefinition im Copybook selbst gefunden wird) als auch zu einer Unterzählung (da die Tatsache außer Acht gelassen wird, dass jedes Programm, das das Copybook einbindet, Zugriff auf den Absatz hat). Die korrekte Aufrufererkennung erfordert das Verständnis, welche Programme welche Copybooks einbinden und welche Absätze innerhalb dieser Copybooks sie tatsächlich aufrufen. Die Beziehung zwischen fest codierten Referenzen und ihren nachgelagerten Nutzern verdeutlicht, warum die Auflösung dieser Aufrufbeziehungen auf Programmebene vor jeder strukturellen Änderung unerlässlich ist: Was wie eine einfache Zeichenkettenreferenz aussieht, kann der einzige Mechanismus sein, über den Dutzende von Programmen kritische Funktionen erreichen.

Wie SMART TS XL Erstellt das vollständige Anruferverzeichnis

SMART TS XL Es wird ein einheitlicher Aufrufgraph für alle Sprachen, Plattformen und Repositories der indizierten Umgebung erstellt. COBOL-Programme, JCL-Jobstreams, Java-Dienste, .NET-Anwendungen, SQL-Prozeduren, Python-Skripte und andere Quellcode-Artefakte werden mithilfe sprachspezifischer Analysen in einen gemeinsamen Querverweisgraphen umgewandelt. Jede Funktion, jeder Absatz, jede Prozedur, jede Methode und jede Programmeinheit bildet einen Knoten in diesem Graphen. Jede Aufrufbeziehung – sei es eine COBOL-CALL-Anweisung, ein Java-Methodenaufruf, ein JCL-EXEC-PGM oder ein SQL-EXEC – ist eine typisierte Kante. Der Graph repräsentiert die vollständige Aufruftopologie des Systems und nicht nur eine sprachspezifische Teilansicht.

Wenn eine Funktion zur Entfernung markiert wird, SMART TS XLDie Aufrufer-Enumeration durchläuft den Aufrufgraphen ausgehend vom Zielfunktionsknoten und erfasst jeden Aufrufer auf jeder Ebene der Aufrufhierarchie. Die Durchlaufung erfolgt rekursiv und folgt dem Graphen über Wrapper-Funktionen, Fassadenschichten und Hilfsfunktionen, bis sie Funktionen ohne Aufrufer erreicht, welche die eigentlichen Einstiegspunkte der Aufrufketten darstellen. Die Ergebnisse werden nach Sprache, Repository, Aufrufertyp und Aufruftiefe geordnet und bieten dem Team so ein strukturiertes Verzeichnis, das direkte von indirekten und aktive von Totcode-Aufrufern unterscheidet.

Die Auswirkungsanalyse der Plattform erweitert dies zu einem strukturierten Änderungsfolgenbericht: Er listet nicht nur auf, welche Funktionen die veraltete Funktion aufrufen, sondern auch, welche Programme, Dienste, Batch-Jobs und JCL-Prozeduren auf jeder Ebene der Abhängigkeitskette betroffen sind. Dieser Bericht ist das Artefakt, das den Prozess von der Kennzeichnung als veraltet bis zur Entfernung konkretisiert: Er benennt die Verantwortlichen, identifiziert die spezifischen Aufrufstellen und quantifiziert den Umfang der Migration, die vor einer sicheren Entfernung erforderlich ist. Wie die detaillierte Auswirkungsanalyse für das Änderungsmanagement in Unternehmen zeigt , ist die Fähigkeit, betroffene Komponenten vor einer strukturellen Änderung aufzulisten, die grundlegende Voraussetzung für den sicheren Betrieb komplexer, vernetzter Unternehmenssysteme.

SMART TS XL Unterstützt auch die laufende Überwachungsphase des Deprecation-Prozesses. Da der Querverweisgraph kontinuierlich aktualisiert wird, sobald Quellcodeänderungen indiziert werden, ist die Aufrufanzahl einer veralteten Funktion stets aktuell. Die CI-Pipeline-Integration ermöglicht es, dass automatisierte Prüfungen bei neuen Aufrufen veralteter Funktionen fehlschlagen. So wird die Migrationsdisziplin direkt bei der Einführung neuen Codes durchgesetzt, anstatt Verstöße erst im Nachhinein zu entdecken. Diese Kombination aus anfänglicher Enumeration, Migrationsanleitung und kontinuierlicher Überwachung deckt den gesamten Lebenszyklus einer veralteten Funktion von der Annotation bis zur sicheren Entfernung ab.

Funktionsentfernung ohne Reue

Der Unterschied zwischen einer reibungslos verlaufenden Funktionsentfernung und einer, die zu Produktionsfehlern führt, liegt fast immer in der Vollständigkeit der Aufrufererkennung. Die Entfernung selbst ist trivial: Definition löschen und bereitstellen. Die eigentliche Arbeit liegt in der Vorbereitung, und diese ist nur so gut wie die zugrunde liegende Aufruferliste.

In Systemen mit flachem, einsprachigem Aufrufgraphen, der auf ein einzelnes Repository beschränkt ist, reichen die Aufrufhierarchie der IDE und Compilerwarnungen zur Vorbereitung aus. In Systemen, deren Aufrufgraphen mehrere Sprachen, Repositories, Plattformen und möglicherweise mehrere Jahrzehnte Code umfassen, decken diese Tools nur einen kleinen und unbekannten Bruchteil der tatsächlichen Aufruferoberfläche ab. Die Diskrepanz zwischen ihren Ergebnissen und dem tatsächlichen Aufruf der Funktion ist die Ursache für Produktionsfehler.

Eine speziell entwickelte, sprach- und repositoryübergreifende Aufrufer-Enumeration ist keine Verfeinerung des Entwickler-Workflows zur Funktionsentfernung. Sie ist vielmehr eine Grundvoraussetzung für die sichere Ausführung dieses Workflows in jedem System, das komplex genug ist, um die Art von systemübergreifenden Aufrufbeziehungen anzusammeln, die veraltete Funktionen in Unternehmenscodebasen üblicherweise aufweisen. Jede veraltete Funktion, die ohne vollständige Aufrufer-Inventarisierung entfernt wird, ist eine Release, die eine unbekannte Anzahl von Laufzeitfehlern enthält, da der spezifische Ausführungspfad, der die fehlende Definition erreicht, nicht gefunden werden kann. Genau diese Unbekannte zu beseitigen, ist der Zweck der strukturierten Aufrufer-Erkennung.