Trova tutti i chiamanti di una funzione obsoleta

Come individuare tutti i chiamanti di una funzione obsoleta prima di rimuoverla

Rimuovere una funzione obsoleta da una codebase è concettualmente una delle operazioni più semplici che uno sviluppatore possa eseguire. Eliminare la definizione, verificare che nessun altro la utilizzi, effettuare il commit. In pratica, per qualsiasi funzione che esista da abbastanza tempo da essere considerata obsoleta, il passaggio "verificare che nessun altro la utilizzi" è il punto in cui il processo si blocca. La funzione potrebbe essere chiamata da codice scritto anni fa da qualcuno che non fa più parte del team, in un repository che riceve modifiche di rado, da un linguaggio o framework non gestito dal team attuale. Potrebbe essere invocata indirettamente tramite un wrapper, tramite reflection o tramite un meccanismo di dispatch a runtime che non compare in alcun grafico statico delle chiamate. Potrebbe essere referenziata nel codice generato, nello scaffolding dei test o in un file di configurazione che la attiva per nome. Lo sviluppatore che la contrassegna come obsoleta e quello che alla fine la rimuove potrebbero non avere modo di sapere nulla di tutto ciò senza uno strumento in grado di creare un inventario completo dei chiamanti tra i repository.

Trova ogni chiamante prima di rimuovere qualsiasi cosa

SMART TS XL Crea un grafico delle chiamate tra linguaggi diversi che identifica ogni chiamante di qualsiasi funzione prima che tu apporti una modifica.

Clicca qui

Il costo di commettere questo errore è immediato e concreto. Una funzione rimossa con una scoperta incompleta del chiamante causa errori di runtime nei sistemi che ancora dipendono da essa. In un monolite con una singola distribuzione, la superficie di errore è limitata. In un sistema distribuito con più servizi, ciascuno distribuito indipendentemente, gli errori si propagano a cascata: il servizio che forniva la funzione viene aggiornato, i consumatori no, e il problema si manifesta in fase di runtime in produzione in sistemi che possono essere di proprietà di team diversi. In un ambiente mainframe in cui i programmi COBOL chiamano paragrafi di utilità condivisi per nome, l'errore potrebbe non manifestarsi fino all'esecuzione di uno specifico job batch, che potrebbe essere settimanale o mensile, rendendo il riferimento mancante invisibile durante i normali cicli di test. Come esaminato nel contesto più ampio di gestione del codice deprecatoNel tempo, i rischi si accumulano: il codice obsoleto rimosso solo parzialmente è più pericoloso del codice obsoleto lasciato in loco, perché la rimozione crea l'illusione di completezza mentre i chiamanti rimanenti continuano a operare su una definizione che non esiste più.

Questo articolo è una guida pratica all'individuazione dei chiamanti prima della rimozione delle funzioni: cosa richiede un inventario completo dei chiamanti, perché gli strumenti a cui gli sviluppatori ricorrono inizialmente sono strutturalmente insufficienti, come diversi tipi di relazioni di chiamata richiedono diversi approcci di analisi e come si presenta una vera e propria enumerazione dei chiamanti tra sistemi diversi in codebase di livello enterprise che mescolano linguaggi, piattaforme e repository.

Perché l'identificazione del chiamante è più difficile di quanto sembri

La versione più superficiale dell'individuazione dei chiamanti è familiare a ogni sviluppatore: si fa clic con il pulsante destro del mouse sul nome di una funzione in un IDE, si seleziona "Trova tutti i riferimenti" o "Mostra gerarchia delle chiamate" e si esaminano i risultati. Questo funziona in modo affidabile nell'ambito di un singolo progetto caricato in una singola istanza dell'IDE. Nel momento in cui il codice si estende oltre tale ambito, i risultati diventano incompleti in modi non visibili nell'output. L'IDE non indica quali chiamanti non ha trovato perché non ha indicizzato i repository che li contengono. Lo sviluppatore vede un set di risultati che sembra completo e procede di conseguenza.

