ciach 0.4.0
ciach: ^0.4.0 copied to clipboard
Finds unused (never-referenced) declarations in a Dart/Flutter package by driving the Dart analysis server over LSP and querying textDocument/references.
ciach πͺ #
Dead code detector for Dart and Flutter. Finds declarations that are never referenced β classes, functions, methods, fields, constants, enum values β 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.
Installation #
Install it globally for a ciach command everywhere, in ~/.pub-cache/bin:
dart pub global activate ciach
Or add it as a dev dependency, which pins the version for the team and CI:
dart pub add --dev ciach
dart run ciach
Examples below show bare ciach β¦; prefix them with dart run for the second.
Usage #
ciach # current package
ciach path/to/package # another package
ciach --no-public -f json # private-only, as JSON
ciach -f github --set-exit-if-changed # CI: annotations, non-zero on finds
ciach --remove # delete findings, asks first
ciach --remove --force # β¦without asking
ciach --verbose # explain each step
Options #
| Option | Default | Description |
|---|---|---|
[path] |
. |
Package root to analyze. |
-h, --help |
β | Print usage information. |
--config <path> |
auto | Read settings from this YAML file instead of the auto-discovered one. See Configuration file. |
--no-config |
off | Ignore the config file, even if one is found. |
--[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. |
--[no-]unused-union-members |
off | Also flag a (sealed) supertype member matched only by type patterns, never constructed. Report-only β never touched by --remove. |
--[no-]report-tojson |
off | Report an otherwise-unused toJson() serialization hook too. Off by default β jsonEncode dispatches to it dynamically. |
--set-exit-if-changed |
off | Exit with status 1 when anything is found (for CI). Named after dart format. |
--[no-]fail-public |
on | Count unused public declarations toward the exit code (with --set-exit-if-changed). --no-fail-public reports them but fails only on private findings. |
--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). |
--generated-suffix <suffix> |
β | Extra filename suffix (with leading dot) to treat as generated and skip, on top of the built-in set; repeatable. Ignored when --generated is set. |
-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. |
-v, --verbose |
off | Explain what's happening on stderr. See Verbose mode. |
--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.
Configuration file #
Every option above can live in a ciach.yaml in the package root, keyed by its
long name minus the --, plus path for the positional argument:
public: false # --no-public
exclude: ['test/**', 'tool/**'] # repeatable options take a list, or a bare string
kinds: [class, function, method]
format: github
set-exit-if-changed: true
Command line beats config file beats default, even when the flag matches the
default (ciach --public overrides public: false), and a repeatable option on
the command line replaces the config's list rather than adding to it. Unknown
keys and wrong-typed values are usage errors naming the file and the key.
Discovery looks for that one file name in the analyzed package root, never in a
parent, so each package in a monorepo owns its config. --config <path> reads
one from elsewhere; --no-config ignores a discovered one; the two can't be
combined.
Verbose mode #
-v narrates the run on stderr, with elapsed times: the config file read and
what it set, every setting and the layer it came from, each scan phase, anything
the definition check rescued, and what --remove touches.
$ ciach -v
[ 0.0s] Read config from ciach.yaml.
[ 0.0s] It sets 2 options:
[ 0.0s] public: false
[ 0.0s] exclude: test/**
[ 0.0s] Settings for this run:
[ 0.0s] path: /home/me/pkg (command line)
[ 0.0s] public: false (config file)
[ 0.0s] exclude: test/** (config file)
[ 0.0s] concurrency: 16 (default)
[ 0.0s] color: true (auto-detected)
β¦
[ 0.1s] Starting Dart analysis serverβ¦
[ 0.3s] Collecting declarations from 13 file(s)β¦
[ 0.5s] Scanned 13 file(s) and checked 44 declaration(s) in 478ms: 4 unused, 1 referenced only from doc comments.
It all goes to stderr, so ciach -v -f json | jq still works. Reach for it when
a config file seems not to apply, or to find the phase eating the time. It
supersedes --progress, whose self-overwriting line would fight with it.
Doc-only findings #
A dartdoc [Xxx] link resolves to a real declaration, so the analysis server
counts it as a reference β but a comment mentioning something isn't the same as
code calling it. Declarations with no code references are reported separately,
in every format:
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)
These never count toward --set-exit-if-changed, are never touched by
--remove, and get a ::notice rather than a ::warning in -f github. Drop
the doc link to have one reported as properly unused.
GitHub Actions #
- run: dart pub get
- run: dart run ciach -f github --set-exit-if-changed
Each finding becomes a ::warning annotation inline on the PR diff. Run it from
the repository root so paths resolve; when scanning a sub-package (ciach -f github app), the scan path is prepended automatically.
For a library or workspace package whose public API is legitimately "unused"
from its own perspective, add --no-fail-public to still surface those
findings while gating the job on unused private declarations only:
- run: dart run ciach -f github --set-exit-if-changed --no-fail-public
Removing declarations #
--remove deletes every reported declaration β doc comment and annotations
included β after showing what it is about to remove and asking:
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.
--force skips the prompt (and is a usage error on its own); with no terminal to
confirm on and no --force, nothing is removed. Run dart format afterward:
removal is conservative about what it deletes β an ambiguous int a = 1, b = 2;
is left alone unless every declarator is unused β but not about spacing.
Removal acts on whatever the finder reports, so it inherits the same
false-positive risk, which --overrides and --operators widen considerably.
Doc-only findings are never included. Review the diff, as
you would after any automated refactor.
What it skips by default #
Each of these is a known source of false positives; the flag opts back in at that cost.
| Skipped | Why | Flag |
|---|---|---|
main |
the entry point is never unused | β |
@override members |
often reached polymorphically or by a framework (build, initState, ==, β¦), which a name-based search misses |
--overrides |
| Operator overloads | the server doesn't resolve a + b back to the declaration, so a used operator is flagged every time |
--operators |
call methods |
implicit-call syntax (obj(β¦)) is unresolvable the same way |
β |
@pragma('vm:entry-point') |
reachable from native code or reflection | β |
| Generated files | by filename convention and the GENERATED CODE - DO NOT MODIFY BY HAND banner. Still opened during analysis, so a declaration used only from a .g.dart isn't misreported |
--generated |
toJson() |
jsonEncode(obj) calls it by dynamic dispatch, leaving no source-level reference |
--report-tojson |
| Type parameters | always "used" within their scope | β |
dartdoc [Xxx] links |
not a code reference; reported as doc-only instead of hidden | β |
Private constructors are not skipped: an unused ClassName._ is dead code
like any other. A sole zero-parameter ClassName._() β the classic
prevent-instantiation marker β is reported with a hint suggesting abstract final class instead. See example/ for a runnable demonstration of each case.
Limitations #
This is a static, reference-based heuristic, so review its output rather than deleting blindly:
- A library package's public API is legitimately unused from inside the
package. Prefer
--no-publicthere, or treat public findings as advisory. - Reflection, dynamic invocation, and names referenced only from generated code you excluded are invisible to a reference search.
- Entry points other than
main(isolate entry points, plugin registrants) need excluding or@pragma('vm:entry-point'). - A package that doesn't analyze cleanly (missing
pub get, errors) yields incomplete references.
Performance #
Runtime is the analysis server's, not the tool's. It analyzes the whole package
once per run β tens of seconds for a large Flutter app, and unskippable, since
incomplete analysis means wrong reference counts β then answers one
textDocument/references per declaration through a pool of -j (default 16),
with scanned files kept open so its resolved-unit cache stays warm.
The lever is how much you ask for. --no-public is by far the cheapest mode:
private declarations are library-scoped, so each query searches one library
instead of the whole workspace, and it surfaces the highest-confidence dead code
anyway. --include/--exclude narrow the scan while still counting references
from everywhere. dart pub global activate compiles ahead of time, so there's no
JIT warmup per run.
Library usage #
The finder is also available 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}');
}
Development #
dart pub get
dart analyze
dart test # spins up a real analysis server against the example/ package
The implementation lives under lib/src/; the CLI entry point is bin/.
License #
Licensed under the Apache License, Version 2.0. See LICENSE.
π οΈ Maintained by LeanCode #
This package is built with π by LeanCode. We are top-tier experts focused on Flutter Enterprise solutions.
Why LeanCode? #
- 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.

