Finding an unused_element warning is easy. Deciding whether a file is safe to delete is not.
Flutter applications have several legitimate ways to use code without an obvious direct reference: generated registries, route tables, dependency injection, conditional imports, flavors, platform channels, annotations, test-only helpers, and public package exports. A search that returns zero references is useful evidence, but it is not a verdict.
That distinction led me to build flutter-dead-code-audit, an open-source Agent Skill for auditing unused Dart declarations and potentially orphaned .dart files through a review-first workflow.
Repository: github.com/LucasMenezeslc/flutter-dead-code-audit
The analyzer answers only part of the question
The Dart analyzer is excellent at detecting local issues such as:
- unused imports;
- unused private fields and methods;
- unreachable branches;
- unused local variables;
- redundant expressions.
But repository cleanup also involves broader questions:
- Is this file reachable from a supported entrypoint?
- Is the symbol referenced through a barrel export?
- Is it registered by dependency injection?
- Is it loaded only for one platform or flavor?
- Is it a public API consumed outside the repository?
- Is it generated or owned by a generator?
- Is it intentionally retained for compatibility or rollback?
A tool that ignores these questions may produce a convincing report and still recommend the wrong deletion.
Write-only state is a good example
Consider a field that is assigned in several lifecycle callbacks but never read:
bool _hasPlayed = false;
void onStarted() {
_hasPlayed = true;
}
void onStopped() {
_hasPlayed = false;
}
Mechanically, the field is write-only. Removing the declaration alone is not enough; every assignment must also be removed for the code to compile.
However, there is a more important product question: was the field abandoned, or was a missing read supposed to control replay, analytics, persistence, or UI state?
The correct finding is therefore not automatically “safe cleanup.” It may be:
The field is write-only in the current code, but product intent is unresolved.
That is why the workflow separates mechanical evidence from human decisions.
A review-first workflow
The skill uses four stages:
1. Audit
Audit mode is read-only. The bundled scripts combine:
- machine-readable Dart analyzer diagnostics;
- deterministic import, export, and
partreachability; - repository-wide symbol and string searches;
- checks for routes, DI, code generation, platforms, flavors, tests, and public APIs.
The result is a list of candidates, not an automatic deletion list.
2. Review
Candidates are presented in small groups. Each receives one of three decisions:
-
remove— evidence and context support removal; -
retain— keep intentionally and record the reason; -
defer— more evidence or team context is required.
Retained code remains visible in later reports. It is not silently ignored, which prevents the same investigation from being repeated every few months.
3. Cleanup
Cleanup is allowed only for candidates explicitly approved as remove. Removals are applied in small batches, followed by analyzer and test verification.
4. Tests
After cleanup, the report suggests behavior-oriented checks based on the affected area. Removing a playback field, for example, should result in a playback test checklist rather than only a green static analyzer result.
Static correctness is necessary, but it does not prove that supported behavior survived.
Evidence instead of confidence theater
A high-confidence finding should normally have at least two independent signals and cleared false-positive checks.
The evidence hierarchy used by the skill is:
- analyzer or compiler diagnostics;
- import/export/part reachability;
- repository-wide symbol and string searches;
- runtime registration and framework-specific inspection;
- tests or builds after an approved removal.
When signals conflict, confidence goes down. The workflow does not average the conflict away.
Reports designed for decisions
Large raw lists are difficult to review. The report therefore groups findings into:
- Ready for review — strong evidence and no plausible dynamic consumer;
- Review carefully — likely unused but located at a sensitive boundary;
- Needs team context — intent cannot be established from the repository;
- Potential logic issue — may reveal a bug rather than cleanup;
- Intentionally retained — kept with a documented reason;
- Not dead / excluded — investigated and shown to be live or protected.
Every candidate preserves its location, evidence, checks, confidence, risk, recommendation, and verification command. The executive view stays compact while detailed evidence remains available when needed.
Installation
From the root of a Flutter or Dart project:
npx skills add LucasMenezeslc/flutter-dead-code-audit
Then start with a read-only request:
Use $flutter-dead-code-audit in Audit mode on this project.
Do not modify files. Create a report and present the first review group.
The scripts require Python 3.10 or newer and use only the Python standard library. Dart can come from PATH or FVM. Script output uses relative paths so reports do not expose the absolute project directory.
Current boundaries
The skill does not currently audit:
- duplicate code;
- unused assets or localization resources;
- unused dependencies in
pubspec.yaml; - application performance or security.
It also does not install project tooling, rewrite analyzer rules, or hand-delete generated files during an audit.
Feedback is welcome
The project is MIT licensed and open to contributions. The most useful feedback is a small reproducible Flutter or Dart example that exposes a false positive, a false negative, or a dynamic usage pattern that is not yet modeled.
If you have encountered dead code hidden behind routing, dependency injection, code generation, conditional imports, platform implementations, or package exports, I would like to learn from that case.
Repository: flutter-dead-code-audit

Top comments (0)