Docs
Install, configure and run ciach, and read its findings with confidence.
Install
Globally, for a ciach command everywhere, or as a dev dependency that pins the version for the team and CI (then prefix commands with
dart run). Requires Dart 3.10+ and analyzes with the SDK it runs with.
dart pub global activate ciach
dart pub add --dev ciach
$ dart pub global activate ciach
$ 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
Options
| Flag | What it does |
|---|---|
--no-public |
Report private declarations only. Cheapest mode; the right one for library packages. |
--remove --force |
Delete what was found after confirming. --force skips the prompt. |
-f text|json|github | Output format. |
--set-exit-if-changed |
Exit 1 when anything is found. Add --no-fail-public to count only private findings. |
-k --kinds |
Restrict to declaration kinds, e.g. class,function,method. |
-i -e |
Include or exclude file globs. References are still counted from everywhere. |
--overrides
--operators
--generated
--report-tojson
|
Opt back into a category skipped by default. |
-v | Narrate the run on stderr with timings. |
Exit codes: 0 clean, 1 findings with --set-exit-if-changed, 2 usage or analysis error. All options →
GitHub Actions
Each finding becomes an annotation on the diff, and the job fails when anything is found. Run from the repository root so paths resolve. For a library whose public API is legitimately unused from the inside, add
--no-fail-public.
- run: dart pub get
- run: dart run ciach -f github --set-exit-if-changed
Configuration file
Every option can live in a ciach.yaml in the package root. Command line beats config file beats default, and a repeatable option on the command line replaces the list instead of appending. Discovery looks in the analyzed package root only, so each package in a monorepo owns its config;
--config <path> reads one from elsewhere, --no-config ignores it.
# ciach.yaml — every option, keyed by its long name minus the "--".
public: false # --no-public
exclude: ['test/**', 'tool/**'] # repeatable options take a list
kinds: [class, function, method]
format: github
set-exit-if-changed: true
Compared with the analyzer
The Dart analyzer already flags unused private declarations through unused_element and friends, one library at a time. ciach starts where that stops.
dart analyze |
ciach |
|
|---|---|---|
| Unused private declarations | ✓ | ✓ |
| Unused public declarations | — | ✓ |
| References followed across libraries | — | ✓ |
| Removes what it finds | — | ✓ |
| GitHub annotations on the pull request | — | ✓ |
| JSON output | ✓ | ✓ |
Both resolve references the same way, because ciach asks the analysis server. The difference is scope and the removal step. Dart Code Metrics covers unused code as well, as part of a larger commercial toolset.
Removing safely
--remove shows what it is about to delete and asks first; with no terminal and no --force, nothing is removed. It deletes whole declarations with their doc comments and annotations, leaves an ambiguous
int a = 1, b = 2; alone unless every declarator is unused, and never touches doc-only findings. Run
dart format afterward and review the diff.
Report-only: removal would not compile
- A sealed member matched only by type patterns (
--unused-union-members). - Every value of a still-referenced enum.
- The sole constructor of a live class with final fields or super forwarding.
- A primary constructor or its declaring parameters.
What it skips, and what it cannot see
Each default skip is a known false-positive source; the flag opts back in at that cost.
| Skipped | Why | Flag |
|---|---|---|
main |
The entry point is never unused. | — |
@override members |
Reached polymorphically or by a framework. | --overrides |
Operator overloads |
The server does not resolve a + b to the declaration. |
--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 and the GENERATED CODE banner; still opened for analysis. |
--generated |
toJson() |
jsonEncode(obj) calls it by dynamic dispatch. |
--report-tojson |
dartdoc [Xxx] links |
Not a code reference; reported as doc-only. | — |
Doc-only findings
A dartdoc link counts as a reference to the analysis server, but a comment is not a call. Declarations with no code references are listed separately, never count toward the exit code and are never removed.
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)
Limitations
-
A library package’s public API is legitimately unused from the inside: prefer
--no-publicthere. - Reflection, dynamic invocation and names used only from excluded generated code are invisible to a reference search.
- Entry points other than
mainneed excluding or@pragma('vm:entry-point'). - A package that does not analyze cleanly yields incomplete references.
Library API
The finder behind the CLI is exported from package:ciach/ciach.dart. Options mirror the flags; the result carries every finding with file, line, kind and qualified name, and doc-only findings in their own list.
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}');
}
FAQ
Does ciach work with Flutter apps?
Yes, with any Dart package. @override members such as build and initState
are skipped by default because frameworks reach them polymorphically; pass --overrides to report them anyway.
Is it safe to run ciach --remove?
ciach --remove?
It shows the list and asks first, leaves ambiguous multi-declarator statements alone and marks findings whose removal would not compile as report-only. Still review the diff, as after any automated refactor.
--overrides and --operators widen the false-positive risk.
My library’s public API is reported as unused.
That is expected from inside the package. Use --no-public to report only private declarations, or keep public findings visible but out of the exit code with
--set-exit-if-changed --no-fail-public.
How fast is it?
As fast as the analysis server: the package is analyzed once per run, then one references query per declaration goes through a pool of 16 concurrent requests.
--no-public is by far the cheapest mode.
Which Dart versions are supported?
Dart 3.10 and newer. ciach analyzes with the SDK it is invoked with, so scanning newer syntax needs an SDK that can parse it.