Encontre todas as chamadas de uma função obsoleta.

Como encontrar todas as chamadas de uma função obsoleta antes de removê-la.

Remover uma função obsoleta de uma base de código é, conceitualmente, uma das coisas mais simples que um desenvolvedor pode fazer. Exclua a definição, confirme que nada a utiliza e faça o commit. Na prática, para qualquer função que exista há tempo suficiente para ser considerada obsoleta, a etapa de "confirmar que nada a utiliza" é onde o processo falha. A função pode ser chamada a partir de código escrito há anos por alguém que não faz mais parte da equipe, em um repositório que recebe alterações com pouca frequência, por uma linguagem ou framework que a equipe atual não controla. Ela pode ser invocada indiretamente por meio de um wrapper, por reflexão ou por um mecanismo de despacho em tempo de execução que não aparece em nenhum grafo de chamadas estático. Ela pode ser referenciada em código gerado, em scaffolding de testes ou em um arquivo de configuração que a aciona pelo nome. O desenvolvedor que a marca como obsoleta e o desenvolvedor que eventualmente a remove podem não ter como saber nada disso sem uma ferramenta capaz de construir um inventário completo de chamadas em todos os repositórios.

Localize todos os chamadores antes de remover qualquer coisa.

SMART TS XL Constrói um grafo de chamadas entre linguagens que identifica todos os chamadores de qualquer função antes que você faça uma alteração.

Clique aqui

O custo de errar nisso é imediato e concreto. Uma função removida com descoberta incompleta de quem a chama causa falhas em tempo de execução nos sistemas que ainda dependem dela. Em um monolito com uma única implantação, a superfície de falha é limitada. Em um sistema distribuído com múltiplos serviços, cada um implantado independentemente, as falhas se propagam em cascata: o serviço que fornecia a função é atualizado, os consumidores não, e a quebra aparece em tempo de execução em produção em sistemas que podem pertencer a equipes diferentes. Em um ambiente mainframe onde programas COBOL chamam parágrafos de utilitários compartilhados por nome, a falha pode não se manifestar até que um job em lote específico seja executado, o que pode ocorrer semanalmente ou mensalmente, tornando a referência ausente invisível durante os ciclos normais de teste. Como examinado no contexto mais amplo do gerenciamento de código obsoleto , os riscos se acumulam com o tempo: código obsoleto que é removido de forma incompleta é mais perigoso do que código obsoleto que permanece no local, porque a remoção cria a ilusão de completude enquanto os chamadores restantes continuam a executar em uma definição que não existe mais.

Este artigo é um guia prático para a descoberta de chamadores antes da remoção de funções: o que um inventário completo de chamadores exige, por que as ferramentas que os desenvolvedores usam primeiro são estruturalmente insuficientes, como diferentes tipos de relações de chamada exigem diferentes abordagens de análise e como é a enumeração genuína de chamadores entre sistemas em bases de código de escala empresarial que misturam linguagens, plataformas e repositórios.

Por que a identificação de chamadas é mais difícil do que parece

A versão mais básica da descoberta de chamadas é familiar a todos os desenvolvedores: basta clicar com o botão direito do mouse no nome de uma função em uma IDE, selecionar "Encontrar todas as referências" ou "Mostrar hierarquia de chamadas" e revisar os resultados. Isso funciona de forma confiável dentro do escopo de um único projeto carregado em uma única instância da IDE. No momento em que a base de código se estende além desse escopo, os resultados se tornam incompletos de maneiras que não são visíveis na saída. A IDE não indica quais chamadas não foram encontradas porque não indexou os repositórios que as contêm. O desenvolvedor vê um conjunto de resultados que parece completo e prossegue de acordo.