Questo è il problema strutturale della scoperta dei chiamanti su larga scala: gli strumenti che gli sviluppatori usano più fluentemente sono limitati dal loro ambito di indicizzazione e, nei sistemi distribuiti, multilingue e di grandi dimensioni, tale ambito copre solo una frazione dei punti in cui una determinata funzione potrebbe essere chiamata. La fiducia dello sviluppatore nella completezza dei risultati è inversamente proporzionale alla completezza effettiva della ricerca. In una codebase piccola e monolingue, la gerarchia delle chiamate dell'IDE è effettivamente affidabile. In un sistema aziendale che si estende su più repository, linguaggi e ambienti di distribuzione, è sistematicamente fuorviante. Come analizzato nel contesto di Entropia del codice e rischio di refactoringI moduli legacy possono dipendere da interfacce obsolete, mentre i servizi più recenti continuano a richiamare routine originariamente progettate per ambienti precedenti, e queste relazioni di chiamata tra sistemi diversi sono proprio quelle che la ricerca limitata all'IDE non riesce a individuare.

Per comprendere le ragioni specifiche per cui il rilevamento del chiamante non riesce, è necessario esaminare ogni tipologia di chiamata principale: chiamate dirette, chiamate indirette, chiamate in lingue diverse e instradamento dinamico. Ciascuna tipologia fallisce per motivi diversi e richiede tecniche di analisi differenti per essere risolta correttamente.

Chiamate dirette attraverso i confini del repository

Le chiamate dirette sono il tipo di chiamata più semplice: una funzione che ne chiama esplicitamente un'altra per nome. All'interno di un singolo repository, gli IDE le gestiscono in modo affidabile. Al di là dei confini dei repository, l'analisi fallisce perché l'indicizzazione dell'IDE non si estende oltre il confine. Se il repository A definisce una funzione di utilità condivisa e i repository B, C e D la importano e la chiamano, l'IDE di ciascuno di questi repository vedrà solo le chiamate all'interno del proprio ambito indicizzato.

Questo schema di chiamata multi-repository è la norma piuttosto che l'eccezione nelle architetture a microservizi, dove le librerie condivise vengono pubblicate come pacchetti e utilizzate da decine di servizi. Il manutentore della libreria che dichiara obsoleta una funzione nel pacchetto condiviso deve sapere quali servizi che la utilizzano la chiamano ancora. Il suo IDE non sa nulla dei consumatori. Il gestore dei pacchetti sa quali servizi dipendono dal pacchetto, ma non quale specifica funzione all'interno del pacchetto ciascun servizio chiama. Mappare da "questo servizio utilizza la versione X di questo pacchetto" a "questo servizio chiama questa specifica funzione obsoleta" richiede l'indicizzazione del codice sorgente di ogni consumatore e la risoluzione della chiamata alla specifica definizione della funzione.

Chiamate indirette: involucri, delegati e facciate

Una funzione potrebbe non essere chiamata direttamente dai suoi utilizzatori. Potrebbe essere chiamata tramite una funzione wrapper che fornisce funzionalità aggiuntive di registrazione, gestione degli errori o trasformazione dei parametri. Potrebbe essere assegnata a un delegato o a un puntatore a funzione e invocata tramite il delegato. Potrebbe essere registrata in un registro di servizi o in un framework di plugin e chiamata per nome tramite un meccanismo di dispatch. In ognuno di questi casi, una ricerca diretta delle chiamate alla funzione deprecata restituisce un risultato incompleto, perché i chiamanti effettivi chiamano il wrapper o il dispatcher, non la funzione deprecata stessa.

L'invocazione mediata da wrapper è particolarmente comune nelle codebase di grandi dimensioni, dove problematiche trasversali come la registrazione dei log, l'autorizzazione e la logica di ripetizione delle chiamate sono stratificate attorno alle funzioni principali tramite pattern wrapper. Una funzione deprecata, incapsulata da un'utility di logging, viene di fatto chiamata da ogni chiamante del wrapper, non da qualsiasi parte del codice che contenga il nome della funzione deprecata. Identificare questi chiamanti richiede di tracciare il wrapper: l'utility di logging chiama la funzione deprecata e, di conseguenza, ogni chiamante dell'utility di logging è un chiamante indiretto della funzione deprecata. Questa traversata ricorsiva del grafo delle chiamate è ciò che distingue un inventario completo dei chiamanti da una ricerca superficiale dei riferimenti.

Consideriamo un esempio rappresentativo in Java in cui un metodo obsoleto viene acceduto tramite un livello di delega:

Giava

// 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
}

Una ricerca “Trova tutti i riferimenti” per calculateLegacyFee problemi computeFee come unico chiamante. Non restituisce processMonthlyBilling, che è il vero consumatore del comportamento deprecato. Un inventario completo del chiamante richiede di attraversare il grafico delle chiamate a monte attraverso computeFee per identificare ogni percorso che in definitiva invoca il metodo obsoleto.

Invocazioni interlinguistiche

