diff --git a/CHANGELOG.md b/CHANGELOG.md index 71bf4f6..301d628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ it is missing). `ignore`, `rules` and `--explain check:` accept it. A mistake in a check is a configuration error with its line (exit code 2). - The production-shaped fixture pins five values with `checks:`. +- `--list-rules` also lists the project checks from the configuration file, + with their severity and reason. ## 0.2.0 diff --git a/doc/configuration.md b/doc/configuration.md index 617a53f..b4b5c46 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -14,7 +14,7 @@ storehound [options] [project-dir] --today=YYYY-MM-DD Date deadlines are measured against (reproducible runs) --brief Text output without the why/fix explanations --[no-]color Colour text output (default: when stdout is a terminal and NO_COLOR is unset) - --list-rules List every rule + --list-rules List every rule, then the project checks from the configuration --explain= Explain one rule, or a project check as check: --version Print the version -h, --help Show the usage diff --git a/lib/src/cli.dart b/lib/src/cli.dart index 26cd1ca..a6868ae 100644 --- a/lib/src/cli.dart +++ b/lib/src/cli.dart @@ -79,7 +79,13 @@ ArgParser _parser() => ArgParser(usageLineLength: 80) 'Colour text output. Default: when stdout is a terminal and ' 'NO_COLOR is unset.', ) - ..addFlag('list-rules', negatable: false, help: 'List every rule and exit.') + ..addFlag( + 'list-rules', + negatable: false, + help: + 'List every rule, and the project checks in the configuration, ' + 'and exit.', + ) ..addOption( 'explain', help: @@ -126,10 +132,6 @@ int runCli( out.writeln('$packageName $packageVersion'); return ExitCodes.ok; } - if (args.flag('list-rules')) { - out.writeln(renderRuleList(builtInRules)); - return ExitCodes.ok; - } final explain = args.option('explain'); if (explain != null) { for (final rule in builtInRules) { @@ -153,6 +155,26 @@ int runCli( return ExitCodes.usage; } final root = p.normalize(p.join(cwd, projectArg)); + if (args.flag('list-rules')) { + // Works anywhere; project checks are listed when a configuration exists. + final PreflightConfig config; + try { + config = _loadConfig(args.option('config'), cwd, root, err); + } on ConfigException catch (e) { + err.writeln('Invalid configuration: $e'); + return ExitCodes.usage; + } + out.writeln( + renderRuleList( + builtInRules, + checks: config.checkRules, + checksSource: config.source == null + ? null + : p.relative(config.source!, from: cwd), + ), + ); + return ExitCodes.ok; + } if (!Directory(root).existsSync()) { err.writeln('Project directory not found: $root'); return ExitCodes.usage; @@ -171,22 +193,7 @@ int runCli( PreflightConfig config; try { - final explicit = args.option('config'); - var path = explicit != null - ? p.join(cwd, explicit) - : p.join(root, configFileName); - if (explicit == null && !File(path).existsSync()) { - final legacy = p.join(root, legacyConfigFileName); - if (File(legacy).existsSync()) { - err.writeln( - 'Reading $legacyConfigFileName; rename it to $configFileName.', - ); - path = legacy; - } - } - config = explicit != null || File(path).existsSync() - ? PreflightConfig.load(path, builtInRules) - : const PreflightConfig(); + config = _loadConfig(args.option('config'), cwd, root, err); } on ConfigException catch (e) { err.writeln('Invalid configuration: $e'); return ExitCodes.usage; @@ -277,6 +284,32 @@ int runCli( return report.failed ? ExitCodes.findings : ExitCodes.ok; } +/// Loads `--config` ([explicit], relative to [cwd]) or the project's +/// configuration file under [root], or returns the defaults when there is +/// none. Throws [ConfigException] for an invalid file. +PreflightConfig _loadConfig( + String? explicit, + String cwd, + String root, + StringSink err, +) { + var path = explicit != null + ? p.join(cwd, explicit) + : p.join(root, configFileName); + if (explicit == null && !File(path).existsSync()) { + final legacy = p.join(root, legacyConfigFileName); + if (File(legacy).existsSync()) { + err.writeln( + 'Reading $legacyConfigFileName; rename it to $configFileName.', + ); + path = legacy; + } + } + return explicit != null || File(path).existsSync() + ? PreflightConfig.load(path, builtInRules) + : const PreflightConfig(); +} + String _usage(ArgParser parser) => ''' Catch App Store and Google Play rejections in a Flutter project before review. diff --git a/lib/src/report/text_reporter.dart b/lib/src/report/text_reporter.dart index 0b1c30e..205eb75 100644 --- a/lib/src/report/text_reporter.dart +++ b/lib/src/report/text_reporter.dart @@ -127,8 +127,13 @@ String _indent(String text, String prefix, {int width = 80}) { return out.join('\n'); } -/// Renders `--list-rules` output. -String renderRuleList(List rules) { +/// Renders `--list-rules` output: [rules] by platform, then the project +/// [checks] from the configuration file [checksSource], with their reasons. +String renderRuleList( + List rules, { + List checks = const [], + String? checksSource, +}) { final out = StringBuffer(); for (final platform in RulePlatform.values) { final group = rules.where((r) => r.platform == platform).toList(); @@ -146,6 +151,22 @@ String renderRuleList(List rules) { } out.writeln(); } + if (checks.isNotEmpty) { + out.writeln( + checksSource == null + ? 'Project checks' + : 'Project checks (from $checksSource)', + ); + for (final c in checks) { + out + ..writeln( + ' ${c.id.padRight(30)} ${c.defaultSeverity.name.padRight(8)}' + ' ${c.title}', + ) + ..writeln(_indent(c.why, ' ')); + } + out.writeln(); + } out.write( 'Run `--explain ` for why a rule exists and how to fix ' 'its findings.', diff --git a/test/cli_test.dart b/test/cli_test.dart index 671a38d..6ea7c1b 100644 --- a/test/cli_test.dart +++ b/test/cli_test.dart @@ -178,6 +178,34 @@ checks: expect(noConfig.err, contains('no configuration file')); }); + test('--list-rules adds the project checks with severity and reason', () { + final root = project(); + final list = run(['--list-rules', '--project', root]); + expect(list.code, ExitCodes.ok, reason: list.err); + expect(list.out, contains('Project checks (from ')); + expect( + list.out, + matches( + RegExp(r'check:no-backup +error +'), + ), + ); + expect(list.out, contains(' Tokens live in shared preferences.')); + for (final rule in builtInRules) { + expect(list.out, contains(rule.id)); + } + + final none = run(['--list-rules', '--project', writeProject({})]); + expect(none.code, ExitCodes.ok); + expect(none.out, isNot(contains('Project checks'))); + + final bad = writeProject({ + 'storehound.yaml': checks.replaceFirst('"false"', 'false'), + }); + final invalid = run(['--list-rules', bad]); + expect(invalid.code, ExitCodes.usage); + expect(invalid.err, contains('storehound.yaml:6')); + }); + test('a mistake in a check exits 2 with its line', () { final root = writeProject({ 'storehound.yaml': checks.replaceFirst('"false"', 'false'),