Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
it is missing). `ignore`, `rules` and `--explain check:<name>` 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

Expand Down
2 changes: 1 addition & 1 deletion doc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<rule-id> Explain one rule, or a project check as check:<name>
--version Print the version
-h, --help Show the usage
Expand Down
75 changes: 54 additions & 21 deletions lib/src/cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
25 changes: 23 additions & 2 deletions lib/src/report/text_reporter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,13 @@ String _indent(String text, String prefix, {int width = 80}) {
return out.join('\n');
}

/// Renders `--list-rules` output.
String renderRuleList(List<Rule> rules) {
/// Renders `--list-rules` output: [rules] by platform, then the project
/// [checks] from the configuration file [checksSource], with their reasons.
String renderRuleList(
List<Rule> rules, {
List<Rule> checks = const [],
String? checksSource,
}) {
final out = StringBuffer();
for (final platform in RulePlatform.values) {
final group = rules.where((r) => r.platform == platform).toList();
Expand All @@ -146,6 +151,22 @@ String renderRuleList(List<Rule> 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 <rule-id>` for why a rule exists and how to fix '
'its findings.',
Expand Down
28 changes: 28 additions & 0 deletions test/cli_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 +<application android:allowBackup>'),
),
);
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'),
Expand Down