Le chiamate tra linguaggi diversi rappresentano la categoria in cui gli strumenti standard di individuazione del chiamante falliscono più clamorosamente. Quando un servizio Java invoca un programma COBOL per nome attraverso un livello middleware, quando uno script Python chiama una stored procedure che incapsula una funzione obsoleta, o quando un job JCL invoca un programma tramite il suo PROGNAME che internamente chiama un paragrafo obsoleto, nessuna di queste relazioni appare nel grafo delle chiamate di un singolo linguaggio. Gli strumenti di ciascun linguaggio vedono solo il proprio lato della chiamata.

Negli ambienti mainframe, le chiamate tra linguaggi diversi sono strutturali e pervasive. Un flusso di job JCL specifica il nome del programma COBOL che esegue. Il programma COBOL chiama paragrafi e sottoprogrammi per nome. I paragrafi di utilità definiti nelle librerie di copia sono condivisi tra molti programmi. Quando un paragrafo in una libreria di copia viene deprecato, trovare tutti i chiamanti richiede la comprensione delle relazioni di chiamata COBOL (quali programmi includono la libreria di copia e quali chiamano il paragrafo), delle relazioni di invocazione JCL (quali job invocano quei programmi) e di eventuali interfacce tra linguaggi diversi (Java o SQL che interagiscono con quei programmi). Nessuno strumento singolo copre tutte queste relazioni. Come esaminato nell'analisi di analisi statica sui sistemi legacyGli strumenti di analisi statica progettati per gli ambienti moderni non sono in grado di visualizzare il quadro completo di come i programmi legacy vengono attivati, richiamati e interconnessi quando le relazioni di chiamata si estendono simultaneamente a JCL, COBOL e interfacce tra sistemi diversi.

Dispacciamento dinamico e riflessione

Alcuni chiamanti invocano una funzione non tramite il suo nome letterale nel codice sorgente ma attraverso un meccanismo che risolve la funzione in fase di esecuzione: reflection in Java o .NET, getattr/__call__ in Python, collegamento ritardato in COBOL tramite CALL identifier, dispatch dinamico tramite polimorfismo o invocazione tramite stringa in framework di plugin e sistemi basati sulla configurazione. Questi chiamanti non contengono il nome della funzione deprecata in alcuna forma che l'analisi statica possa rilevare in modo affidabile.

Un file di configurazione che specifica un nome di funzione come stringa, caricato in fase di esecuzione e utilizzato per richiamare la funzione tramite reflection, è un chiamante che non appare in nessuna analisi del codice sorgente. Un framework di plugin che scopre e richiama gestori registrati tramite interfaccia è un chiamante che appare nel grafico delle chiamate solo come una chiamata al meccanismo di dispatch, non come una chiamata a un gestore specifico. L'identificazione di questi chiamanti richiede una combinazione di analisi statica per trovare modelli di dispatch dinamici, tracciamento in fase di esecuzione per osservare le invocazioni effettive e ispezione manuale della logica di configurazione e registrazione che determina a quali funzioni vengono indirizzate le chiamate. Come discusso nell'esame di analisi statica su codice offuscato e generatoQuando i percorsi di esecuzione non sono espressi direttamente nel codice sorgente, l'analisi statica deve ricostruire i percorsi probabili a partire da modelli strutturali piuttosto che da riferimenti testuali diretti, e tali ricostruzioni richiedono un'analisi del meccanismo di dispatch stesso che tenga conto del linguaggio.

Gli strumenti che gli sviluppatori utilizzano per primi e dove si fermano

Esiste una sequenza prevedibile di strumenti che gli sviluppatori utilizzano quando tentano di identificare il chiamante, e un punto altrettanto prevedibile in cui ciascuno di essi smette di fornire risultati affidabili. Comprendere questa sequenza è importante perché l'output di ogni strumento appare completo anche quando non lo è.

Gerarchia delle chiamate IDE: affidabile all'interno di un singolo progetto

Le funzionalità di gerarchia delle chiamate degli IDE rappresentano il primo passo più naturale. IntelliJ IDEA, Visual Studio, VS Code ed Eclipse offrono tutti una qualche forma di "trova tutti i chiamanti" o "mostra gerarchia delle chiamate" che enumera ricorsivamente i chiamanti di una funzione selezionata, all'interno dell'ambito indicizzato del progetto o dell'area di lavoro corrente. Per una funzione utilizzata esclusivamente all'interno di un repository e di un linguaggio, queste funzionalità sono accurate e sufficienti.

