ciach 0.4.0 copy "ciach: ^0.4.0" to clipboard
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.

Banner

ciach πŸ”ͺ #

ciach pub.dev badge Test status License: Apache 2.0

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-public there, 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 #

LeanCode Logo

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 ciach across 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.

Need help with your Flutter project?

πŸ‘‰ Hire our team   β€’   Check our other packages

30
likes
0
points
436
downloads

Publisher

verified publisherleancode.co

Weekly Downloads

Finds unused (never-referenced) declarations in a Dart/Flutter package by driving the Dart analysis server over LSP and querying textDocument/references.

Repository (GitHub)
View/report issues

Topics

#static-analysis #dead-code #linter #cli #developer-tools

License

unknown (license)

Dependencies

args, collection, config, glob, path, pro_lsp, stream_channel, yaml

More

Packages that depend on ciach