From c05c3024bafd7210ddfd6711dc78c5e3c36ad5b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 19:14:55 +0000 Subject: [PATCH 1/2] feat: add --no-fail-public to report public findings without failing Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_012RMbmsRmDqx5eWXFa8R82V --- README.md | 12 ++++++ bin/ciach.dart | 11 ++++- lib/src/cli/args.dart | 8 ++++ test/cli_exit_code_test.dart | 82 ++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 test/cli_exit_code_test.dart diff --git a/README.md b/README.md index 756e969..a17d109 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,9 @@ ciach --no-public -f json # GitHub Actions annotations; fail the job if anything is found ciach -f github --set-exit-if-changed +# Fail CI only on unused private declarations; still report public ones +ciach . --set-exit-if-changed --no-fail-public + # Remove what's found, after confirming ciach --remove @@ -93,6 +96,7 @@ ciach --remove --force | `--[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 ` | — | Skip files matching the glob (repeatable). | @@ -149,6 +153,14 @@ 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. +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: + +```yaml +- run: dart run ciach -f github --set-exit-if-changed --no-fail-public +``` + ### Removing declarations `--remove` deletes every reported declaration from source — its doc comment diff --git a/bin/ciach.dart b/bin/ciach.dart index d480047..9bc4094 100644 --- a/bin/ciach.dart +++ b/bin/ciach.dart @@ -144,8 +144,15 @@ Future _run(List arguments) async { await _removeUnused(result, rootDir.absolute.path, args, format, useColor); } - if (result.unused.isNotEmpty && args.flag('set-exit-if-changed')) { - return 1; + if (args.flag('set-exit-if-changed')) { + // Public findings are still reported above; --no-fail-public only keeps + // them out of the exit code, so the build fails on private findings alone. + final failing = args.flag('fail-public') + ? result.unused + : result.unused.where((d) => d.isPrivate); + if (failing.isNotEmpty) { + return 1; + } } return 0; } diff --git a/lib/src/cli/args.dart b/lib/src/cli/args.dart index 603aa4f..e3ab027 100644 --- a/lib/src/cli/args.dart +++ b/lib/src/cli/args.dart @@ -71,6 +71,14 @@ ArgParser buildParser() => .new() 'private (underscore-prefixed) declarations, which are the\n' 'highest-confidence dead code.', ) + ..addFlag( + 'fail-public', + defaultsTo: true, + help: + 'Count unused public declarations toward the exit code (with\n' + '--set-exit-if-changed). Use --no-fail-public to report them\n' + 'without failing the build.', + ) ..addFlag( 'generated', help: 'Scan generated files (*.g.dart, *.freezed.dart, …). Off by default.', diff --git a/test/cli_exit_code_test.dart b/test/cli_exit_code_test.dart new file mode 100644 index 0000000..329d30c --- /dev/null +++ b/test/cli_exit_code_test.dart @@ -0,0 +1,82 @@ +/* + * AI-Provenance: + * model: claude-opus-4-8 + * harness: Claude Code + * plugins: + * - lean-ai-provenance + * skills: + * - mark-ai-provenance + */ + +@Timeout(Duration(minutes: 5)) +library; + +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + // Drives the real CLI (bin/ciach.dart) against the example package, the same + // `sample_pkg` fixture the finder tests use, and asserts the process exit + // code — the behavior --set-exit-if-changed / --no-fail-public controls. + final fixturePath = p.join(Directory.current.path, 'example'); + final entrypoint = p.join('bin', 'ciach.dart'); + + setUpAll(() async { + // The fixture is a real package; the analysis server needs its + // package_config.json to resolve `package:sample_pkg/...` imports. + final config = File( + p.join(fixturePath, '.dart_tool', 'package_config.json'), + ); + if (!config.existsSync()) { + final result = await Process.run(Platform.resolvedExecutable, [ + 'pub', + 'get', + ], workingDirectory: fixturePath); + expect(result.exitCode, 0, reason: '${result.stdout}\n${result.stderr}'); + } + }); + + Future runCli(List args) => Process.run( + Platform.resolvedExecutable, + ['run', entrypoint, fixturePath, '--no-progress', ...args], + ); + + // orphans.dart has only unused *public* declarations; greeting.dart also has + // an unused *private* one (`_danglingPrivate`). + const publicOnly = ['--include', 'lib/orphans.dart']; + const withPrivate = ['--include', 'lib/greeting.dart']; + + test( + '--set-exit-if-changed --no-fail-public: only public unused -> exit 0', + () async { + final result = await runCli([ + ...publicOnly, + '--set-exit-if-changed', + '--no-fail-public', + ]); + expect(result.exitCode, 0, reason: '${result.stdout}\n${result.stderr}'); + // Public findings are still reported, just not counted toward the exit. + expect(result.stdout, contains('UnusedClass')); + }, + ); + + test( + '--set-exit-if-changed --no-fail-public: an unused private -> exit 1', + () async { + final result = await runCli([ + ...withPrivate, + '--set-exit-if-changed', + '--no-fail-public', + ]); + expect(result.exitCode, 1, reason: '${result.stdout}\n${result.stderr}'); + expect(result.stdout, contains('_danglingPrivate')); + }, + ); + + test('--set-exit-if-changed alone: public counts -> exit 1', () async { + final result = await runCli([...publicOnly, '--set-exit-if-changed']); + expect(result.exitCode, 1, reason: '${result.stdout}\n${result.stderr}'); + }); +} From bdb714580f5f54fa832836aa4a4272cacfe9ad9d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 19:15:43 +0000 Subject: [PATCH 2/2] docs: changelog for #23 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_012RMbmsRmDqx5eWXFa8R82V --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0944432..0d14044 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +- Add `--[no-]fail-public` (default on): `--no-fail-public` still reports unused + public declarations but excludes them from `--set-exit-if-changed`, so CI fails + only on unused private ones. ([#23](https://github.com/leancodepl/ciach/pull/23)) - Document the `--unused-union-members`, `--report-tojson`, `--generated-suffix`, and `--help` options in the README, which existed in the CLI but were missing from the options table.