La limitazione è esplicitata nella definizione dell'ambito: "all'interno dell'ambito indicizzato". I chiamanti in altri repository, in runtime di altri linguaggi o in servizi che dipendono da questo codice tramite un gestore di pacchetti anziché tramite un riferimento diretto al progetto sono al di fuori dell'ambito. L'IDE non indica cosa non ha cercato. Lo sviluppatore riceve un set di risultati e non ha visibilità su quanti repository aggiuntivi esistono che non sono stati indicizzati, quanti di questi utilizzano la funzione deprecata o se il risultato "zero chiamanti" significhi effettivamente zero chiamanti o "zero chiamanti all'interno della porzione di sistema che questo strumento può vedere".

grep e ricerca testuale: ampia ma strutturalmente cieca

Quando si sospetta che la ricerca nell'IDE sia incompleta, il passo successivo è solitamente la ricerca testuale: si utilizza grep nelle directory sorgente disponibili oppure si effettua una ricerca sulla piattaforma tramite la ricerca del codice di GitHub o GitLab. Questo amplia considerevolmente l'ambito di ricerca e permette di trovare i chiamanti anche in altri repository, se questi sono accessibili. Il problema strutturale è che la ricerca testuale trova le stringhe, non le chiamate. Restituisce ogni occorrenza del nome della funzione, inclusi i commenti che la menzionano, le stringhe di documentazione, i messaggi di log che la specificano a scopo di debug e le stringhe letterali che contengono il nome della funzione ma non la chiamano. Inoltre, non individua i chiamanti in cui il nome della funzione differisce dalla stringa di ricerca: chiamanti tramite alias, nomi parzialmente corrispondenti in COBOL dove i nomi possono essere abbreviati o invocazione dinamica in cui il nome viene assemblato in fase di esecuzione.

Il set di risultati della ricerca testuale richiede un filtraggio manuale per determinare quali occorrenze corrispondono a luoghi di chiamata effettivi, quali sono riferimenti alla documentazione e quali sono falsi positivi dovuti a collisioni di stringhe. In un sistema di grandi dimensioni, questo filtraggio rappresenta di per sé un notevole sforzo e il processo di filtraggio non può verificarne la completezza: se un chiamante è stato escluso perché utilizza un nome diverso, il set di risultati filtrato non contiene alcuna indicazione dell'omissione.

Avvisi del compilatore e @Deprecated Annotazioni

I linguaggi e le toolchain moderni forniscono meccanismi di annotazione di deprecazione che generano avvisi quando vengono chiamate funzioni deprecate. Java @Deprecated annotazione combinata con -Xlint:deprecation produce avvisi in fase di compilazione nei punti di chiamata. C# [Obsolete] L'attributo genera avvisi in fase di compilazione. La convenzione di Go di nominare le funzioni deprecate e di documentarle in godoc non produce avvisi automaticamente. Questi meccanismi sono utili ma limitati in un modo specifico: funzionano solo per i chiamanti che compilano sulla stessa codebase in cui è annotata la deprecazione.

Un chiamante che utilizza una versione precedente della libreria, antecedente alla @Deprecated Un'annotazione non riceve alcun avviso. Un chiamante che utilizza un artefatto binario anziché compilare dal codice sorgente non riceve alcun avviso. Un chiamante in un linguaggio diverso che effettua chiamate tramite un'interfaccia cross-language non riceve alcun avviso. E, cosa fondamentale, gli avvisi prodotti durante la compilazione sono locali alla vista del compilatore: avvertono delle chiamate che esso rileva, non delle chiamate in altri repository che vengono compilati separatamente. Utilizzare gli avvisi del compilatore come unico meccanismo per l'individuazione dei chiamanti in un sistema multi-servizio significa non rilevare ogni chiamante che compila in modo indipendente, che è la condizione normale nelle architetture a microservizi.

Strumenti di analisi statica: migliori, ma con ambito di applicazione limitato

Gli strumenti di analisi statica specifici per il linguaggio di destinazione forniscono un'enumerazione delle chiamate più accurata rispetto agli IDE e, se configurati per indicizzare più codebase, possono spesso superare i confini dei repository. Creano grafi di chiamate appropriati anziché basarsi sulla corrispondenza testuale, gestiscono meglio gli alias e le chiamate indirette rispetto alla ricerca degli IDE e possono essere eseguiti nelle pipeline di CI per rilevare nuove chiamate man mano che vengono aggiunte. Rappresentano l'approccio più efficace attualmente disponibile per un singolo linguaggio.

