Eliminar una función obsoleta de un código fuente es, conceptualmente, una de las cosas más sencillas que un desarrollador puede hacer. Se borra la definición, se confirma que nadie la utiliza y se confirman los cambios. En la práctica, para cualquier función que haya existido el tiempo suficiente como para estar obsoleta, el paso de "confirmar que nadie la utiliza" es donde el proceso se complica. La función puede ser llamada desde código escrito hace años por alguien que ya no forma parte del equipo, en un repositorio que recibe cambios con poca frecuencia, o por un lenguaje o framework que no pertenece al equipo actual. Puede ser invocada indirectamente a través de un envoltorio, mediante reflexión o mediante un mecanismo de despacho en tiempo de ejecución que no aparece en ningún gráfico de llamadas estático. Puede ser referenciada en código generado, en la estructura de pruebas o en un archivo de configuración que la activa por su nombre. El desarrollador que la marca como obsoleta y el desarrollador que finalmente la elimina pueden no tener forma de saber nada de esto sin una herramienta capaz de crear un inventario completo de llamadas entre repositorios.
Identifique a todas las personas que llaman antes de eliminar nada.
SMART TS XL Crea un gráfico de llamadas entre diferentes lenguajes que identifica a todos los que llaman a cualquier función antes de que realices un cambio.
Haga clic aquíEl coste de cometer este error es inmediato y concreto. Una función eliminada sin una detección completa de la función que la llama provoca fallos en tiempo de ejecución en los sistemas que aún dependen de ella. En un sistema monolítico con una única implementación, la superficie de fallo está limitada. En un sistema distribuido con múltiples servicios, cada uno implementado de forma independiente, los fallos se propagan: el servicio que proporcionaba la función se actualiza, los consumidores no, y el fallo aparece en tiempo de ejecución en producción en sistemas que pueden pertenecer a diferentes equipos. En un entorno de mainframe donde los programas COBOL llaman a párrafos de utilidad compartida por su nombre, el fallo puede no manifestarse hasta que se ejecuta un trabajo por lotes específico, que podría ser semanal o mensual, lo que hace que la referencia faltante sea invisible durante los ciclos de prueba normales. Como se analiza en el contexto más amplio de la gestión de código obsoleto , los riesgos se acumulan con el tiempo: el código obsoleto que se elimina de forma incompleta es más peligroso que el código obsoleto que se deja en su lugar, porque la eliminación crea la ilusión de completitud mientras que las funciones que siguen llamando a la función continúan ejecutándose con una definición que ya no existe.
Este artículo es una guía práctica para descubrir qué funciones realizan llamadas antes de eliminar una función: qué requiere un inventario completo de funciones que realizan llamadas, por qué las herramientas a las que recurren los desarrolladores en primer lugar son estructuralmente insuficientes, cómo los diferentes tipos de relaciones de llamadas requieren diferentes enfoques de análisis y cómo se ve una enumeración genuina de funciones que realizan llamadas entre sistemas en bases de código a escala empresarial que mezclan lenguajes, plataformas y repositorios.
Por qué identificar a la persona que llama es más difícil de lo que parece
La versión básica de detección de llamadas es familiar para cualquier desarrollador: basta con hacer clic con el botón derecho en el nombre de una función en un IDE, seleccionar «Buscar todas las referencias» o «Mostrar jerarquía de llamadas» y revisar los resultados. Esto funciona correctamente dentro del ámbito de un único proyecto cargado en una única instancia del IDE. En cuanto el código fuente se extiende más allá de ese ámbito, los resultados se vuelven incompletos de maneras que no son visibles en la salida. El IDE no indica qué llamadas no encontró porque no indexó los repositorios que las contienen. El desarrollador ve un conjunto de resultados que parece completo y procede en consecuencia.
Este es el problema estructural del descubrimiento de llamadas a gran escala: las herramientas que los desarrolladores usan con mayor fluidez están limitadas por su alcance de indexación, y en sistemas grandes, distribuidos y multilingües, ese alcance cubre solo una fracción de los lugares donde se podría llamar a una función determinada. La confianza del desarrollador en la exhaustividad de los resultados es inversamente proporcional a la exhaustividad real de la búsqueda. En una base de código pequeña y monolingüe, la jerarquía de llamadas del IDE es realmente fiable. En un sistema empresarial que abarca múltiples repositorios, lenguajes y entornos de despliegue, resulta sistemáticamente engañosa. Como se analiza en el contexto de la entropía del código y el riesgo de refactorización , los módulos heredados pueden depender de interfaces obsoletas, mientras que los servicios más recientes siguen llamando a rutinas diseñadas originalmente para entornos anteriores, y estas relaciones de llamadas entre sistemas son precisamente las que la búsqueda limitada al IDE no puede ver.
Para comprender las razones específicas por las que falla la detección del origen de la llamada, es necesario examinar cada tipo principal de llamada: llamadas directas, llamadas indirectas, llamadas en varios idiomas y despacho dinámico. Cada una falla por diferentes motivos y requiere diferentes técnicas de análisis para resolverla correctamente.
Llamadas directas a través de los límites de los repositorios
Las llamadas directas son el tipo de llamada más simple: una función llama explícitamente a otra por su nombre. Dentro de un mismo repositorio, los IDE las gestionan correctamente. Sin embargo, el análisis falla entre repositorios porque la indexación del IDE no abarca el límite. Si el repositorio A define una función de utilidad compartida y los repositorios B, C y D la importan y la llaman, el IDE de cualquiera de estos repositorios solo verá las llamadas dentro de su propio ámbito indexado.
Este patrón de llamadas a múltiples repositorios es la norma, no la excepción, en las arquitecturas de microservicios, donde las bibliotecas compartidas se publican como paquetes y se utilizan en decenas de servicios. El responsable del mantenimiento de la biblioteca que declara obsoleta una función en el paquete compartido necesita saber qué servicios que la utilizan aún la llaman. Su IDE desconoce la información de los consumidores. El gestor de paquetes sabe qué servicios dependen del paquete, pero no qué función específica dentro del paquete llama cada servicio. Para mapear la información de «este servicio usa la versión X de este paquete» a «este servicio llama a esta función obsoleta específica», es necesario indexar el código fuente de cada consumidor y resolver la llamada a la definición de la función específica.
Llamadas indirectas: envoltorios, delegados y fachadas
Una función puede no ser llamada directamente por sus consumidores. Puede ser llamada a través de una función contenedora que proporciona registro adicional, manejo de errores o transformación de parámetros. Puede asignarse a un delegado o a un puntero de función e invocarse a través del delegado. Puede registrarse en un registro de servicios o en un marco de complementos y llamarse por su nombre mediante un mecanismo de distribución. En cada uno de estos casos, una búsqueda directa de llamadas a la función obsoleta devuelve un resultado incompleto, porque quienes la llaman realmente invocan la función contenedora o el mecanismo de distribución, no la función obsoleta en sí.
La invocación mediada por envoltorios es particularmente común en bases de código extensas donde aspectos transversales como el registro, la autorización y la lógica de reintentos se superponen a las funciones principales mediante patrones de envoltorio. Una función obsoleta envuelta por una utilidad de registro es invocada efectivamente por cada llamador del envoltorio, no por cualquier código que contenga el nombre de la función obsoleta. Identificar a esos llamadores requiere rastrear el envoltorio: la utilidad de registro llama a la función obsoleta, y cada llamador de la utilidad de registro es, por lo tanto, un llamador indirecto de la función obsoleta. Este recorrido recursivo del grafo de llamadas es lo que distingue un inventario exhaustivo de llamadores de una búsqueda de referencia superficial.
Consideremos un ejemplo representativo de Java en el que se accede a un método obsoleto a través de una capa de delegación:
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
}
Una búsqueda de “Encontrar todas las referencias” calculateLegacyFee devoluciones computeFee como la única persona que llama. No devuelve processMonthlyBilling, que es el verdadero consumidor del comportamiento obsoleto. Un inventario completo de llamadas requiere recorrer el gráfico de llamadas en sentido ascendente a través de computeFee para identificar todas las rutas que, en última instancia, invocan el método obsoleto.
Invocaciones entre idiomas
Las llamadas entre lenguajes son la categoría donde las herramientas estándar de detección de llamadas fallan por completo. Cuando un servicio Java invoca un programa COBOL por su nombre a través de una capa de middleware, cuando un script de Python llama a un procedimiento almacenado que encapsula una función obsoleta, o cuando un trabajo JCL invoca un programa por su PROGNAME que internamente llama a un párrafo obsoleto, ninguna de estas relaciones aparece en el gráfico de llamadas de ningún lenguaje individual. Las herramientas de cada lenguaje solo ven su propia parte de la llamada.
En entornos de mainframe, las llamadas entre lenguajes son estructurales y omnipresentes. Un flujo de trabajo JCL especifica el nombre del programa COBOL que ejecuta. El programa COBOL llama a párrafos y subprogramas por su nombre. Los párrafos de utilidad definidos en bibliotecas de copia se comparten entre muchos programas. Cuando un párrafo en una biblioteca de copia queda obsoleto, encontrar todos los llamadores requiere comprender las relaciones de llamada de COBOL (qué programas incluyen la biblioteca de copia y cuáles llaman al párrafo), las relaciones de invocación de JCL (qué trabajos invocan esos programas) y cualquier interfaz entre lenguajes (Java o SQL que interactúe con esos programas). Ninguna herramienta abarca todas estas relaciones. Como se examinó en el análisis estático de sistemas heredados , las herramientas de análisis estático diseñadas para entornos modernos no pueden ver el panorama completo de cómo se activan, llaman e interconectan los programas heredados cuando las relaciones de llamada abarcan JCL, COBOL e interfaces entre sistemas simultáneamente.
Despacho dinámico y reflexión
Algunos llamadores invocan una función no por su nombre literal en el código fuente sino a través de un mecanismo que resuelve la función en tiempo de ejecución: reflexión en Java o .NET, getattr/__call__ en Python, enlace tardío en COBOL a través de CALL identifier, despacho dinámico a través de polimorfismo, o invocación por cadena en marcos de complementos y sistemas controlados por configuración. Estas llamadas no contienen el nombre de la función obsoleta en ninguna forma que el análisis estático pueda detectar de manera fiable.
Un archivo de configuración que especifica el nombre de una función como una cadena, cargado en tiempo de ejecución y utilizado para invocar la función mediante reflexión, es un llamador que no aparece en ningún análisis de código fuente. Un marco de complementos que descubre e invoca manejadores registrados mediante interfaz es un llamador que aparece en el gráfico de llamadas solo como una llamada al mecanismo de despacho, no como una llamada a ningún manejador específico. La identificación de estos llamadores requiere una combinación de análisis estático para encontrar patrones de despacho dinámico, rastreo en tiempo de ejecución para observar las invocaciones reales e inspección manual de la lógica de configuración y registro que determina a qué funciones se despachan. Como se discute en el examen del análisis estático en código ofuscado y generado , cuando las rutas de ejecución no se expresan directamente en el código fuente, el análisis estático debe reconstruir las rutas probables a partir de patrones estructurales en lugar de referencias textuales directas, y esas reconstrucciones requieren un análisis del mecanismo de despacho que tenga en cuenta el lenguaje.
Las herramientas a las que recurren primero los desarrolladores y dónde se detienen
Existe una secuencia predecible de herramientas que los desarrolladores utilizan al intentar descubrir quién realiza la llamada, y un punto igualmente predecible en el que cada una deja de proporcionar resultados fiables. Comprender esta secuencia es importante porque la salida de cada herramienta parece completa incluso cuando no lo es.
Jerarquía de llamadas del IDE: Confiable dentro de un mismo proyecto
Las funciones de jerarquía de llamadas del IDE son el primer paso más lógico. IntelliJ IDEA, Visual Studio, VS Code y Eclipse ofrecen alguna forma de "encontrar todas las llamadas" o "mostrar la jerarquía de llamadas" que enumera recursivamente las llamadas a una función seleccionada dentro del ámbito indexado del proyecto o espacio de trabajo actual. Para una función que se usa exclusivamente dentro de un repositorio y un lenguaje, estas funciones son precisas y suficientes.
La limitación es explícita en el alcance: «dentro del ámbito indexado». Las llamadas desde otros repositorios, en otros entornos de ejecución de lenguajes o en servicios que dependen de este código a través de un gestor de paquetes en lugar de una referencia directa al proyecto quedan fuera del alcance. El IDE no indica qué no buscó. El desarrollador recibe un conjunto de resultados y no tiene visibilidad sobre cuántos repositorios adicionales existen que no fueron indexados, cuántos de ellos usan la función obsoleta, o si el resultado de «cero llamadas» significa realmente cero llamadas o «cero llamadas dentro de la fracción del sistema que esta herramienta puede ver».
grep y búsqueda de texto: amplios pero estructuralmente limitados.
Cuando se sospecha que la búsqueda en el IDE es incompleta, el siguiente paso suele ser la búsqueda de texto: usar grep en los directorios de código fuente disponibles o realizar una búsqueda en la plataforma a través de la búsqueda de código de GitHub o GitLab. Esto amplía considerablemente el alcance y encuentra llamadas en otros repositorios si estos son accesibles. El problema estructural es que la búsqueda de texto encuentra cadenas, no llamadas. Devuelve cada aparición del nombre de la función, incluyendo comentarios que la mencionan, cadenas de documentación, mensajes de registro que nombran la función con fines de depuración y literales de cadena que contienen el nombre de la función pero no la llaman. También omite las llamadas donde el nombre de la función difiere de la cadena de búsqueda: llamadas a través de alias, a través de nombres parcialmente coincidentes en COBOL donde los nombres pueden estar abreviados o a través de invocación dinámica donde el nombre se ensambla en tiempo de ejecución.
El conjunto de resultados de la búsqueda de texto requiere un filtrado manual para determinar qué ocurrencias son sitios de llamada reales, cuáles son referencias a la documentación y cuáles son falsos positivos debido a colisiones de cadenas. En un sistema grande, este filtrado supone un esfuerzo considerable, y el proceso de filtrado no puede verificar la exhaustividad: si se omitió una llamada porque utiliza un nombre diferente, el conjunto de resultados filtrado no contiene ninguna indicación de la omisión.
Advertencias del compilador y @Deprecated Anotaciones
Los lenguajes y conjuntos de herramientas modernos proporcionan mecanismos de anotación de obsolescencia que generan advertencias cuando se llaman funciones obsoletas. Java @Deprecated anotación combinada con -Xlint:deprecation produce advertencias en tiempo de compilación en los sitios de llamada. C# [Obsolete] El atributo genera advertencias en tiempo de compilación. La convención de Go de nombrar las funciones obsoletas y documentarlas en godoc no genera advertencias automáticamente. Estos mecanismos son valiosos, pero tienen una limitación específica: solo funcionan para las funciones que compilan con el mismo código fuente donde se indica la obsolescencia.
Un usuario que utiliza una versión anterior de la biblioteca, anterior a la @Deprecated Una llamada que utiliza un artefacto binario en lugar de compilar desde el código fuente no recibe ninguna advertencia. Una llamada en un lenguaje diferente que realiza la llamada a través de una interfaz entre lenguajes no recibe ninguna advertencia. Y, lo que es crucial, las advertencias generadas durante la compilación son locales a la vista del compilador: advierten sobre las llamadas que este ve, no sobre las llamadas en otros repositorios que se compilan por separado. Utilizar las advertencias del compilador como único mecanismo para la detección de llamadas en un sistema multiservicio deja sin detectar a todas las llamadas que compilan de forma independiente, que es la condición normal en las arquitecturas de microservicios.
Herramientas de análisis estático: Mejores, pero con alcance limitado.
Las herramientas de análisis estático diseñadas específicamente para este fin proporcionan una enumeración de llamadas más precisa que los IDE para su lenguaje de destino y, a menudo, pueden trascender los límites de los repositorios si se configuran para indexar múltiples bases de código. Crean gráficos de llamadas adecuados en lugar de basarse en la coincidencia de texto, manejan mejor los alias y las llamadas indirectas que la búsqueda de los IDE, y pueden ejecutarse en pipelines de CI para detectar nuevas llamadas a medida que se agregan. Son el enfoque más potente disponible para un solo lenguaje.
La limitación es la misma que restringe a los IDE, pero ahora a nivel de herramienta: una herramienta de análisis estático de Java no indexa programas COBOL, un analizador COBOL no indexa servicios Java, y ninguno indexa flujos de trabajos JCL. En un sistema donde la función obsoleta es una utilidad COBOL llamada desde programas COBOL que se invocan desde trabajos JCL y cuya salida de datos es consumida por servicios Java, cada herramienta de análisis estático ve un fragmento de la relación de llamadas. Como se examina en el contexto de las técnicas esenciales de refactorización , identificar todas las rutas de ejecución a una sección de código determinada, incluidas las condiciones de error poco frecuentes y las ramas de reserva, requiere el tipo de mapeo completo del grafo de llamadas que las herramientas de un solo lenguaje no pueden construir entre diferentes lenguajes.
Lo que realmente requiere un inventario completo de personas que llaman
Un inventario completo de llamadas a una función obsoleta en un sistema empresarial no es un resultado de búsqueda. Es una enumeración estructurada de todas las rutas de ejecución a través de las cuales se puede acceder a la función obsoleta, incluyendo rutas directas, indirectas, entre lenguajes y de despacho dinámico. La creación de esta enumeración requiere varias capacidades que ninguna herramienta estándar proporciona.
Un grafo de llamadas unificado entre diferentes lenguajes. Este grafo debe abarcar todos los lenguajes del sistema. Una llamada desde un procedimiento JCL a un programa COBOL, una llamada desde el programa COBOL a un párrafo de utilidad compartida y una llamada desde un servicio Java al mismo programa COBOL a través de una interfaz de middleware deben ser nodos y aristas en el mismo grafo. La función obsoleta es un nodo en este grafo, y una enumeración de llamadas consiste en recorrer todas las aristas entrantes, directas y transitivas, independientemente del lenguaje de origen.
Recorrido recursivo a través del grafo de llamadas completo. Las funciones que realizan llamadas directas constituyen solo la primera capa. Un inventario completo requiere seguir el grafo de llamadas en sentido ascendente a través de las funciones que realizan llamadas indirectas, las funciones envoltorio y las capas de fachada hasta que el recorrido alcance las funciones que no tienen funciones que las llamen, las cuales son los verdaderos puntos de entrada de las cadenas de llamadas. Cada función en una ruta que termina en la función obsoleta es una función que realiza llamadas en el sentido pertinente: eliminar la función obsoleta interrumpirá todas las rutas que la atraviesen.
Indexación entre repositorios. El grafo de llamadas debe incluir código de todos los repositorios que potencialmente podrían llamar a la función, incluidos aquellos que dependen de ella a través de una biblioteca o paquete compartido. Esto requiere indexar todos los repositorios simultáneamente y resolver las relaciones de importación entre repositorios para conectar las llamadas de un repositorio con las definiciones de otro.
Detección de patrones de invocación indirecta. El análisis debe identificar las llamadas realizadas mediante reflexión, despacho dinámico, punteros a funciones, delegados e invocación basada en cadenas en los archivos de configuración. Esto requiere una detección basada en patrones en lugar de la resolución directa de los límites de las llamadas: encontrar los mecanismos de despacho dinámico en el código y determinar a qué funciones pueden enviar llamadas bajo qué condiciones.
Distinción entre llamadores activos y llamadores solo de prueba o inactivos. No todos los llamadores requieren la misma respuesta. Un llamador que existe solo en un entorno de prueba para la función obsoleta debe eliminarse como parte de la limpieza, no migrarse. Un llamador en código que ha sido identificado como código muerto mediante análisis de uso no es un impedimento para la eliminación de la función. Comprender estas distinciones requiere combinar la enumeración de llamadores con información sobre qué rutas de código están realmente activas. Como se detalla en el examen de la detección de código muerto mediante análisis estático , el código inaccesible y las funciones no utilizadas pueden persistir durante años en sistemas de misión crítica debido a documentación incompleta o incertidumbre sobre dependencias históricas, y el inventario de llamadores para una función obsoleta debe distinguir entre los llamadores que están activos y los que están inactivos.
El proceso de descontinuación y eliminación: un enfoque estructurado
Considerar la eliminación de funciones obsoletas como un evento aislado, en lugar de un proceso estructurado, es la causa de la mayoría de los fallos en la detección de llamadas. El enfoque correcto trata la eliminación como el paso final de un proceso multifásico que comienza mucho antes de que se borre cualquier código.
Fase 1: Marcar y medir
El primer paso es anotar la función como obsoleta utilizando el mecanismo integrado del lenguaje (@Deprecated En Java, [Obsolete] Cª#, #[deprecated] en Rust (o su equivalente apropiado) y estableciendo un recuento base de llamadas. Este recuento base no es el resultado de una sola búsqueda, sino de indexar cada base de código conocida que podría llamar a la función y contar los resultados. El recuento base cumple dos propósitos: cuantifica el alcance de la migración y proporciona una referencia para medir el progreso a medida que se migran las llamadas.
La información de referencia debe organizarse por tipo de persona que llama y ubicación:
| Categoría de la persona que llama | Contar | Prioridad | Propietario |
|---|---|---|---|
| Llamadas directas en el mismo repositorio | N | Alto | Equipo actual |
| Dirija las llamadas a los servicios dependientes. | N | Alto | Propietarios de servicios |
| Llamadas a través de funciones contenedoras | N | Media | Propietarios de envoltorios |
| Llamadas en código generado o de marco de trabajo | N | Media | Equipo de Framework |
| Llamadas en código solo de prueba | N | Bajo | Equipo actual |
| Llamadas en código muerto | N | Solo limpieza | Equipo actual |
Fase 2: Notificar y migrar
Con un inventario completo de llamadas, la migración se convierte en un esfuerzo organizado en lugar de uno reactivo. Cada propietario de llamada recibe una notificación con las ubicaciones de llamada específicas: no "puede que esté llamando a esta función", sino "llama a esta función en la línea 247 de BillingService.java, línea 82 de AccountProcessor.javay en la prueba de integración en la línea 14 de BillingServiceTest.javaEste nivel de especificidad es lo que permite el inventario del emisor de la llamada y lo que las advertencias genéricas de obsolescencia no pueden proporcionar.
Es fundamental proporcionar una ruta de migración junto con la notificación. La obsolescencia debe incluir documentación de la función de reemplazo, una descripción de las diferencias de comportamiento entre las implementaciones antigua y nueva, y, cuando el cambio no sea trivial, un ejemplo de código que muestre el antes y el después. Para quienes utilizan el código de otros equipos, el cronograma de migración debe negociarse explícitamente en lugar de anunciarse unilateralmente, ya que estos equipos tienen sus propias prioridades y compromisos de entrega. Como se analizó en el contexto de la refactorización de bases de datos en sistemas dependientes , la implementación gradual de la nueva estructura antes de la obsolescencia de la antigua es la práctica que evita que los cambios incompatibles se presenten como incidentes inesperados.
Fase 3: Monitorear el número de llamadas.
Entre la medición inicial y la fecha de eliminación prevista, se debe monitorear continuamente el número de llamadas. Cada vez que una llamada migra a la función de reemplazo, el contador disminuye. El punto de eliminación se alcanza cuando el contador de llamadas activas llega a cero (las llamadas de solo prueba y las de código obsoleto pueden eliminarse simultáneamente con la función). El monitoreo continuo requiere ejecutar la enumeración de llamadas según un cronograma como parte del pipeline de CI, en lugar de depender de un inventario único que se vuelve obsoleto a medida que cambia el código.
El sistema de monitorización también detecta las nuevas llamadas añadidas durante el periodo de obsolescencia. En grandes organizaciones, es común que se escriba código nuevo que llame a una función obsoleta durante la ventana de migración, ya sea porque el desarrollador desconocía la obsolescencia, porque una revisión de código no la detectó o porque un generador de código automatizado produce código que llama a la función obsoleta. La detección de llamadas a nivel de integración continua para la función obsoleta, configurada para fallar en los nuevos puntos de llamada, evita que el número de llamadas aumente mientras la migración está en curso.
Fase 4: Verificar la integridad antes de la eliminación.
Justo antes de eliminar la función, se debe ejecutar una última vez la enumeración de llamadas en todo el código fuente conocido. Esta verificación final sirve como medida de seguridad: confirma que el número de llamadas activas ha llegado a cero e identifica cualquier adición tardía que no haya sido detectada por la monitorización de CI. En este punto, el inventario también debe verificar la ausencia de llamadas dinámicas: archivos de configuración que hacen referencia a la función mediante cadenas de texto, registros basados en reflexión y cualquier otro mecanismo de invocación indirecta que se haya identificado durante el análisis inicial.
La verificación debe extenderse al gráfico de dependencias de cualquier biblioteca o paquete compartido que exponga la función obsoleta. Si la función forma parte de una API pública utilizada por terceros, el cronograma de eliminación debe tener en cuenta a los usuarios externos que podrían no ser accesibles mediante el análisis interno del código. Para sistemas internos, la verificación abarca todo el código fuente indexado. Para API publicadas públicamente, la verificación abarca el conjunto conocido de usuarios, además de un período de transición definido durante el cual los usuarios externos deben migrar.
Cómo funciona de manera diferente la detección de llamadas en entornos heredados y de mainframe
Los desafíos descritos anteriormente se aplican a cualquier sistema de software de gran tamaño, pero son particularmente acuciantes en entornos de mainframe y sistemas heredados, ya que las relaciones de llamadas en esos entornos se expresan a través de mecanismos que las herramientas modernas de detección de llamadas no fueron diseñadas para analizar.
En entornos COBOL, las funciones se invocan mediante sentencias CALL que pueden hacer referencia al destino mediante una cadena literal, un elemento de datos que contiene el nombre del programa o un puntero a procedimiento. El caso de la cadena literal se resuelve mediante análisis estático; el caso del elemento de datos requiere análisis de flujo de datos para determinar qué valor podría contener dicho elemento en el momento de la llamada; y el caso del puntero a procedimiento requiere rastrear cómo se asigna el puntero. Cada uno de estos mecanismos de llamada se presenta de forma diferente en el código fuente y requiere un análisis distinto para su resolución.
En entornos JCL, los programas se invocan por su nombre en las sentencias EXEC PGM=. El nombre del programa es una cadena que se corresponde con un módulo compilado en una biblioteca de carga. Para rastrear las llamadas a un programa COBOL a través de JCL, es necesario analizar el JCL para extraer los nombres de los programas, asignar esos nombres a los programas COBOL compilados que los implementan y determinar qué párrafos COBOL dentro de esos programas llaman a la utilidad obsoleta. Esta resolución de varios pasos está completamente fuera del alcance de un analizador COBOL o un analizador JCL que trabajen de forma aislada.
Los copybooks compartidos son un caso particularmente importante en entornos COBOL. Un párrafo obsoleto definido en un copybook puede incluirse en muchos programas mediante sentencias COPY. El párrafo no se duplica físicamente en cada programa; se incluye en tiempo de compilación. Un análisis que cuente las ocurrencias del nombre del párrafo en los archivos fuente sin resolver las inclusiones en los copybooks resultará en un conteo excesivo (al encontrar la definición del párrafo en el propio copybook) y un conteo insuficiente (al pasar por alto el hecho de que cada programa que incluye el copybook tiene acceso al párrafo). La correcta identificación del llamador requiere comprender qué programas incluyen qué copybooks y qué párrafos dentro de esos copybooks llaman realmente. La relación entre las referencias codificadas y sus consumidores posteriores ilustra por qué es esencial resolver estas relaciones de invocación a nivel de programa antes de cualquier cambio estructural: lo que parece ser una simple referencia de cadena puede ser el único mecanismo a través del cual docenas de programas acceden a funcionalidades críticas.
Cómo SMART TS XL Crea el inventario completo de personas que llaman.
SMART TS XL Construye un grafo de llamadas unificado que abarca todos los lenguajes, plataformas y repositorios del entorno indexado. Los programas COBOL, los flujos de trabajo JCL, los servicios Java, las aplicaciones .NET, los procedimientos almacenados SQL, los scripts Python y otros artefactos fuente se analizan mediante un análisis específico de cada lenguaje para generar un grafo de referencias cruzadas común. Cada función, párrafo, procedimiento, método y unidad de programa es un nodo en dicho grafo. Cada relación de llamada, ya sea una instrucción CALL de COBOL, una invocación de método Java, un programa EXEC de JCL o un programa EXEC de SQL, es una arista tipificada. El grafo representa la topología de llamadas completa del sistema, no una vista parcial por lenguaje.
Cuando una función está marcada para su eliminación, SMART TS XLLa enumeración de llamadas recorre el grafo de llamadas desde el nodo de la función objetivo, recopilando cada llamador en cada nivel de la jerarquía de llamadas. El recorrido es recursivo, siguiendo el grafo a través de funciones de envoltura, capas de fachada y utilidades intermedias hasta llegar a funciones sin llamadores, que representan los verdaderos puntos de entrada de las cadenas de llamadas. Los resultados se organizan por lenguaje, por repositorio, por tipo de llamador y por profundidad de llamada, lo que proporciona al equipo un inventario estructurado que separa los llamadores directos de los indirectos y los llamadores activos de los de código muerto.
La capacidad de análisis de impacto de la plataforma amplía esto a un informe estructurado de impacto de cambios: no solo qué funciones llaman a la función obsoleta, sino también qué programas, servicios, trabajos por lotes y procedimientos JCL se ven afectados en cada nivel de la cadena de dependencias. Este informe es el artefacto que hace que el proceso de obsolescencia a eliminación sea viable: nombra a los responsables, identifica las ubicaciones de llamada específicas y cuantifica el alcance de la migración necesaria antes de que la eliminación pueda proceder de forma segura. Como se examina en detalle en el análisis de impacto para la gestión de cambios empresariales , la capacidad de enumerar los componentes afectados antes de realizar un cambio estructural es el requisito fundamental para el funcionamiento seguro de sistemas empresariales complejos e interconectados.
SMART TS XL También respalda la fase de monitoreo continuo del proceso de obsolescencia. Dado que el gráfico de referencias cruzadas se actualiza constantemente a medida que se indexan los cambios en el código fuente, el número de llamadas a una función obsoleta siempre está actualizado. La integración con la canalización de CI permite que las comprobaciones automatizadas fallen en las nuevas llamadas a funciones obsoletas, lo que garantiza la disciplina de migración en el momento en que se introduce el nuevo código, en lugar de descubrir las infracciones a posteriori. Esta combinación de enumeración inicial, guía de migración y monitoreo continuo abarca todo el ciclo de vida de una función obsoleta, desde la anotación hasta su eliminación segura.
Eliminación de funciones sin remordimientos
La diferencia entre la eliminación de una función sin problemas y una que provoca fallos en producción radica casi siempre en la exhaustividad del descubrimiento de las funciones que la llaman. La eliminación en sí es sencilla: basta con borrar la definición e implementarla. El trabajo reside en la preparación, y esta solo será efectiva si se basa en un inventario completo de las funciones que la llaman.
En sistemas donde el grafo de llamadas es simple, monolingüe y se encuentra dentro de un único repositorio, la jerarquía de llamadas del IDE y las advertencias del compilador son suficientes. En sistemas donde el grafo de llamadas abarca múltiples lenguajes, repositorios, plataformas y, potencialmente, varias décadas de código, estas herramientas cubren solo una pequeña e incalculable fracción de la superficie real de llamadas. La brecha entre lo que devuelven y lo que realmente llama a la función es donde se originan los fallos en producción.
La enumeración de llamadas entre lenguajes y repositorios, diseñada específicamente para este fin, no es una mejora del flujo de trabajo del desarrollador para la eliminación de funciones. Es un requisito previo para ejecutar dicho flujo de trabajo de forma segura en cualquier sistema lo suficientemente complejo como para haber acumulado el tipo de relaciones de llamadas entre sistemas que suelen tener las funciones obsoletas en las bases de código empresariales. Cada función obsoleta que se elimina sin un inventario completo de llamadas es una versión que contiene un número desconocido de fallos en tiempo de ejecución que esperan la ruta de ejecución específica que alcanza la definición faltante. Eliminar esa incertidumbre es la finalidad del descubrimiento estructurado de llamadas.


