ciach 0.1.0
ciach: ^0.1.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 🔪 #
"Ciach!" 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.
Finds unused (never-referenced) declarations — classes, functions, methods, fields, constants, enum values, and so on — in a Dart or Flutter package.
Rather than re-implementing reference resolution, it drives the Dart analysis
server over LSP and asks it, for every declaration, textDocument/references
with includeDeclaration: false. If the server reports zero references, the
declaration is unused. This reuses the analyzer's battle-tested, cross-file
reference search (including polymorphic dispatch through interfaces).
The LSP transport (JSON-RPC framing, request/response correlation, lifecycle,
and the typed wire models) is handled by the
pro_lsp package, whose types are used
directly throughout. lib/src/lsp/lsp_client.dart is a thin session wrapper
that adds only what pro_lsp doesn't: spawning the server process, awaiting the
Dart-specific $/analyzerStatus idle signal, and shutting the process down
cleanly.
How it works #
- Spawn the analysis server:
dart language-server --protocol=lsp, using the same SDK that runs the tool (Platform.resolvedExecutable). initializeat the package root and wait for the initial analysis to settle (tracked via the server's$/analyzerStatusnotifications).- For each
.dartfile, enumerate declarations withtextDocument/documentSymbol. - For each declaration, query
textDocument/referencesat its name. Empty result ⇒ unused.
Because the whole package is analyzed, references from anywhere — including test files — count as usage, so the tool won't flag production code that is only used by tests.
Usage #
# Scan the current package
dart run ciach
# Scan a specific package
dart run ciach path/to/package
# Only the highest-confidence dead code (private, never-referenced), as JSON
dart run ciach --no-public -f json
# GitHub Actions annotations; fail the job if anything is found
dart run ciach -f github --set-exit-if-changed
Install it globally with dart pub global activate --source path . and then run
ciach directly.
Options #
| 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. |
--set-exit-if-changed |
off | Exit with status 1 when anything is found (for CI). Named after dart format. |
-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, operator. |
-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.
GitHub Actions #
- run: dart run ciach -f github --set-exit-if-changed
Each 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.
What it skips by default #
main— the program entry point.@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.@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.
Limitations #
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'). - Operator overloads (
operator +,operator ==, …) are effectively always reported: the analysis server's reference search does not resolve infix operator syntax (a + b) back to the operator's declaration, even though the call is perfectly real. Seeexample/lib/extensions.dart. - Dartdoc
[Xxx]reference links count as references too, which can hide real dead code: linking to a method that overrides or implements another (e.g./// See [Dog.sound]) can mark the interface member it overrides as "used" as a side effect. - Results are only as good as the analysis: a package that does not analyze
cleanly (missing
pub get, errors) may yield incomplete references.
Performance #
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 (or
dart pub global activate --source path .).
Library usage #
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}');
}
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/. See
example/ for a runnable demonstration.
License #
Licensed under the Apache License, Version 2.0. See LICENSE.