Dead code detector and unused code finder for Dart and Flutter. Finds unused (never-referenced) declarations β classes, functions, methods, fields, constants, enum values, and so on β in a Dart or Flutter package, and can remove them for you.
"Ciach!" β pronounced /tΝ‘Ιax/ β is Polish for the sound of a clean chop, the noise a knife makes right before something falls off. Fitting, since that's exactly what this tool finds for you: dead code, waiting to be cut.
There are two ways to get the ciach command, depending on how you want to
run it:
-
Global activation β a single
ciachcommand available everywhere, independent of any particular project:dart pub global activate ciach ciach
This puts
ciachin~/.pub-cache/bin; add that to yourPATHifdart pub global activatewarns that it isn't there already. -
As a dev dependency β pinned per-project, so everyone on the team (and CI) uses the same version:
dart pub add --dev ciach dart run ciach
The rest of this README shows bare ciach β¦ commands; prefix them with
dart run if you installed it as a dev dependency instead of globally.
# Scan the current package
ciach
# Scan a specific package
ciach path/to/package
# Only the highest-confidence dead code (private, never-referenced), as JSON
ciach --no-public -f json
# GitHub Actions annotations; fail the job if anything is found
ciach -f github --set-exit-if-changed
# Remove what's found, after confirming
ciach --remove
# Remove without asking (e.g. from a script)
ciach --remove --force| Option | Default | Description |
|---|---|---|
[path] |
. |
Package root to analyze. |
--[no-]public |
on | Report unused public declarations too. Disable to report only private (_-prefixed) ones. |
--[no-]generated |
off | Scan generated files (*.g.dart, *.freezed.dart, *.mocks.dart, β¦). |
--[no-]overrides |
off | Report @override members too. Off by default β see limitations. |
--[no-]operators |
off | Report operator overloads (operator +, operator ==, β¦) too. Off by default β see limitations. |
--set-exit-if-changed |
off | Exit with status 1 when anything is found (for CI). Named after dart format. |
--remove |
off | Remove unused declarations after reporting them. Prompts for confirmation first. |
--force |
off | Skip the confirmation prompt for --remove. Requires --remove. |
-e, --exclude <glob> |
β | Skip files matching the glob (repeatable). |
-i, --include <glob> |
β | Only scan files matching the glob (repeatable). |
-k, --kinds <list> |
all | Restrict to kinds: class, mixin, interface, enum, extension, function, method, constructor, field, property, getter, setter, variable, constant, enum-value. |
-f, --format <fmt> |
text |
text, json, or github (GitHub Actions ::warning annotations). |
-j, --concurrency <n> |
16 |
Reference queries kept in flight against the analysis server. |
--[no-]color |
auto | Colorize text output. |
--[no-]progress |
auto | Show scan progress on stderr. |
--dart <path> |
current SDK | Path to the dart executable to launch the server with. |
Exit codes: 0 success, 1 unused found with --set-exit-if-changed, 2
usage or analysis error.
A dartdoc [Xxx] comment link resolves to a real declaration, so the
analysis server counts it as a reference β but "someone linked to this in a
comment" isn't the same confidence level as "something actually calls this".
Declarations with no code references, only a comment link, are reported
separately as doc-only, in every format:
$ ciach
lib/greeting.dart
15:6 function danglingFunction (public)
Referenced only from doc comments β not counted as unused, never removed:
lib/greeting.dart
40:6 function docOnlyMentioned (public)
Found 1 unused declaration in 1 file (scanned 6 files, 44 declarations, 0.5s). 1 more referenced only from doc comments.
Doc-only findings are informational: they never count toward
--set-exit-if-changed, are never touched by --remove, and get a ::notice
(not ::warning) in -f github output. If one really is dead code, remove
its doc comment link (or the comment itself) and re-run to have it reported
as properly unused.
Add ciach as a dev dependency (see Installation) so the
version is pinned and dart pub get is all the setup CI needs:
- run: dart pub get
- run: dart run ciach -f github --set-exit-if-changedEach finding becomes a ::warning annotation shown inline on the PR diff. Run
it from the repository root so annotation paths resolve; when scanning a
sub-package (e.g. ciach -f github app), the scan path is
prepended automatically so annotations still point at the right files.
--remove deletes every reported declaration from source β its doc comment
and annotations included β after showing what it's about to remove and asking
for confirmation:
$ ciach --remove
lib/greeting.dart
15:6 function danglingFunction (public)
...
Found 4 unused declarations in 2 files (scanned 6 files, 44 declarations, 0.5s).
Remove 4 unused declarations? [y/N] y
Removed 4 unused declarations from 2 files.
--remove --force skips the prompt; use it in a script once you're confident
in the results (--force on its own, without --remove, is a usage error).
Without a terminal to confirm on (e.g. piped into another program) and
without --force, nothing is removed.
Run dart format afterward β removal is conservative about what to delete
(it leaves ambiguous multi-variable statements like int a = 1, b = 2;
alone unless every declarator in them is unused) but not about spacing, so
expect the odd extra blank line.
Because removal acts on whatever the finder reports, it inherits the same
false-positive risk as the report itself (see What it skips by
default and Limitations below) β
enabling --overrides or --operators widens that risk considerably.
Doc-only findings are never included, regardless of
those flags. Review the diff (or your test suite) after removing, the same as
you would after any automated refactor.
These are all off by default because they're known sources of false positives β a used declaration reported as unused. Each has a flag to opt back in, at the cost of reintroducing that risk:
mainβ the program entry point. Always skipped; there's no flag for this one.@overridemembers β they are frequently reached polymorphically or by a framework (Flutter'sbuild,initState,dispose,toString,==, β¦), which a name-based reference search can miss. Use--overridesto include them.- Operator overloads (
operator +,operator ==, β¦) β the analysis server's reference search does not resolve infix operator syntax (a + b) back to the operator's declaration, so a used operator is reported as unused every time. Seeexample/lib/extensions.dart. Use--operatorsto include them. @pragma('vm:entry-point')β reachable from native code / reflection.- Generated files β by filename convention and the
GENERATED CODE - DO NOT MODIFY BY HANDbanner. Use--generatedto include. type parametersand non-declaration symbols.
dartdoc [Xxx] reference links are a related wrinkle, handled a bit
differently: a link resolves to a real declaration, so the analysis server
counts it as a reference, which would otherwise hide genuinely dead code
(e.g. /// See [Dog.sound] would keep Dog.sound looking used). Rather than
a flag, these get their own always-on category β see Doc-only
findings.
This is a static, reference-based heuristic. Expect to review its output rather than delete blindly:
- Public API of a library package is legitimately "unused" from the
package's own perspective. Prefer
--no-publicfor library packages, or treat public findings as advisory. - Reflection / dynamic invocation / serialization (e.g.
dart:mirrors, code that is only referenced by name in generated code you excluded) is not visible to a reference search. - Entry points beyond
main(isolate entry points, plugin registrants) may need excluding or annotating with@pragma('vm:entry-point'). - Results are only as good as the analysis: a package that does not analyze
cleanly (missing
pub get, errors) may yield incomplete references.
Runtime is dominated by the analysis server, not the tool. Two phases matter:
- Initial analysis β the server analyzes the whole package (and, for a Flutter app, the SDK/dependencies) once before any query. This is a fixed per-run cost (tens of seconds for a large app) and cannot be skipped: incomplete analysis would produce wrong reference counts.
- Reference queries β one
textDocument/referencesper declaration. Requests run through a global pool (-j/--concurrency, default 16) and the scanned files are kept open so the server's resolved-unit cache stays warm.
The biggest lever is how much you ask:
--no-publicis by far the cheapest mode. Private declarations are library-scoped, so the server only searches one library per query instead of the whole workspace β often several times faster, and it surfaces the highest-confidence dead code.--include/--excludeto scan only the part of the tree you care about β references are still counted from everywhere, so results stay correct.-jto tune concurrency; the default (16) is near the point of diminishing returns for the analysis server's internal parallelism.
For repeated runs, compile once to skip the JIT warmup:
dart compile exe bin/ciach.dart -o ciach β dart pub global activate already
does this for you.
The tool also exposes a public API for running the finder programmatically:
import 'package:ciach/ciach.dart';
final result = await Ciach(
FinderOptions(rootPath: 'path/to/package', includePublic: false),
).run();
for (final decl in result.unused) {
print('${decl.filePath}:${decl.line} ${decl.qualifiedName}');
}dart pub get
dart analyze
dart test # spins up a real analysis server against the example/ packageThe implementation lives under lib/src/; the CLI entry point is bin/. See
example/ for a runnable demonstration.
Licensed under the Apache License, Version 2.0. See LICENSE.
This package is built with π by LeanCode. We are top-tier experts focused on Flutter Enterprise solutions.
- Creators of Patrol β the next-gen testing framework for Flutter.
- Battle-Tested β we run
ciachacross our own Flutter and Dart codebases to keep them free of dead code. - Full-Cycle Product Development β we take your product from scratch to long-term maintenance.