La limitazione è lo stesso confine di ambito che vincola gli IDE, ma ora a livello di strumento: uno strumento di analisi statica Java non indicizza i programmi COBOL, un analizzatore COBOL non indicizza i servizi Java e nessuno dei due indicizza i flussi di job JCL. In un sistema in cui la funzione deprecata è un'utilità COBOL chiamata da programmi COBOL che vengono invocati da job JCL e il cui output di dati viene consumato dai servizi Java, ogni strumento di analisi statica vede un frammento della relazione di chiamata. Come esaminato nel contesto di tecniche di refactoring essenzialiL'identificazione di tutti i percorsi di esecuzione verso una determinata sezione di codice, comprese le rare condizioni di errore e i rami di fallback, richiede una mappatura completa del grafo delle chiamate che gli strumenti monolingue non sono in grado di costruire tra linguaggi diversi.

Cosa richiede realmente un inventario completo dei chiamanti

Un inventario completo dei chiamanti di una funzione obsoleta in un sistema aziendale non è il risultato di una ricerca. Si tratta di un'enumerazione strutturata di ogni percorso di esecuzione attraverso il quale è possibile raggiungere la funzione obsoleta, inclusi i percorsi diretti, indiretti, tra linguaggi diversi e con dispatch dinamico. La creazione di questa enumerazione richiede diverse funzionalità che nessuno strumento standard è in grado di offrire.

Un grafico unificato delle chiamate multilingue. Il grafo delle chiamate deve estendersi a tutti i linguaggi del sistema. Una chiamata da una procedura JCL a un programma COBOL, una chiamata dal programma COBOL a un paragrafo di utilità condiviso e una chiamata da un servizio Java allo stesso programma COBOL tramite un'interfaccia middleware devono essere tutte nodi e archi nello stesso grafo. La funzione deprecata è un nodo in questo grafo e un'enumerazione dei chiamanti è un attraversamento di tutti gli archi in entrata, diretti e transitivi, indipendentemente dal linguaggio di origine.

Attraversamento ricorsivo dell'intero grafo delle chiamate. I chiamanti diretti rappresentano solo il primo livello. Un inventario completo richiede di risalire il grafo delle chiamate attraverso i chiamanti indiretti, le funzioni wrapper e i livelli di facciata, fino a raggiungere le funzioni che non hanno chiamanti propri, che sono i veri punti di ingresso delle catene di chiamate. Ogni funzione in un percorso che termina con la funzione deprecata è un chiamante nel senso rilevante: la rimozione della funzione deprecata interromperà ogni percorso che la attraversa.

Indicizzazione tra repository. Il grafo delle chiamate deve includere il codice di ogni repository che potrebbe potenzialmente chiamare la funzione, compresi i repository che dipendono dalla funzione tramite una libreria o un pacchetto condiviso. Ciò richiede l'indicizzazione simultanea di tutti i repository e la risoluzione delle relazioni di importazione tra repository per collegare le chiamate in un repository alle definizioni in un altro.

Rilevamento di schemi di invocazione indiretta. L'analisi deve identificare le chiamate effettuate tramite reflection, dispatch dinamico, puntatori a funzione, delegati e invocazione basata su stringhe nei file di configurazione. Ciò richiede un rilevamento basato su pattern piuttosto che una risoluzione diretta dei bordi delle chiamate: individuare i meccanismi di dispatch dinamico nel codice e determinare a quali funzioni possono essere indirizzate le chiamate in base alle condizioni.

Distinzione tra chiamanti attivi e chiamanti che effettuano solo test o che non rispondono. Non tutti i chiamanti richiedono la stessa risposta. Un chiamante che esiste solo in un test fixture per la funzione deprecata stessa deve essere rimosso come parte della pulizia, non migrato. Un chiamante in codice che è stato identificato come codice morto attraverso l'analisi dell'utilizzo non è un ostacolo alla rimozione della funzione. Comprendere queste distinzioni richiede di combinare l'enumerazione dei chiamanti con informazioni su quali percorsi di codice sono effettivamente attivi. Come dettagliato nell'esame di Rilevamento del codice morto tramite analisi staticaIl codice irraggiungibile e le funzioni inutilizzate possono persistere per anni nei sistemi critici a causa di una documentazione incompleta o di incertezza sulle dipendenze storiche, e l'inventario dei chiamanti per una funzione deprecata deve distinguere tra i chiamanti che sono attivi e quelli che non lo sono più.

Il processo di ammortamento e dismissione: un approccio strutturato