Este é o problema estrutural da descoberta de chamadas em larga escala: as ferramentas que os desenvolvedores usam com mais fluência são limitadas pelo seu escopo de indexação e, em sistemas grandes, distribuídos e multilíngues, esse escopo cobre apenas uma fração dos locais onde uma determinada função pode ser chamada. A confiança do desenvolvedor na completude dos resultados é inversamente proporcional à completude real da busca. Em uma pequena base de código monolíngue, a hierarquia de chamadas da IDE é genuinamente confiável. Em um sistema corporativo que abrange múltiplos repositórios, linguagens e ambientes de implantação, ela é sistematicamente enganosa. Conforme analisado no contexto da entropia do código e do risco de refatoração , módulos legados podem depender de interfaces obsoletas, enquanto serviços mais recentes ainda chamam rotinas originalmente projetadas para ambientes anteriores, e esses relacionamentos de chamadas entre sistemas são precisamente aqueles que a busca limitada pela IDE não consegue detectar.

Para entender as razões específicas pelas quais a descoberta de chamadores falha, é necessário examinar cada tipo principal de chamada: chamadas diretas, chamadas indiretas, invocações entre idiomas e despacho dinâmico. Cada uma falha por motivos diferentes e exige técnicas de análise distintas para ser resolvida corretamente.

Chamadas diretas entre repositórios

Chamadas diretas são o tipo de chamada mais simples: uma função chamando explicitamente outra pelo nome. Dentro de um mesmo repositório, as IDEs lidam com isso de forma confiável. Entre repositórios diferentes, a análise falha porque a indexação da IDE não abrange o limite. Se o repositório A define uma função utilitária compartilhada e os repositórios B, C e D a importam e chamam, a IDE de qualquer um desses repositórios verá apenas as chamadas dentro de seu próprio escopo indexado.

Esse padrão de chamadas a múltiplos repositórios é a norma, e não a exceção, em arquiteturas de microsserviços, onde bibliotecas compartilhadas são publicadas como pacotes e consumidas por dezenas de serviços. O mantenedor da biblioteca que torna uma função do pacote compartilhado obsoleta precisa saber quais serviços consumidores ainda a utilizam. Seu IDE não tem conhecimento algum sobre os consumidores. O gerenciador de pacotes sabe quais serviços dependem do pacote, mas não qual função específica dentro do pacote cada serviço chama. Mapear a relação entre "este serviço usa a versão X deste pacote" e "este serviço chama esta função obsoleta específica" exige indexar o código-fonte de cada consumidor e resolver a chamada para a definição da função específica.

Chamadas indiretas: Wrappers, Delegates e Facades

Uma função não pode ser chamada diretamente por seus consumidores. Ela pode ser chamada por meio de uma função de encapsulamento que fornece registro adicional, tratamento de erros ou transformação de parâmetros. Ela pode ser atribuída a um delegado ou a um ponteiro de função e invocada por meio do delegado. Ela pode ser registrada em um registro de serviços ou em uma estrutura de plugins e chamada pelo nome por meio de um mecanismo de despacho. Em cada um desses casos, uma busca direta por chamadas à função obsoleta retorna um resultado incompleto, porque os chamadores reais chamam o encapsulamento ou o despachante, não a própria função obsoleta.

A invocação mediada por wrappers é particularmente comum em grandes bases de código, onde preocupações transversais como registro de logs, autorização e lógica de repetição são sobrepostas em torno de funções principais por meio de padrões de wrapper. Uma função obsoleta encapsulada por um utilitário de registro de logs é efetivamente chamada por todos os chamadores do wrapper, e não por qualquer código que contenha o nome da função obsoleta. Identificar esses chamadores requer rastrear o wrapper: o utilitário de registro de logs chama a função obsoleta e, portanto, todos os chamadores do utilitário de registro de logs são chamadores indiretos da função obsoleta. Essa travessia recursiva do grafo de chamadas é o que distingue um inventário completo de chamadores de uma busca superficial por referências.

Considere um exemplo típico em Java onde um método obsoleto é acessado por meio de uma camada de delegação:

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
}

Uma pesquisa "Encontrar todas as referências" para calculateLegacyFee Retorna computeFee como único chamador. Não retorna. processMonthlyBilling, que é o verdadeiro consumidor do comportamento obsoleto. Um inventário completo de chamadores requer percorrer o grafo de chamadas a montante através de computeFee Identificar todos os caminhos que, em última instância, invocam o método obsoleto.

