All posts
Engineering / AI Research

Finding dead code that is actually safe to delete

Abstract dead code detection visualization with isolated disconnected nodes on dark background

There is a class of code deletion that feels safe but is not. A static analysis tool finds a function that has zero call sites in the repository. You trust the tool, delete the function, and three weeks later something breaks in a way nobody predicted. The function was being called by a third-party library via reflection. Or by name in a configuration file. Or it was the implementation of an interface method that a framework calls based on a naming convention.

This is the core challenge Pylon-Refactor faces when identifying dead code candidates for removal. Static reachability is a necessary condition for safe deletion, but it is not sufficient. The question is always: are there invocation paths that static analysis cannot see?

The limits of static reachability analysis

Static reachability analysis answers the question: starting from a set of entry points (main functions, public API handlers, test entry points), can I reach this function by following explicit call edges in the code? A function that is not reachable from any entry point through explicit calls is a candidate for dead code.

The word "explicit" is doing a lot of work in that definition. Modern codebases have many forms of non-explicit invocation:

Reflection-based dispatch. Python's getattr(obj, method_name)(), Java's Method.invoke(), and JavaScript's bracket notation obj[key]() all call functions by name at runtime. The specific name called may come from a database, a config file, or user input. Static analysis cannot predict what method_name will be at runtime.

Convention-based frameworks. Django's URL routing looks for view functions by name in urls.py. Spring's dependency injection calls bean factory methods by naming convention. Pytest discovers test functions by the test_ prefix. Functions that are only ever called via framework convention will show zero explicit call sites in static analysis.

Serialized references. A function name stored in a database, a queue message, or a webhook payload is a runtime reference. The code that receives that message and dispatches the call may be generic, and the specific target function has no static call site.

Interface implementations. A class that implements an interface method may show no direct call sites for that method if the callers only know about the interface type. The implementation is reached via interface dispatch, which a simple call graph analysis may not trace through correctly.

The four-stage safety check

Before Pylon-Refactor proposes a deletion, a candidate dead function goes through four checks, applied in order from cheapest to most expensive.

Stage 1: String reference search. We search the entire repository for occurrences of the function name as a string literal. Any occurrence in a configuration file, a YAML file, a JSON file, a string constant, or a comment that looks like a docstring reference is a flag. This is cheap and catches a lot of convention-based framework usage. A function called handle_payment_webhook that appears as a string in a Django urls.py is not dead, even if no Python file imports it by name.

Stage 2: Reflection pattern detection. We scan the codebase for common reflection patterns and check whether the function name could plausibly be constructed by any of them. This is heuristic, not exhaustive. We look for patterns like getattr(obj, name) where name comes from a variable, and trace what values that variable could hold. If the value comes from a fixed set of strings, we check whether our candidate function is in that set. If the value is dynamic, we conservatively flag the function as potentially reached via reflection and do not propose deletion.

Stage 3: Interface and override check. For object-oriented codebases, we check whether the function is an implementation of an interface method, an abstract method override, or a method that matches a naming convention for event handlers or lifecycle hooks. A method named on_message_received in a class that inherits from a framework base class is a lifecycle hook, even if no Python call site calls it explicitly.

Stage 4: Runtime call graph augmentation. If you have profiling data or runtime trace data from your production or staging environment, we can augment the static call graph with observed runtime calls. A function that has never appeared in any execution trace over a rolling 90-day window (configurable) is a stronger dead code candidate than one we are evaluating purely statically. We support ingestion of OpenTelemetry trace data for this purpose. If runtime data is available, we show the last-seen timestamp for each dead code candidate alongside the static analysis result.

Confidence levels and what they mean

Pylon-Refactor assigns a confidence level to each dead code candidate, expressed as a category rather than a percentage: Safe to delete, Likely safe with review, and Flagged for human judgment.

"Safe to delete" means: the function has no string references, no reflection pattern matches, no interface implementation characteristics, and either runtime data shows zero calls or no runtime data was available but the static analysis is thorough. We open a PR proposing the deletion and the test run confirms nothing breaks. This category is usually pure utility functions that were once used and have been replaced.

"Likely safe with review" means: the function passed all four stages, but there is some ambiguity. Maybe the function name is common enough (like validate or process) that a string search returns false positives. Maybe the function is in a module that uses reflection patterns elsewhere, even if this specific function is not demonstrably reached that way. We open a PR with the proposed deletion and flag it prominently for reviewer attention. We do not auto-approve these.

"Flagged for human judgment" means we found evidence that the function might be reached through a non-static path and we cannot determine with confidence that it is safe to delete. We report these in the refactoring job summary without opening a deletion PR. The developer can review the evidence and make a call.

The deletion PR itself

When we do open a dead code deletion PR, we take a conservative approach to what goes in the diff. We delete the function and any imports that become unused as a result. We do not reorganize the file, rename nearby variables, or make any changes beyond the deletion and its direct cleanup. A reviewer looking at a dead code deletion PR should be able to see exactly what was removed and nothing else. Mixing dead code removal with other refactoring makes it harder to verify that the deletion is safe.

The PR body includes the full evidence record from the four-stage check: which patterns were searched, what the string reference search returned, whether runtime data was used and what it showed. If a reviewer wants to second-guess our conclusion, the evidence is there to evaluate.

We also add a comment to each deleted function in the git commit message: "Removed [function name]: zero static call sites, no string references found, last seen in runtime trace: [date] or [never observed]." This makes the deletion auditable in git history for the team member who asks "why did we delete this" six months later.

A concrete case where we stopped

In a Java analytics service, Pylon-Refactor identified a class method named computeLegacyScore with no direct call sites in the repository. Stage 1 string search found the string "computeLegacyScore" in one place: a handler_config.json file that configured which computation method to invoke for a specific report type. The method was being selected by name from the config and called via reflection at runtime.

Without Stage 1, this would have appeared as a straightforward deletion candidate. With it, the method was correctly flagged as "Flagged for human judgment," no deletion PR was opened, and the developer who reviewed the report understood the config-based dispatch pattern and added a comment to the method explaining why it was intentionally kept despite having no static call sites.

That is the outcome we want: not an automated deletion, but an informed human decision with better evidence than a static analysis tool alone would provide.