diff --git a/packages/custom_lint/bin/custom_lint.dart b/packages/custom_lint/bin/custom_lint.dart index ae479044..6dd2998c 100644 --- a/packages/custom_lint/bin/custom_lint.dart +++ b/packages/custom_lint/bin/custom_lint.dart @@ -6,6 +6,16 @@ import 'package:custom_lint/custom_lint.dart'; Future entrypoint([List args = const []]) async { final parser = ArgParser() + ..addFlag( + 'fatal-infos', + help: 'Treat info level issues as fatal', + defaultsTo: true, + ) + ..addFlag( + 'fatal-warnings', + help: 'Treat warning level issues as fatal', + defaultsTo: true, + ) ..addFlag( 'watch', help: "Watches plugins' sources and perform a hot-reload on change", @@ -27,8 +37,15 @@ Future entrypoint([List args = const []]) async { } final watchMode = result['watch'] as bool; + final fatalInfos = result['fatal-infos'] as bool; + final fatalWarnings = result['fatal-warnings'] as bool; - await customLint(workingDirectory: Directory.current, watchMode: watchMode); + await customLint( + workingDirectory: Directory.current, + watchMode: watchMode, + fatalInfos: fatalInfos, + fatalWarnings: fatalWarnings, + ); } void main([List args = const []]) async { diff --git a/packages/custom_lint/lib/custom_lint.dart b/packages/custom_lint/lib/custom_lint.dart index c3ef55bc..e64dbbe8 100644 --- a/packages/custom_lint/lib/custom_lint.dart +++ b/packages/custom_lint/lib/custom_lint.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:analyzer_plugin/protocol/protocol_common.dart'; import 'package:analyzer_plugin/protocol/protocol_generated.dart'; import 'package:collection/collection.dart'; import 'package:path/path.dart' as p; @@ -36,6 +37,8 @@ q: Quit Future customLint({ bool watchMode = true, required Directory workingDirectory, + bool fatalInfos = true, + bool fatalWarnings = true, }) async { // Reset the code exitCode = 0; @@ -46,6 +49,8 @@ Future customLint({ channel, watchMode: watchMode, workingDirectory: workingDirectory, + fatalInfos: fatalInfos, + fatalWarnings: fatalWarnings, ); } catch (_) { exitCode = 1; @@ -58,6 +63,8 @@ Future _runServer( ServerIsolateChannel channel, { required bool watchMode, required Directory workingDirectory, + required bool fatalInfos, + required bool fatalWarnings, }) async { final customLintServer = await CustomLintServer.start( sendPort: channel.receivePort.sendPort, @@ -84,10 +91,17 @@ Future _runServer( runner, reload: false, workingDirectory: workingDirectory, + fatalInfos: fatalInfos, + fatalWarnings: fatalWarnings, ); if (watchMode) { - await _startWatchMode(runner, workingDirectory: workingDirectory); + await _startWatchMode( + runner, + workingDirectory: workingDirectory, + fatalInfos: fatalInfos, + fatalWarnings: fatalWarnings, + ); } } finally { await runner?.close(); @@ -105,15 +119,17 @@ Future _runPlugins( CustomLintRunner runner, { required bool reload, required Directory workingDirectory, + required bool fatalInfos, + required bool fatalWarnings, }) async { try { final lints = await runner.getLints(reload: reload); - - if (lints.any((lintsForFile) => lintsForFile.errors.isNotEmpty)) { - exitCode = 1; - } - - _renderLints(lints, workingDirectory: workingDirectory); + _renderLints( + lints, + workingDirectory: workingDirectory, + fatalInfos: fatalInfos, + fatalWarnings: fatalWarnings, + ); } catch (err, stack) { exitCode = 1; stderr.writeln('$err\n$stack'); @@ -123,6 +139,8 @@ Future _runPlugins( void _renderLints( List lints, { required Directory workingDirectory, + required bool fatalInfos, + required bool fatalWarnings, }) { var errors = lints.expand((lint) => lint.errors); @@ -150,18 +168,31 @@ void _renderLints( return; } - exitCode = 1; + var hasErrors = false; + var hasWarnings = false; + var hasInfos = false; for (final error in errors) { stdout.writeln( ' ${_relativeFilePath(error.location.file, workingDirectory)}:${error.location.startLine}:${error.location.startColumn}' ' • ${error.message} • ${error.code} • ${error.severity.name}', ); + hasErrors = hasErrors || error.severity == AnalysisErrorSeverity.ERROR; + hasWarnings = + hasWarnings || error.severity == AnalysisErrorSeverity.WARNING; + hasInfos = hasInfos || error.severity == AnalysisErrorSeverity.INFO; + } + + if (hasErrors || (fatalWarnings && hasWarnings) || (fatalInfos && hasInfos)) { + exitCode = 1; + return; } } Future _startWatchMode( CustomLintRunner runner, { required Directory workingDirectory, + required bool fatalInfos, + required bool fatalWarnings, }) async { if (stdin.hasTerminal) { stdin @@ -183,6 +214,8 @@ Future _startWatchMode( runner, reload: true, workingDirectory: workingDirectory, + fatalInfos: fatalInfos, + fatalWarnings: fatalWarnings, ); break; case 'q': diff --git a/packages/custom_lint/test/cli_test.dart b/packages/custom_lint/test/cli_test.dart index 91b49186..586e8f3c 100644 --- a/packages/custom_lint/test/cli_test.dart +++ b/packages/custom_lint/test/cli_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:analyzer/error/error.dart'; import 'package:test/test.dart'; import '../bin/custom_lint.dart' as cli; @@ -86,6 +87,73 @@ lib/custom_lint_client.dart:13:29: Error: Undefined name 'createPlugin'. ); }); + test('exits with 0 when pass argument `--no-fatal-infos`', () async { + final plugin = createPlugin(name: 'test_lint', main: helloWordPluginSource); + + final app = createLintUsage( + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + await runWithIOOverride( + (out, err) async { + await cli.entrypoint(['--no-fatal-infos']); + + expect(exitCode, 0); + expect( + out.join(), + completion( + matchIgnoringAnsi(contains, ''' +lib/main.dart:1:6 • Hello world • hello_world • INFO +'''), + ), + ); + expect(err, emitsDone); + }, + currentDirectory: app, + ); + }); + + test( + 'exits with 0 when found warning and pass argument `--no-fatal-warnings`', + () async { + final plugin = createPlugin( + name: 'test_lint', + main: createPluginSource([ + TestLintRule( + code: 'hello_world', + message: 'Hello world', + errorSeverity: ErrorSeverity.WARNING, + ), + ]), + ); + + final app = createLintUsage( + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': plugin.uri}, + name: 'test_app', + ); + + await runWithIOOverride( + (out, err) async { + await cli.entrypoint(['--no-fatal-warnings']); + + expect(exitCode, 0); + expect( + out.join(), + completion( + matchIgnoringAnsi(contains, ''' +lib/main.dart:1:6 • Hello world • hello_world • WARNING +'''), + ), + ); + expect(err, emitsDone); + }, + currentDirectory: app, + ); + }); + test('CLI lists warnings from all plugins and set exit code', () async { final plugin = createPlugin(name: 'test_lint', main: helloWordPluginSource); final plugin2 = createPlugin(name: 'test_lint2', main: oyPluginSource); diff --git a/packages/custom_lint/test/create_project.dart b/packages/custom_lint/test/create_project.dart index bef10be2..dff3c059 100644 --- a/packages/custom_lint/test/create_project.dart +++ b/packages/custom_lint/test/create_project.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:analyzer/error/error.dart'; import 'package:path/path.dart'; import 'package:test/scaffolding.dart'; @@ -30,6 +31,7 @@ class TestLintRule { this.onVariable = '', this.ruleMembers = '', this.fixes = const [], + this.errorSeverity = ErrorSeverity.INFO, }); final String code; @@ -39,6 +41,7 @@ class TestLintRule { final String onVariable; final String ruleMembers; final List fixes; + final ErrorSeverity errorSeverity; } class TestLintFix { @@ -103,7 +106,9 @@ class ${fix.name} extends DartFix { class ${rule.code} extends DartLintRule { ${rule.code}() : super( - code: LintCode(name: '${rule.code}', problemMessage: '${rule.message}'), + code: LintCode(name: '${rule.code}', + problemMessage: '${rule.message}', + errorSeverity: ErrorSeverity.${rule.errorSeverity.displayName.toUpperCase()}), ); $fixes diff --git a/packages/custom_lint/test/goldens/server_test/redirect_logs.golden b/packages/custom_lint/test/goldens/server_test/redirect_logs.golden index 6183a0a2..29dfd2d6 100644 --- a/packages/custom_lint/test/goldens/server_test/redirect_logs.golden +++ b/packages/custom_lint/test/goldens/server_test/redirect_logs.golden @@ -1,4 +1,4 @@ [hello_world] 1990-01-01T00:00:00.000 Hello world [hello_world] 1990-01-01T00:00:00.000 Plugin hello_world threw while analyzing app/lib/another.dart: [hello_world] 1990-01-01T00:00:00.000 Bad state: fail -[hello_world] 1990-01-01T00:00:00.000 #0 hello_world.run. (package:test_lint/test_lint.dart:26:3) \ No newline at end of file +[hello_world] 1990-01-01T00:00:00.000 #0 hello_world.run. (package:test_lint/test_lint.dart:28:3) \ No newline at end of file diff --git a/packages/custom_lint/test/server_test.dart b/packages/custom_lint/test/server_test.dart index e73e23c8..a9043041 100644 --- a/packages/custom_lint/test/server_test.dart +++ b/packages/custom_lint/test/server_test.dart @@ -385,7 +385,9 @@ if (node.name.lexeme == "fail") { }); group('hot-reload', () { - test('Supports starting custom_lint twice in watch mode at once', () async { + test( + timeout: const Timeout.factor(2), + 'Supports starting custom_lint twice in watch mode at once', () async { final plugin = createPlugin( name: 'test_lint', main: helloWordPluginSource,