Invocações entre idiomas

As chamadas entre linguagens são a categoria em que as ferramentas padrão de descoberta de chamadas falham completamente. Quando um serviço Java invoca um programa COBOL pelo nome através de uma camada intermediária, quando um script Python chama um procedimento armazenado que encapsula uma função obsoleta, ou quando um job JCL invoca um programa pelo seu PROGNAME que internamente chama um parágrafo obsoleto, nenhuma dessas relações aparece no grafo de chamadas de uma única linguagem. As ferramentas de cada linguagem veem apenas o seu próprio lado da chamada.

Em ambientes mainframe, as chamadas entre linguagens são estruturais e onipresentes. Um fluxo de trabalho JCL especifica o nome do programa COBOL que executa. O programa COBOL chama parágrafos e subprogramas pelo nome. Parágrafos de utilitários definidos em bibliotecas de cópias são compartilhados entre vários programas. Quando um parágrafo em uma biblioteca de cópias é descontinuado, encontrar todos os chamadores exige compreender os relacionamentos de chamada COBOL (quais programas incluem a biblioteca de cópias e quais chamam o parágrafo), os relacionamentos de invocação JCL (quais trabalhos invocam esses programas) e quaisquer interfaces entre linguagens (Java ou SQL que interagem com esses programas). Nenhuma ferramenta única abrange todos esses relacionamentos. Como examinado na análise de análise estática em sistemas legados , as ferramentas de análise estática criadas para ambientes modernos não conseguem visualizar o panorama completo de como os programas legados são acionados, chamados e interconectados quando os relacionamentos de chamada abrangem JCL, COBOL e interfaces entre sistemas simultaneamente.

Despacho e reflexão dinâmicos

Algumas funções são invocadas não pelo seu nome literal no código-fonte, mas por meio de um mecanismo que resolve a função em tempo de execução: reflexão em Java ou .NET. getattr/__call__ Em Python, vinculação tardia em COBOL através de CALL identifier, despacho dinâmico por meio de polimorfismo ou invocação por string em frameworks de plugins e sistemas orientados a configuração. Esses chamadores não contêm o nome da função obsoleta de nenhuma forma que a análise estática possa detectar de maneira confiável.

Um arquivo de configuração que especifica o nome de uma função como uma string, carregado em tempo de execução e usado para invocar a função por meio de reflexão, é um chamador que não aparece em nenhuma análise de código-fonte. Um framework de plugins que descobre e invoca manipuladores registrados por interface é um chamador que aparece no grafo de chamadas apenas como uma chamada ao mecanismo de despacho, e não como uma chamada a um manipulador específico. Identificar esses chamadores requer uma combinação de análise estática para encontrar padrões de despacho dinâmico, rastreamento em tempo de execução para observar as invocações reais e inspeção manual da lógica de configuração e registro que determina para quais funções o despacho é direcionado. Como discutido na análise estática de código ofuscado e gerado , quando os caminhos de execução não são expressos diretamente no código-fonte, a análise estática deve reconstruir os caminhos prováveis ​​a partir de padrões estruturais, em vez de referências textuais diretas, e essas reconstruções exigem uma análise do próprio mecanismo de despacho que leve em consideração a linguagem utilizada.

As ferramentas que os desenvolvedores usam primeiro e onde elas param.

Existe uma sequência previsível de ferramentas que os desenvolvedores usam ao tentar descobrir quem está chamando a chamada, e um ponto igualmente previsível em que cada uma delas deixa de fornecer resultados confiáveis. Compreender essa sequência é importante porque a saída de cada ferramenta parece completa, mesmo quando não está.

Hierarquia de Chamadas da IDE: Confiável Dentro de um Projeto

Os recursos de hierarquia de chamadas da IDE são o primeiro passo mais natural. IntelliJ IDEA, Visual Studio, VS Code e Eclipse oferecem alguma forma de "encontrar todos os chamadores" ou "mostrar hierarquia de chamadas" que enumera recursivamente os chamadores de uma função selecionada, dentro do escopo indexado do projeto ou espaço de trabalho atual. Para uma função usada exclusivamente em um repositório e uma linguagem específicos, esses recursos são precisos e suficientes.