Considerare la rimozione delle funzioni obsolete come un singolo evento anziché come un processo strutturato è la causa principale degli errori di rilevamento delle chiamate. L'approccio corretto prevede di considerare la rimozione come la fase finale di un processo a più fasi che inizia ben prima della cancellazione del codice.

Fase 1: Marcatura e misurazione

Il primo passo consiste nell'annotare la funzione come deprecata utilizzando il meccanismo integrato del linguaggio (@Deprecated a Giava, [Obsolete] in C#, #[deprecated] in Rust, o l'equivalente appropriato) e stabilendo un conteggio di riferimento per le chiamate. Questo conteggio di riferimento non è il risultato di una singola ricerca; è il risultato dell'indicizzazione di ogni codebase nota che potrebbe chiamare la funzione e del conteggio dei risultati. Il conteggio di riferimento ha due scopi: quantifica l'ambito della migrazione e fornisce un riferimento rispetto al quale è possibile misurare i progressi man mano che le chiamate vengono migrate.

I dati di base dovrebbero essere organizzati in base al tipo di chiamante e alla posizione geografica:

Categoria chiamanteContarePrioritàProprietario
Chiamate dirette nello stesso repositoryNAltoSquadra attuale
Chiamate dirette ai servizi per i dipendentiNAltoProprietari del servizio
Chiamanti tramite funzioni wrapperNMedioProprietari dell'involucro
Chiamanti nel codice generato o nel frameworkNMedioTeam di coordinamento
Chiamanti nel codice di sola provaNBassoSquadra attuale
Chiamanti in codice mortoNSolo puliziaSquadra attuale

Fase 2: Notifica e migrazione

Con un inventario completo dei chiamanti, la migrazione diventa uno sforzo organizzato piuttosto che reattivo. Ogni proprietario del chiamante viene informato con le posizioni di chiamata specifiche: non "potresti chiamare questa funzione" ma "chiami questa funzione alla riga 247 di BillingService.java, riga 82 di AccountProcessor.java, e nel test di integrazione alla riga 14 di BillingServiceTest.javaQuesto livello di specificità è ciò che l'inventario dei chiamanti rende possibile e che gli avvisi di deprecazione generici non possono fornire.

Fornire un percorso di migrazione insieme alla notifica è essenziale. La deprecazione dovrebbe includere la documentazione della funzione sostitutiva, una descrizione di eventuali differenze comportamentali tra le vecchie e le nuove implementazioni e, laddove la modifica non sia banale, un esempio di codice che mostri il prima e il dopo. Per i chiamanti nelle codebase di altri team, la tempistica per la migrazione dovrebbe essere negoziata esplicitamente piuttosto che annunciata unilateralmente, perché tali team hanno le proprie priorità e impegni di consegna. Come esplorato nel contesto di refactoring del database tra sistemi dipendentiL'introduzione graduale dei consumatori nella nuova struttura prima della dismissione della vecchia è la disciplina che impedisce che i cambiamenti radicali si presentino come incidenti imprevisti.

Fase 3: Monitorare il numero di chiamate

Tra la misurazione di riferimento e la data di rimozione pianificata, il conteggio delle chiamate deve essere monitorato continuamente. Ogni volta che una chiamata migra alla funzione sostitutiva, il conteggio diminuisce. Il momento della rimozione viene raggiunto quando il conteggio delle chiamate attive raggiunge lo zero (le chiamate solo di test e quelle relative a codice morto possono essere rimosse contemporaneamente alla funzione stessa). Il monitoraggio continuo richiede l'esecuzione dell'enumerazione delle chiamate a intervalli regolari come parte della pipeline CI, e non l'affidamento a un inventario una tantum che diventa obsoleto con le modifiche al codice.

Il monitoraggio rileva anche le nuove chiamate aggiunte durante il periodo di deprecazione. Nelle grandi organizzazioni, è frequente che venga scritto nuovo codice che chiama una funzione deprecata durante la finestra di migrazione, sia perché lo sviluppatore non era a conoscenza della deprecazione, sia perché una revisione del codice non l'ha rilevata, sia perché un generatore automatico di codice produce codice che chiama la funzione deprecata. Il rilevamento delle chiamate a livello di CI per la funzione deprecata, configurato per fallire in caso di nuove chiamate, impedisce che il numero di chiamate aumenti durante la migrazione.

Fase 4: Verificare la completezza prima della rimozione

