Removing a deprecated function from a codebase is conceptually one of the simplest things a developer can do. Delete the definition, confirm nothing uses it, commit. In practice, for any function that has existed long enough to be deprecated, the step “confirm nothing uses it” is where the process breaks down. The function may be called from code that was written years ago by someone no longer on the team, in a repository that receives changes infrequently, by a language or framework the current team does not own. It may be invoked indirectly through a wrapper, through reflection, or through a runtime dispatch mechanism that does not appear in any static call graph. It may be referenced in generated code, in test scaffolding, or in a configuration file that triggers it by name. The developer who marks it deprecated and the developer who eventually removes it may have no way of knowing any of this without a tool capable of building a complete, cross-repository caller inventory.
Find Every Caller Before You Remove Anything
SMART TS XL builds a cross-language call graph that identifies every caller of any function before you make a change.
Klikněte zdeThe cost of getting this wrong is immediate and concrete. A function removed with incomplete caller discovery causes runtime failures in the systems that still depend on it. In a monolith with a single deployment, the failure surface is bounded. In a distributed system with multiple services, each deployed independently, the failures cascade: the service that provided the function is updated, the consumers are not, and breakage appears at runtime in production in systems that may be owned by different teams. In a mainframe environment where COBOL programs call shared utility paragraphs by name, the failure may not manifest until a specific batch job runs, which could be weekly or monthly, making the missing reference invisible through normal testing cycles. As examined in the broader context of správa zastaralého kódu, the risks compound over time: deprecated code that is incompletely removed is more dangerous than deprecated code that is left in place, because the removal creates the illusion of completeness while the remaining callers continue running against a definition that no longer exists.
This article is a practical guide to caller discovery before function removal: what a complete caller inventory requires, why the tools developers reach for first are structurally insufficient, how different types of call relationships require different analysis approaches, and what genuine cross-system caller enumeration looks like in enterprise-scale codebases that mix languages, platforms, and repositories.
Why Caller Discovery Is Harder Than It Looks
The surface-level version of caller discovery is familiar to every developer: right-click a function name in an IDE, select “Find All References” or “Show Call Hierarchy,” and review the results. This works reliably within the scope of a single project loaded in a single IDE instance. The moment the codebase extends beyond that scope, the results become incomplete in ways that are not visible in the output. The IDE does not indicate which callers it did not find because it did not index the repositories that contain them. The developer sees a result set that appears complete and proceeds accordingly.
This is the structural problem with caller discovery at scale: the tools that developers use most fluently are bounded by their indexing scope, and in large, distributed, multi-language systems, that scope covers a fraction of the places where a given function might be called. The developer’s confidence in the completeness of the results is inversely proportional to the actual completeness of the search. In a small, single-language codebase, IDE call hierarchy is genuinely reliable. In an enterprise system spanning multiple repositories, languages, and deployment environments, it is systematically misleading. As analyzed in the context of code entropy and refactoring risk, legacy modules may depend on deprecated interfaces while newer services still call routines originally designed for earlier environments, and these cross-system call relationships are precisely the ones that IDE-bounded search cannot see.
Understanding the specific reasons caller discovery fails requires examining each major call type: direct calls, indirect calls, cross-language invocations, and dynamic dispatch. Each fails for different reasons and requires different analysis techniques to resolve correctly.
Direct Calls Across Repository Boundaries
Direct calls are the simplest call type: one function explicitly calling another by name. Within a single repository, IDEs handle these reliably. Across repository boundaries, the analysis fails because the IDE’s indexing does not span the boundary. If repository A defines a shared utility function and repositories B, C, and D each import and call it, the IDE for any one of these repositories sees only the calls within its own indexed scope.
This multi-repository calling pattern is the norm rather than the exception in microservice architectures, where shared libraries are published as packages and consumed across dozens of services. The library maintainer who deprecates a function in the shared package needs to know which consuming services still call it. Their IDE knows nothing about the consumers. The package manager knows which services depend on the package, but not which specific function within the package each service calls. Mapping from “this service uses version X of this package” to “this service calls this specific deprecated function” requires indexing every consumer’s source code and resolving the call to the specific function definition.
Indirect Calls: Wrappers, Delegates, and Facades
A function may not be called directly by its consumers. It may be called through a wrapper function that provides additional logging, error handling, or parameter transformation. It may be assigned to a delegate or a function pointer and invoked through the delegate. It may be registered in a service registry or a plugin framework and called by name through a dispatch mechanism. In each of these cases, a direct search for calls to the deprecated function returns an incomplete result, because the actual callers call the wrapper or the dispatcher, not the deprecated function itself.
Wrapper-mediated invocation is particularly common in large codebases where cross-cutting concerns like logging, authorization, and retry logic are layered around core functions through wrapper patterns. A deprecated function wrapped by a logging utility is effectively called by every caller of the wrapper, not by any code that contains the deprecated function’s name. Identifying those callers requires tracing through the wrapper: the logging utility calls the deprecated function, and every caller of the logging utility is therefore an indirect caller of the deprecated function. This recursive call graph traversal is what distinguishes a thorough caller inventory from a surface-level reference search.
Consider a representative Java example where a deprecated method is accessed through a delegation layer:
Jáva
// 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
}
A “Find All References” search for calculateLegacyFee Vrací computeFee as the only caller. It does not return processMonthlyBilling, which is the genuine consumer of the deprecated behavior. A complete caller inventory requires traversing the call graph upstream through computeFee to identify every path that ultimately invokes the deprecated method.
Cross-Language Invocations
Cross-language calls are the category where standard caller discovery tools fail most completely. When a Java service invokes a COBOL program by name through a middleware layer, when a Python script calls a stored procedure that wraps a deprecated function, or when a JCL job invokes a program by its PROGNAME that internally calls a deprecated paragraph, none of these relationships appear in any single language’s call graph. Each language’s tooling sees only its own side of the call.
In mainframe environments, cross-language calls are structural and pervasive. A JCL job stream specifies the name of the COBOL program it executes. The COBOL program calls paragraphs and subprograms by name. Utility paragraphs defined in copy libraries are shared across many programs. When a paragraph in a copy library is deprecated, finding all callers requires understanding the COBOL call relationships (which programs include the copy library and which call the paragraph), the JCL invocation relationships (which jobs invoke those programs), and any cross-language interfaces (Java or SQL that interact with those programs). No single tool spans all of these relationships. As examined in the analysis of static analysis on legacy systems, static analysis tools built for modern environments cannot see the full picture of how legacy programs are triggered, called, and interconnected when the call relationships span JCL, COBOL, and cross-system interfaces simultaneously.
Dynamic Dispatch and Reflection
Some callers invoke a function not by its literal name in the source code but through a mechanism that resolves the function at runtime: reflection in Java or .NET, getattr/__call__ in Python, late binding in COBOL through CALL identifier, dynamic dispatch through polymorphism, or invocation-by-string in plugin frameworks and configuration-driven systems. These callers do not contain the deprecated function’s name in any form that static analysis can reliably detect.
A configuration file that specifies a function name as a string, loaded at runtime and used to invoke the function through reflection, is a caller that appears nowhere in any source code analysis. A plugin framework that discovers and invokes registered handlers by interface is a caller that appears in the call graph only as a call to the dispatch mechanism, not as a call to any specific handler. Identifying these callers requires a combination of static analysis to find dynamic dispatch patterns, runtime tracing to observe actual invocations, and manual inspection of configuration and registration logic that determines which functions are dispatched to. As discussed in the examination of static analysis on obfuscated and generated code, when execution paths are not expressed directly in source code, static analysis must reconstruct the likely paths from structural patterns rather than direct textual references, and those reconstructions require language-aware analysis of the dispatch mechanism itself.
The Tools Developers Reach For First and Where They Stop
There is a predictable sequence of tools that developers use when attempting caller discovery, and an equally predictable point at which each one stops providing reliable results. Understanding this sequence is important because each tool’s output looks complete even when it is not.
IDE Call Hierarchy: Reliable Within One Project
IDE call hierarchy features are the most natural first step. IntelliJ IDEA, Visual Studio, VS Code, and Eclipse all provide some form of “find all callers” or “show call hierarchy” that recursively enumerates the callers of a selected function, within the indexed scope of the current project or workspace. For a function that is used exclusively within one repository and one language, these features are accurate and sufficient.
The limitation is explicit in the scoping: “within the indexed scope.” Callers in other repositories, in other language runtimes, or in services that depend on this code through a package manager rather than through direct project reference are outside the scope. The IDE does not indicate what it did not search. The developer receives a result set and has no visibility into how many additional repositories exist that were not indexed, how many of those use the deprecated function, or whether the “zero callers” result means genuinely zero callers or “zero callers within the fraction of the system this tool can see.”
grep and Text Search: Broad but Structurally Blind
When IDE search is suspected to be incomplete, the next step is usually text search: grep across the available source directories, or platform search through GitHub or GitLab code search. This widens the scope considerably and does find callers in other repositories if those repositories are accessible. The structural problem is that text search finds strings, not calls. It returns every occurrence of the function name, including comments that mention the function, documentation strings, log messages that name the function for debugging purposes, and string literals that contain the function name but do not call it. It also misses callers where the function name differs from the search string: callers through aliases, through partially matched names in COBOL where names may be abbreviated, or through dynamic invocation where the name is assembled at runtime.
The result set from text search requires manual filtering to determine which occurrences are actual call sites, which are documentation references, and which are false positives from string collisions. In a large system, this filtering is itself a significant effort, and the filtering process cannot verify completeness: if a caller was missed because it uses a different name, the filtered result set contains no indication of the omission.
Compiler Warnings and @Deprecated Anotace
Modern languages and toolchains provide deprecation annotation mechanisms that generate warnings when deprecated functions are called. Java’s @Deprecated annotation combined with -Xlint:deprecation produces compile-time warnings at call sites. C#’s [Obsolete] attribute generates warnings at build time. Go’s convention of naming deprecated functions and documenting them in godoc does not produce warnings automatically. These mechanisms are valuable but limited in a specific way: they only work for callers that compile against the same codebase in which the deprecation is annotated.
A caller that uses an older version of the library, pre-dating the @Deprecated annotation, receives no warning. A caller that uses a binary artifact rather than compiling from source receives no warning. A caller in a different language that calls through a cross-language interface receives no warning. And crucially, the warnings produced during compilation are local to the compiler’s view: they warn about the calls it sees, not about the calls in other repositories that are compiled separately. Using compiler warnings as the sole mechanism for caller discovery in a multi-service system misses every caller that compiles independently, which is the normal condition in microservice architectures.
Static Analysis Tools: Better, but Scope-Bounded
Purpose-built static analysis tools provide more accurate caller enumeration than IDEs for their target language and can often cross repository boundaries if configured to index multiple codebases. They build proper call graphs rather than relying on text matching, handle aliasing and indirect calls better than IDE search, and can be run in CI pipelines to catch new callers as they are added. They are the most capable single-language approach available.
The limitation is the same scope boundary that constrains IDEs, but now at the tool level: a Java static analysis tool does not index COBOL programs, a COBOL analyzer does not index Java services, and neither indexes JCL job streams. In a system where the deprecated function is a COBOL utility called from COBOL programs that are invoked from JCL jobs and whose data output is consumed by Java services, each static analysis tool sees one fragment of the call relationship. As examined in the context of základní techniky refaktoringu, identifying all execution paths to a given section of code, including rare error conditions and fallback branches, requires the kind of complete call graph mapping that single-language tools cannot construct across language boundaries.
What a Complete Caller Inventory Actually Requires
A complete caller inventory for a deprecated function in an enterprise system is not a search result. It is a structured enumeration of every execution path through which the deprecated function can be reached, including paths that are direct, indirect, cross-language, and dynamically dispatched. Building this enumeration requires several capabilities that no single standard tool provides.
A unified cross-language call graph. The call graph must span every language in the system. A call from a JCL procedure to a COBOL program, a call from the COBOL program to a shared utility paragraph, and a call from a Java service to the same COBOL program through a middleware interface must all be nodes and edges in the same graph. The deprecated function is a node in this graph, and a caller enumeration is a traversal of all inbound edges, direct and transitive, regardless of which language they originate in.
Recursive traversal through the full call graph. Direct callers are only the first layer. A complete inventory requires following the call graph upstream through indirect callers, wrapper functions, and facade layers until the traversal reaches functions that have no callers of their own, which are the true entry points of the call chains. Every function in a path that ends at the deprecated function is a caller in the relevant sense: removing the deprecated function will break every path that passes through it.
Cross-repository indexing. The call graph must include code from every repository that could potentially call the function, including repositories that depend on the function through a shared library or package. This requires indexing all repositories simultaneously and resolving cross-repository import relationships to connect calls in one repository to definitions in another.
Detection of indirect invocation patterns. The analysis must identify calls made through reflection, dynamic dispatch, function pointers, delegates, and string-based invocation in configuration files. These require pattern-based detection rather than direct call edge resolution: finding the dynamic dispatch mechanisms in the code and determining which functions they can dispatch to under which conditions.
Distinction between active callers and test-only or dead callers. Not all callers require the same response. A caller that exists only in a test fixture for the deprecated function itself needs to be removed as part of the cleanup, not migrated. A caller in code that has itself been identified as dead code through usage analysis is not a blocker for function removal. Understanding these distinctions requires combining the caller enumeration with information about which code paths are actually active. As detailed in the examination of dead code detection through static analysis, unreachable code and unused functions can persist for years in mission-critical systems due to incomplete documentation or uncertainty about historical dependencies, and the caller inventory for a deprecated function must distinguish between callers that are themselves live and callers that are themselves dead.
The Deprecation-to-Removal Process: A Structured Approach
Treating deprecated function removal as a single event rather than a structured process is the source of most caller-discovery failures. The correct approach treats removal as the final step in a multi-phase process that begins long before any code is deleted.
Phase 1: Mark and Measure
The first step is annotating the function as deprecated using the language’s built-in mechanism (@Deprecated v Javě, [Obsolete] in C#, #[deprecated] in Rust, or the appropriate equivalent) and establishing a baseline caller count. This baseline is not the result of any single search; it is the result of indexing every known codebase that might call the function and counting the results. The baseline serves two purposes: it quantifies the migration scope, and it provides a reference against which progress can be measured as callers are migrated.
The baseline should be organized by caller type and location:
| Caller category | Počítat | Priorita | Majitel |
|---|---|---|---|
| Direct callers in same repository | N | Vysoký | Současný tým |
| Direct callers in dependent services | N | Vysoký | Service owners |
| Callers through wrapper functions | N | Střední | Wrapper owners |
| Callers in generated or framework code | N | Střední | Framework team |
| Callers in test-only code | N | Nízké | Současný tým |
| Callers in dead code | N | Cleanup only | Současný tým |
Phase 2: Notify and Migrate
With a complete caller inventory, migration becomes an organized effort rather than a reactive one. Each caller owner is notified with the specific call locations: not “you may be calling this function” but “you call this function at line 247 of BillingService.java, line 82 of AccountProcessor.java, and in the integration test at line 14 of BillingServiceTest.java.” This level of specificity is what the caller inventory makes possible and what generic deprecation warnings cannot provide.
Providing a migration path alongside the notification is essential. The deprecation should include documentation of the replacement function, a description of any behavioral differences between the old and new implementations, and where the change is non-trivial, a code example showing the before and after. For callers in other teams’ codebases, the timeline for migration should be negotiated explicitly rather than announced unilaterally, because those teams have their own priorities and delivery commitments. As explored in the context of database refactoring across dependent systems, phasing in consumers of the new structure before deprecating the old one is the discipline that prevents breaking changes from appearing as unexpected incidents.
Phase 3: Monitor the Caller Count
Between the baseline measurement and the planned removal date, the caller count should be monitored continuously. Each time a caller migrates to the replacement function, the count decreases. The removal gate is reached when the count reaches zero for active callers (test-only and dead-code callers may be removed concurrently with the function itself). Continuous monitoring requires running the caller enumeration on a schedule as part of the CI pipeline, not relying on a one-time inventory that becomes stale as code changes.
The monitoring also catches new callers added during the deprecation period. In large organizations, it is common for new code to be written that calls a deprecated function during the migration window, either because the developer was unaware of the deprecation, because a code review missed it, or because an automated code generator produces code that calls the deprecated function. CI-level caller detection for the deprecated function, configured to fail on new call sites, prevents the caller count from growing while the migration is in progress.
Phase 4: Verify Completeness Before Removal
Immediately before removing the function, the caller enumeration should be run one final time against the full scope of all known codebases. This final check serves as the safety gate: it confirms that the caller count has reached zero for active callers and identifies any late-stage additions that were not caught by CI monitoring. At this point, the inventory should also verify the absence of dynamic callers: configuration files that reference the function by string, reflection-based registrations, and any other indirect invocation mechanisms that were identified during the initial analysis.
The verification should extend to the dependency graph of any shared libraries or packages that expose the deprecated function. If the function is part of a public API consumed by external parties, the removal timeline must account for external consumers who may not be reachable through internal code analysis. For internal systems, the verification covers every indexed codebase. For publicly published APIs, the verification covers the known set of consumers plus a defined sunset period during which external consumers must migrate.
How Caller Discovery Works Differently in Legacy and Mainframe Environments
The challenges described above apply to any large software system, but they are particularly acute in mainframe and legacy environments because the call relationships in those environments are expressed through mechanisms that modern caller-discovery tools were not designed to analyze.
In COBOL environments, functions are called through CALL statements that may reference the target by a literal string, by a data item that contains the program name, or by a procedure pointer. The literal-string case is resolvable through static analysis; the data-item case requires data flow analysis to determine what value the data item might hold at the point of the call; and the procedure pointer case requires tracking how the pointer is assigned. Each of these call mechanisms appears differently in the source code and requires different analysis to resolve.
In JCL environments, programs are invoked by name in EXEC PGM= statements. The program name is a string that maps to a compiled module in a load library. Tracing callers of a COBOL program through JCL requires parsing the JCL to extract program names, mapping those names to the compiled COBOL programs that implement them, and resolving which COBOL paragraphs within those programs call the deprecated utility. This multi-step resolution is entirely outside the scope of either a COBOL analyzer or a JCL analyzer working in isolation.
Shared copybooks are a particularly important case in COBOL environments. A deprecated paragraph defined in a copybook may be included in many programs through COPY statements. The paragraph is not physically duplicated in each program; it is included at compile time. An analysis that counts occurrences of the paragraph name in source files without resolving copybook inclusions will both overcount (finding the paragraph definition in the copybook itself) and undercount (missing the fact that every program that includes the copybook has access to the paragraph). Correct caller discovery requires understanding which programs include which copybooks and which paragraphs within those copybooks they actually call. The relationship between hardcoded references and their downstream consumers illustrates why resolving these program-level invocation relationships is essential before any structural change: what appears to be a simple string reference may be the sole mechanism through which dozens of programs reach critical functionality.
Jak SMART TS XL Builds the Complete Caller Inventory
SMART TS XL constructs a unified call graph across every language, platform, and repository in the indexed environment. COBOL programs, JCL job streams, Java services, .NET applications, SQL stored procedures, Python scripts, and other source artifacts are all parsed using language-specific analysis into a common cross-reference graph. Each function, paragraph, procedure, method, and program unit is a node in that graph. Each call relationship, whether a COBOL CALL statement, a Java method invocation, a JCL EXEC PGM, or a SQL EXEC, is a typed edge. The graph represents the complete call topology of the system, not a per-language partial view.
When a function is marked for removal, SMART TS XL’s caller enumeration traverses the call graph inbound from the target function node, collecting every caller at every level of the call hierarchy. The traversal is recursive, following the graph through wrapper functions, facade layers, and intermediate utilities until it reaches functions with no callers, which represent the true entry points of the call chains. Results are organized by language, by repository, by caller type, and by call depth, giving the team a structured inventory that separates direct callers from indirect callers and active callers from dead-code callers.
The platform’s impact analysis capability extends this into a structured change-impact report: not just which functions call the deprecated function, but which programs, services, batch jobs, and JCL procedures are affected at every level of the dependency chain. This report is the artifact that makes the deprecation-to-removal process actionable: it names the owners, identifies the specific call locations, and quantifies the scope of migration required before removal can proceed safely. As examined in the detail of impact analysis for enterprise change management, the ability to enumerate affected components before making a structural change is the foundational requirement for safe operation of complex, interconnected enterprise systems.
SMART TS XL also supports the ongoing monitoring phase of the deprecation process. Because the cross-reference graph is continuously updated as source code changes are indexed, the caller count for a deprecated function is always current. CI pipeline integration allows automated checks to fail on new calls to deprecated functions, enforcing the migration discipline at the point where new code is introduced rather than discovering violations after the fact. This combination of initial enumeration, migration guidance, and continuous monitoring covers the full lifecycle of a deprecated function from annotation to safe removal.
Function Removal Without Regret
The difference between a function removal that goes smoothly and one that triggers production failures is almost always a difference in caller discovery completeness. The removal itself is trivial: delete the definition and deploy. The preparation is where the work is, and the preparation is only as good as the caller inventory it is based on.
In systems where the call graph is shallow, monolingual, and contained within a single repository, IDE call hierarchy and compiler warnings are adequate preparation. In systems where the call graph spans multiple languages, multiple repositories, multiple platforms, and potentially multiple decades of code, those tools cover a small and unknowable fraction of the actual caller surface. The gap between what they return and what actually calls the function is where production failures originate.
Purpose-built cross-language, cross-repository caller enumeration is not a refinement of the developer workflow for function removal. It is a prerequisite for executing that workflow safely in any system complex enough to have accumulated the kind of cross-system call relationships that deprecated functions in enterprise codebases routinely carry. Every deprecated function that is removed without a complete caller inventory is a release that contains an unknown number of runtime failures waiting for the specific execution path that reaches the missing definition. Eliminating that unknown is what structured caller discovery is for.