A limitação é explícita no escopo: “dentro do escopo indexado”. Chamadores em outros repositórios, em outros ambientes de execução de linguagem ou em serviços que dependem deste código por meio de um gerenciador de pacotes, em vez de por meio de referência direta ao projeto, estão fora do escopo. O IDE não indica o que não pesquisou. O desenvolvedor recebe um conjunto de resultados e não tem visibilidade de quantos repositórios adicionais existem que não foram indexados, quantos deles usam a função obsoleta ou se o resultado “zero chamadores” significa realmente zero chamadores ou “zero chamadores dentro da fração do sistema que esta ferramenta consegue visualizar”.

grep e Busca de Texto: Abrangente, mas Estruturalmente Cego

Quando se suspeita que a busca na IDE esteja incompleta, o próximo passo geralmente é a busca textual: usar o comando grep nos diretórios de código-fonte disponíveis ou realizar uma busca na plataforma por meio da busca de código do GitHub ou GitLab. Isso amplia consideravelmente o escopo e encontra chamadas em outros repositórios, caso esses repositórios estejam acessíveis. O problema estrutural é que a busca textual encontra strings, não chamadas. Ela retorna todas as ocorrências do nome da função, incluindo comentários que a mencionam, strings de documentação, mensagens de log que nomeiam a função para fins de depuração e literais de string que contêm o nome da função, mas não a chamam. Ela também não encontra chamadas em que o nome da função difere da string de busca: chamadas por meio de aliases, por meio de nomes parcialmente correspondentes em COBOL, onde os nomes podem ser abreviados, ou por meio de invocação dinâmica, onde o nome é montado em tempo de execução.

O conjunto de resultados da busca textual requer filtragem manual para determinar quais ocorrências são locais de chamada reais, quais são referências de documentação e quais são falsos positivos devido a colisões de strings. Em um sistema grande, essa filtragem por si só representa um esforço considerável, e o processo de filtragem não garante a completude dos resultados: se um chamador foi omitido por usar um nome diferente, o conjunto de resultados filtrado não contém nenhuma indicação da omissão.

Avisos do compilador e @Deprecated Anotações

Linguagens e ferramentas modernas fornecem mecanismos de anotações de depreciação que geram avisos quando funções obsoletas são chamadas. O Java, por exemplo, possui um mecanismo semelhante. @Deprecated anotação combinada com -Xlint:deprecation Produz avisos em tempo de compilação nos locais de chamada. C#'s [Obsolete] O atributo gera avisos em tempo de compilação. A convenção do Go de nomear funções obsoletas e documentá-las no godoc não produz avisos automaticamente. Esses mecanismos são valiosos, mas limitados de uma forma específica: eles só funcionam para chamadas que compilam com a mesma base de código em que a obsolescência está anotada.

Um usuário que utiliza uma versão mais antiga da biblioteca, anterior à @Deprecated Uma anotação não gera nenhum aviso. Um chamador que usa um artefato binário em vez de compilar a partir do código-fonte também não recebe nenhum aviso. Um chamador em uma linguagem diferente que faz a chamada por meio de uma interface multilíngue também não recebe nenhum aviso. E, crucialmente, os avisos gerados durante a compilação são locais para a visão do compilador: eles alertam sobre as chamadas que ele vê, não sobre as chamadas em outros repositórios que são compiladas separadamente. Usar avisos do compilador como o único mecanismo para descoberta de chamadores em um sistema com múltiplos serviços ignora todos os chamadores que compilam independentemente, o que é a condição normal em arquiteturas de microsserviços.

Ferramentas de análise estática: melhores, mas com escopo limitado

Ferramentas de análise estática desenvolvidas especificamente para esse fim fornecem uma enumeração de chamadas mais precisa do que as IDEs para a linguagem alvo e, muitas vezes, podem ultrapassar limites de repositórios se configuradas para indexar várias bases de código. Elas constroem grafos de chamadas adequados em vez de depender de correspondência de texto, lidam com aliases e chamadas indiretas melhor do que a busca da IDE e podem ser executadas em pipelines de CI para detectar novas chamadas à medida que são adicionadas. Elas representam a abordagem de linguagem única mais completa disponível.