Immediatamente prima di rimuovere la funzione, l'enumerazione dei chiamanti dovrebbe essere eseguita un'ultima volta sull'intero ambito di tutte le codebase conosciute. Questo controllo finale funge da filtro di sicurezza: conferma che il conteggio dei chiamanti attivi abbia raggiunto lo zero e identifica eventuali aggiunte tardive non rilevate dal monitoraggio CI. A questo punto, l'inventario dovrebbe anche verificare l'assenza di chiamanti dinamici: file di configurazione che fanno riferimento alla funzione tramite stringa, registrazioni basate sulla reflection e qualsiasi altro meccanismo di invocazione indiretta identificato durante l'analisi iniziale.

La verifica deve estendersi al grafo delle dipendenze di tutte le librerie o pacchetti condivisi che espongono la funzione deprecata. Se la funzione fa parte di un'API pubblica utilizzata da terze parti, la tempistica di rimozione deve tenere conto degli utenti esterni che potrebbero non essere raggiungibili tramite l'analisi del codice interno. Per i sistemi interni, la verifica copre ogni codebase indicizzata. Per le API pubblicate pubblicamente, la verifica copre l'insieme noto degli utenti più un periodo di transizione definito durante il quale gli utenti esterni devono migrare.

Come funziona in modo diverso l'individuazione del chiamante negli ambienti legacy e mainframe

Le problematiche descritte sopra si applicano a qualsiasi sistema software di grandi dimensioni, ma risultano particolarmente acute negli ambienti mainframe e legacy, poiché le relazioni tra le chiamate in tali ambienti sono espresse attraverso meccanismi che i moderni strumenti di identificazione del chiamante non sono stati progettati per analizzare.

Negli ambienti COBOL, le funzioni vengono chiamate tramite istruzioni CALL che possono fare riferimento alla destinazione tramite una stringa letterale, un elemento dati contenente il nome del programma o un puntatore a procedura. Il caso della stringa letterale è risolvibile tramite analisi statica; il caso dell'elemento dati richiede un'analisi del flusso di dati per determinare quale valore l'elemento dati potrebbe contenere nel punto della chiamata; e il caso del puntatore a procedura richiede di tracciare come viene assegnato il puntatore. Ciascuno di questi meccanismi di chiamata appare diverso nel codice sorgente e richiede un'analisi diversa per essere risolto.

Negli ambienti JCL, i programmi vengono richiamati per nome nelle istruzioni EXEC PGM=. Il nome del programma è una stringa che corrisponde a un modulo compilato in una libreria di caricamento. Tracciare le chiamate di un programma COBOL tramite JCL richiede l'analisi sintattica del JCL per estrarre i nomi dei programmi, mappare tali nomi ai programmi COBOL compilati che li implementano e individuare quali paragrafi COBOL all'interno di tali programmi richiamano l'utility obsoleta. Questa risoluzione in più fasi esula completamente dalle capacità di un analizzatore COBOL o di un analizzatore JCL che operano singolarmente.

I copybook condivisi rappresentano un caso particolarmente importante negli ambienti COBOL. Un paragrafo obsoleto definito in un copybook può essere incluso in molti programmi tramite istruzioni COPY. Il paragrafo non viene duplicato fisicamente in ogni programma; viene incluso in fase di compilazione. Un'analisi che conta le occorrenze del nome del paragrafo nei file sorgente senza risolvere le inclusioni nei copybook porterà sia a un conteggio eccessivo (trovando la definizione del paragrafo nel copybook stesso) sia a un conteggio insufficiente (non considerando che ogni programma che include il copybook ha accesso al paragrafo). La corretta individuazione dei chiamanti richiede la comprensione di quali programmi includono quali copybook e quali paragrafi all'interno di tali copybook vengono effettivamente chiamati. La relazione tra riferimenti hardcoded e i loro consumatori a valle Questo esempio illustra perché risolvere queste relazioni di invocazione a livello di programma sia essenziale prima di qualsiasi modifica strutturale: quello che sembra un semplice riferimento a una stringa potrebbe essere l'unico meccanismo attraverso il quale decine di programmi accedono a funzionalità critiche.

Come SMART TS XL Crea l'inventario completo dei chiamanti

SMART TS XL Costruisce un grafo di chiamate unificato per ogni linguaggio, piattaforma e repository nell'ambiente indicizzato. Programmi COBOL, flussi di job JCL, servizi Java, applicazioni .NET, stored procedure SQL, script Python e altri artefatti sorgente vengono analizzati, utilizzando un'analisi specifica per linguaggio, in un grafo di riferimenti incrociati comune. Ogni funzione, paragrafo, procedura, metodo e unità di programma è un nodo in questo grafo. Ogni relazione di chiamata, che si tratti di un'istruzione COBOL CALL, di una chiamata a un metodo Java, di un JCL EXEC PGM o di un SQL EXEC, è un arco tipizzato. Il grafo rappresenta la topologia completa delle chiamate del sistema, non una visione parziale per linguaggio.

