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.

install
dart pub global activate ciach
dart pub add --dev ciach
usage
$ 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

FlagWhat 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|githubOutput 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.
-vNarrate 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.

.github/workflows/test.yml
- 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
# 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.

SkippedWhyFlag
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-public there.
  • Reflection, dynamic invocation and names used only from excluded generated code are invisible to a reference search.
  • Entry points other than main need 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.

tool/dead_code.dart
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?

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.