A limitação é a mesma que restringe as IDEs, mas agora no nível da ferramenta: uma ferramenta de análise estática Java não indexa programas COBOL, um analisador COBOL não indexa serviços Java e nenhum deles indexa fluxos de tarefas JCL. Em um sistema onde a função obsoleta é um utilitário COBOL chamado a partir de programas COBOL que são invocados por tarefas JCL e cuja saída de dados é consumida por serviços Java, cada ferramenta de análise estática vê apenas um fragmento da relação de chamadas. Conforme examinado no contexto de técnicas essenciais de refatoração , identificar todos os caminhos de execução para uma determinada seção de código, incluindo condições de erro raras e ramificações de fallback, requer o tipo de mapeamento completo do grafo de chamadas que ferramentas de linguagem única não conseguem construir entre diferentes linguagens.

O que um inventário completo de chamadas realmente exige

Um inventário completo de chamadas para uma função obsoleta em um sistema corporativo não é um resultado de busca. Trata-se de uma enumeração estruturada de todos os caminhos de execução pelos quais a função obsoleta pode ser alcançada, incluindo caminhos diretos, indiretos, entre linguagens e com despacho dinâmico. A construção dessa enumeração requer diversas funcionalidades que nenhuma ferramenta padrão oferece.

Um grafo de chamadas unificado e multilíngue. O grafo de chamadas deve abranger todas as linguagens do sistema. Uma chamada de um procedimento JCL para um programa COBOL, uma chamada do programa COBOL para um parágrafo de utilitário compartilhado e uma chamada de um serviço Java para o mesmo programa COBOL por meio de uma interface de middleware devem ser nós e arestas no mesmo grafo. A função obsoleta é um nó neste grafo, e uma enumeração de chamadores é uma travessia de todas as arestas de entrada, diretas e transitivas, independentemente da linguagem de origem.

Percurso recursivo por todo o grafo de chamadas. Os chamadores diretos representam apenas a primeira camada. Um inventário completo requer seguir o grafo de chamadas a montante, passando por chamadores indiretos, funções de encapsulamento e camadas de fachada, até que o percurso alcance funções que não possuem chamadores próprios, que são os verdadeiros pontos de entrada das cadeias de chamadas. Toda função em um caminho que termina na função obsoleta é um chamador no sentido relevante: remover a função obsoleta interromperá todos os caminhos que passam por ela.

Indexação entre repositórios. O grafo de chamadas deve incluir o código de todos os repositórios que potencialmente podem chamar a função, incluindo repositórios que dependem da função por meio de uma biblioteca ou pacote compartilhado. Isso requer a indexação simultânea de todos os repositórios e a resolução de relações de importação entre repositórios para conectar chamadas em um repositório a definições em outro.

Detecção de padrões de invocação indireta. A análise deve identificar chamadas feitas por meio de reflexão, despacho dinâmico, ponteiros de função, delegados e invocação baseada em strings em arquivos de configuração. Isso requer detecção baseada em padrões, em vez de resolução direta das arestas de chamada: encontrar os mecanismos de despacho dinâmico no código e determinar para quais funções eles podem despachar e sob quais condições.

Distinção entre chamadores ativos e chamadores de teste ou inativos. Nem todos os chamadores exigem a mesma resposta. Um chamador que existe apenas em um ambiente de teste para a própria função obsoleta precisa ser removido como parte da limpeza, e não migrado. Um chamador em um código que foi identificado como código morto por meio de análise de uso não impede a remoção da função. Compreender essas distinções exige combinar a enumeração de chamadores com informações sobre quais caminhos de código estão realmente ativos. Conforme detalhado na análise da detecção de código morto por meio de análise estática , código inacessível e funções não utilizadas podem persistir por anos em sistemas de missão crítica devido à documentação incompleta ou incerteza sobre dependências históricas, e o inventário de chamadores para uma função obsoleta deve distinguir entre chamadores que estão ativos e chamadores que estão inativos.