Quando una funzione viene contrassegnata per la rimozione, SMART TS XLL'enumerazione dei chiamanti attraversa il grafo delle chiamate in entrata dal nodo della funzione di destinazione, raccogliendo ogni chiamante a ogni livello della gerarchia delle chiamate. L'attraversamento è ricorsivo e segue il grafo attraverso funzioni wrapper, livelli di facciata e utilità intermedie fino a raggiungere le funzioni senza chiamanti, che rappresentano i veri punti di ingresso delle catene di chiamate. I risultati sono organizzati per linguaggio, per repository, per tipo di chiamante e per profondità di chiamata, fornendo al team un inventario strutturato che separa i chiamanti diretti da quelli indiretti e i chiamanti attivi da quelli che non vengono utilizzati.

La capacità di analisi dell'impatto della piattaforma estende questo concetto in un report strutturato sull'impatto delle modifiche: non solo quali funzioni chiamano la funzione deprecata, ma anche quali programmi, servizi, job batch e procedure JCL sono interessati a ogni livello della catena di dipendenza. Questo report è l'artefatto che rende attuabile il processo di deprecazione e rimozione: nomina i responsabili, identifica le posizioni di chiamata specifiche e quantifica l'ambito della migrazione necessaria prima che la rimozione possa procedere in sicurezza. Come esaminato nel dettaglio di analisi d'impatto per la gestione del cambiamento aziendaleLa capacità di enumerare i componenti interessati prima di apportare una modifica strutturale è il requisito fondamentale per il funzionamento sicuro di sistemi aziendali complessi e interconnessi.

SMART TS XL Supporta inoltre la fase di monitoraggio continuo del processo di deprecazione. Poiché il grafico dei riferimenti incrociati viene aggiornato costantemente man mano che le modifiche al codice sorgente vengono indicizzate, il conteggio delle chiamate a una funzione deprecata è sempre aggiornato. L'integrazione con la pipeline CI consente ai controlli automatici di fallire in caso di nuove chiamate a funzioni deprecate, imponendo la disciplina di migrazione nel momento in cui viene introdotto nuovo codice, anziché scoprire le violazioni a posteriori. Questa combinazione di enumerazione iniziale, guida alla migrazione e monitoraggio continuo copre l'intero ciclo di vita di una funzione deprecata, dall'annotazione alla rimozione sicura.

Rimozione della funzione senza rimpianti

La differenza tra la rimozione di una funzione che procede senza intoppi e quella che causa errori in produzione risiede quasi sempre nella completezza dell'individuazione dei chiamanti. La rimozione in sé è banale: basta eliminare la definizione e distribuire. Il lavoro vero e proprio sta nella fase di preparazione, e la qualità di tale preparazione dipende dalla completezza dell'inventario dei chiamanti su cui si basa.

Nei sistemi in cui il grafo delle chiamate è superficiale, monolingue e contenuto in un singolo repository, la gerarchia delle chiamate dell'IDE e gli avvisi del compilatore sono una preparazione adeguata. Nei sistemi in cui il grafo delle chiamate si estende su più linguaggi, più repository, più piattaforme e potenzialmente su più decenni di codice, questi strumenti coprono solo una piccola e inconoscibile frazione della superficie effettiva dei chiamanti. Il divario tra ciò che restituiscono e ciò che effettivamente chiama la funzione è dove hanno origine i problemi in produzione.

La creazione di un sistema di enumerazione dei chiamanti cross-linguaggio e cross-repository appositamente progettato non è un perfezionamento del flusso di lavoro di sviluppo per la rimozione delle funzioni. È un prerequisito per eseguire tale flusso di lavoro in modo sicuro in qualsiasi sistema sufficientemente complesso da aver accumulato il tipo di relazioni di chiamata tra sistemi diversi che le funzioni deprecate nei codebase aziendali portano abitualmente. Ogni funzione deprecata rimossa senza un inventario completo dei chiamanti è una release che contiene un numero imprecisato di errori di runtime in attesa del percorso di esecuzione specifico che raggiunge la definizione mancante. Eliminare questa incognita è lo scopo della scoperta strutturata dei chiamanti.