O Processo de Descontinuação e Remoção: Uma Abordagem Estruturada

Tratar a remoção de funções obsoletas como um evento isolado, em vez de um processo estruturado, é a origem da maioria das falhas na detecção de chamadas. A abordagem correta considera a remoção como a etapa final de um processo multifásico que começa muito antes de qualquer código ser excluído.

Fase 1: Marcar e Medir

O primeiro passo é anotar a função como obsoleta usando o mecanismo integrado da linguagem (@Deprecated em Java, [Obsolete] Em C#, #[deprecated] em Rust (ou equivalente apropriado) e estabelecendo uma contagem de chamadas de referência. Essa linha de base não é o resultado de uma única busca; ela resulta da indexação de todas as bases de código conhecidas que possam chamar a função e da contagem dos resultados. A linha de base serve a dois propósitos: quantifica o escopo da migração e fornece uma referência para medir o progresso à medida que as chamadas são migradas.

A linha de base deve ser organizada por tipo de chamador e localização:

Categoria do chamadorContarPrioridadeProprietário
Chamadas diretas no mesmo repositórioNAltoTime atual
Atendimentos diretos a pessoas dependentesNAltoProprietários de serviços
Chamadas por meio de funções de encapsulamentoNSuporte:Proprietários de embalagens
Chamadores em código gerado ou de estruturaNSuporte:Equipe Framework
Chamadores em código somente de testeNBaixoTime atual
Chamadas em código mortoNSomente limpezaTime atual

Fase 2: Notificar e migrar

Com um inventário completo de chamadas, a migração se torna um esforço organizado em vez de reativo. Cada responsável pela chamada é notificado com os locais específicos da chamada: não "você pode estar chamando esta função", mas sim "você está chamando esta função na linha 247 de...". BillingService.java, linha 82 de AccountProcessor.javae no teste de integração na linha 14 de BillingServiceTest.javaEsse nível de especificidade é o que o inventário de chamadas possibilita e o que os avisos genéricos de obsolescência não conseguem fornecer.

Fornecer um plano de migração juntamente com a notificação é essencial. A descontinuação deve incluir a documentação da função substituta, uma descrição de quaisquer diferenças de comportamento entre as implementações antiga e nova e, quando a alteração for significativa, um exemplo de código mostrando o antes e o depois. Para usuários em bases de código de outras equipes, o cronograma de migração deve ser negociado explicitamente, em vez de anunciado unilateralmente, pois essas equipes têm suas próprias prioridades e compromissos de entrega. Como explorado no contexto da refatoração de banco de dados em sistemas dependentes , a implementação gradual da nova estrutura para os usuários antes da descontinuação da antiga é a disciplina que impede que alterações incompatíveis surjam como incidentes inesperados.

Fase 3: Monitorar o número de chamadas

Entre a medição inicial e a data de remoção planejada, a contagem de chamadas deve ser monitorada continuamente. Cada vez que uma chamada migra para a função substituta, a contagem diminui. O ponto de remoção é atingido quando a contagem chega a zero para chamadas ativas (chamadas somente para teste e chamadas de código morto podem ser removidas simultaneamente com a própria função). O monitoramento contínuo requer a execução da enumeração de chamadas de forma programada como parte do pipeline de CI, e não depender de um inventário único que se torna obsoleto à medida que o código é alterado.

O monitoramento também detecta novos chamadores adicionados durante o período de descontinuação. Em grandes organizações, é comum que novos códigos sejam escritos chamando uma função descontinuada durante a janela de migração, seja porque o desenvolvedor desconhecia a descontinuação, porque uma revisão de código não a detectou ou porque um gerador de código automatizado produz código que chama a função descontinuada. A detecção de chamadores em nível de CI para a função descontinuada, configurada para falhar em novos pontos de chamada, impede que o número de chamadores aumente enquanto a migração está em andamento.

Fase 4: Verificar a integridade antes da remoção

Imediatamente antes de remover a função, a enumeração de chamadas deve ser executada uma última vez em todo o escopo de todas as bases de código conhecidas. Essa verificação final serve como um mecanismo de segurança: ela confirma que a contagem de chamadas chegou a zero para chamadas ativas e identifica quaisquer adições de última hora que não foram detectadas pelo monitoramento de CI. Nesse ponto, o inventário também deve verificar a ausência de chamadas dinâmicas: arquivos de configuração que referenciam a função por string, registros baseados em reflexão e quaisquer outros mecanismos de invocação indireta que foram identificados durante a análise inicial.

A verificação deve abranger o grafo de dependências de quaisquer bibliotecas ou pacotes compartilhados que exponham a função obsoleta. Se a função fizer parte de uma API pública consumida por terceiros, o cronograma de remoção deve levar em conta os consumidores externos que podem não ser alcançáveis ​​por meio de análise de código interna. Para sistemas internos, a verificação abrange todas as bases de código indexadas. Para APIs publicadas, a verificação abrange o conjunto conhecido de consumidores, além de um período de transição definido durante o qual os consumidores externos devem migrar.

Como a descoberta de chamadas funciona de forma diferente em ambientes legados e mainframe

Os desafios descritos acima se aplicam a qualquer sistema de software de grande porte, mas são particularmente graves em ambientes mainframe e legados, porque os relacionamentos de chamadas nesses ambientes são expressos por meio de mecanismos que as ferramentas modernas de descoberta de chamadores não foram projetadas para analisar.

Em ambientes COBOL, as funções são chamadas por meio de instruções CALL que podem referenciar o destino por uma string literal, por um item de dados que contém o nome do programa ou por um ponteiro de procedimento. O caso da string literal pode ser resolvido por meio de análise estática; o caso do item de dados requer análise de fluxo de dados para determinar qual valor o item de dados pode conter no momento da chamada; e o caso do ponteiro de procedimento requer o rastreamento de como o ponteiro é atribuído. Cada um desses mecanismos de chamada aparece de forma diferente no código-fonte e requer análises distintas para sua resolução.

Em ambientes JCL, os programas são invocados por nome em instruções EXEC PGM=. O nome do programa é uma string que mapeia para um módulo compilado em uma biblioteca de carregamento. Rastrear os chamadores de um programa COBOL por meio do JCL requer analisar o JCL para extrair os nomes dos programas, mapear esses nomes para os programas COBOL compilados que os implementam e resolver quais parágrafos COBOL dentro desses programas chamam o utilitário obsoleto. Essa resolução em várias etapas está completamente fora do escopo de um analisador COBOL ou de um analisador JCL trabalhando isoladamente.

Os copybooks compartilhados são um caso particularmente importante em ambientes COBOL. Um parágrafo obsoleto definido em um copybook pode ser incluído em vários programas por meio de instruções COPY. O parágrafo não é fisicamente duplicado em cada programa; ele é incluído em tempo de compilação. Uma análise que conta as ocorrências do nome do parágrafo nos arquivos de origem sem resolver as inclusões de copybook resultará tanto em superestimação (encontrando a definição do parágrafo no próprio copybook) quanto em subestimação (ignorando o fato de que todo programa que inclui o copybook tem acesso ao parágrafo). A descoberta correta do chamador requer a compreensão de quais programas incluem quais copybooks e quais parágrafos dentro desses copybooks eles realmente chamam. A relação entre referências codificadas e seus consumidores subsequentes ilustra por que resolver essas relações de invocação em nível de programa é essencial antes de qualquer alteração estrutural: o que parece ser uma simples referência de string pode ser o único mecanismo pelo qual dezenas de programas acessam funcionalidades críticas.

Como SMART TS XL Constrói o inventário completo de chamadas.

SMART TS XL Constrói um grafo de chamadas unificado para todas as linguagens, plataformas e repositórios no ambiente indexado. Programas COBOL, fluxos de tarefas JCL, serviços Java, aplicações .NET, procedimentos armazenados SQL, scripts Python e outros artefatos de origem são analisados ​​usando análises específicas de cada linguagem e transformados em um grafo de referência cruzada comum. Cada função, parágrafo, procedimento, método e unidade de programa é um nó nesse grafo. Cada relação de chamada, seja uma instrução CALL em COBOL, uma invocação de método Java, um EXEC PGM em JCL ou um EXEC em SQL, é uma aresta tipada. O grafo representa a topologia completa de chamadas do sistema, e não uma visão parcial por linguagem.

Quando uma função é marcada para remoção, SMART TS XLA enumeração de chamadas percorre o grafo de chamadas a partir do nó da função de destino, coletando todas as chamadas em cada nível da hierarquia de chamadas. O percurso é recursivo, seguindo o grafo através de funções de encapsulamento, camadas de fachada e utilitários intermediários até alcançar funções sem chamadas, que representam os verdadeiros pontos de entrada das cadeias de chamadas. Os resultados são organizados por linguagem, por repositório, por tipo de chamada e por profundidade da chamada, fornecendo à equipe um inventário estruturado que separa chamadas diretas de chamadas indiretas e chamadas ativas de chamadas de código morto.

A capacidade de análise de impacto da plataforma amplia isso para um relatório estruturado de impacto de mudanças: não apenas quais funções chamam a função obsoleta, mas também quais programas, serviços, jobs em lote e procedimentos JCL são afetados em todos os níveis da cadeia de dependências. Este relatório é o artefato que torna o processo de obsolescência e remoção viável: ele nomeia os responsáveis, identifica os locais específicos de chamada e quantifica o escopo da migração necessária antes que a remoção possa prosseguir com segurança. Como examinado em detalhes na análise de impacto para gerenciamento de mudanças corporativas , a capacidade de enumerar os componentes afetados antes de realizar uma mudança estrutural é o requisito fundamental para a operação segura de sistemas corporativos complexos e interconectados.

SMART TS XL Também oferece suporte à fase de monitoramento contínuo do processo de descontinuação. Como o gráfico de referência cruzada é atualizado continuamente à medida que as alterações no código-fonte são indexadas, a contagem de chamadas para uma função descontinuada está sempre atualizada. A integração com o pipeline de CI permite que verificações automatizadas falhem em novas chamadas para funções descontinuadas, reforçando a disciplina de migração no momento em que o novo código é introduzido, em vez de descobrir violações posteriormente. Essa combinação de enumeração inicial, orientação de migração e monitoramento contínuo abrange todo o ciclo de vida de uma função descontinuada, desde a anotação até a remoção segura.

Remoção de função sem arrependimentos

A diferença entre a remoção de uma função que ocorre sem problemas e aquela que causa falhas em produção reside quase sempre na completude da descoberta de quem a chamou. A remoção em si é trivial: basta excluir a definição e implantar. O trabalho está na preparação, e essa preparação só é tão boa quanto o inventário de quem a chamou.

Em sistemas onde o grafo de chamadas é raso, monolíngue e contido em um único repositório, a hierarquia de chamadas da IDE e os avisos do compilador são uma preparação adequada. Em sistemas onde o grafo de chamadas abrange múltiplas linguagens, múltiplos repositórios, múltiplas plataformas e potencialmente décadas de código, essas ferramentas cobrem uma pequena e desconhecida fração da superfície real de chamadas. A lacuna entre o que elas retornam e o que de fato chama a função é onde se originam as falhas em produção.

A enumeração de chamadores multiplataforma e multirepositório, desenvolvida especificamente para este fim, não é um mero aprimoramento do fluxo de trabalho do desenvolvedor para remoção de funções. É um pré-requisito para a execução segura desse fluxo de trabalho em qualquer sistema complexo o suficiente para ter acumulado o tipo de relações de chamadas entre sistemas que funções obsoletas em bases de código corporativas rotineiramente carregam. Cada função obsoleta removida sem um inventário completo de chamadores representa uma versão com um número desconhecido de falhas em tempo de execução, aguardando o caminho de execução específico que leva à definição ausente. Eliminar essa incógnita é o objetivo da descoberta estruturada de chamadores.