diff --git a/packages/custom_lint/CHANGELOG.md b/packages/custom_lint/CHANGELOG.md index 1941fd91..11a0df7b 100644 --- a/packages/custom_lint/CHANGELOG.md +++ b/packages/custom_lint/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased patch + +- Support JSON output format via CLI parameter `--format json|default` (thanks to @kuhnroyal) + ## 0.5.6 - 2023-10-30 Optimized logic for finding an unused VM_service port. diff --git a/packages/custom_lint/bin/custom_lint.dart b/packages/custom_lint/bin/custom_lint.dart index 6dd2998c..6775331f 100644 --- a/packages/custom_lint/bin/custom_lint.dart +++ b/packages/custom_lint/bin/custom_lint.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:custom_lint/custom_lint.dart'; +import 'package:custom_lint/src/output/output_format.dart'; Future entrypoint([List args = const []]) async { final parser = ArgParser() @@ -16,6 +17,23 @@ Future entrypoint([List args = const []]) async { help: 'Treat warning level issues as fatal', defaultsTo: true, ) + ..addOption( + 'format', + valueHelp: 'value', + help: 'Specifies the format to display lints.', + defaultsTo: 'default', + allowed: [ + OutputFormatEnum.plain.name, + OutputFormatEnum.json.name, + ], + allowedHelp: { + 'default': + 'The default output format. This format is intended to be user ' + 'consumable.\nThe format is not specified and can change ' + 'between releases.', + 'json': 'A machine readable output in a JSON format.', + }, + ) ..addFlag( 'watch', help: "Watches plugins' sources and perform a hot-reload on change", @@ -39,12 +57,14 @@ 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; + final format = result['format'] as String; await customLint( workingDirectory: Directory.current, watchMode: watchMode, fatalInfos: fatalInfos, fatalWarnings: fatalWarnings, + format: OutputFormatEnum.fromName(format), ); } diff --git a/packages/custom_lint/lib/custom_lint.dart b/packages/custom_lint/lib/custom_lint.dart index a0f0841f..d6098381 100644 --- a/packages/custom_lint/lib/custom_lint.dart +++ b/packages/custom_lint/lib/custom_lint.dart @@ -2,13 +2,11 @@ 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:cli_util/cli_logging.dart'; -import 'package:collection/collection.dart'; -import 'package:path/path.dart' as p; import 'src/cli_logger.dart'; +import 'src/output/output_format.dart'; +import 'src/output/render_lints.dart'; import 'src/plugin_delegate.dart'; import 'src/runner.dart'; import 'src/server_isolate_channel.dart'; @@ -41,6 +39,7 @@ Future customLint({ required Directory workingDirectory, bool fatalInfos = true, bool fatalWarnings = true, + OutputFormatEnum format = OutputFormatEnum.plain, }) async { // Reset the code exitCode = 0; @@ -53,6 +52,7 @@ Future customLint({ workingDirectory: workingDirectory, fatalInfos: fatalInfos, fatalWarnings: fatalWarnings, + format: format, ); } catch (_) { exitCode = 1; @@ -67,6 +67,7 @@ Future _runServer( required Directory workingDirectory, required bool fatalInfos, required bool fatalWarnings, + required OutputFormatEnum format, }) async { final customLintServer = await CustomLintServer.start( sendPort: channel.receivePort.sendPort, @@ -101,6 +102,7 @@ Future _runServer( workingDirectory: workingDirectory, fatalInfos: fatalInfos, fatalWarnings: fatalWarnings, + format: format, ); if (watchMode) { @@ -110,6 +112,7 @@ Future _runServer( workingDirectory: workingDirectory, fatalInfos: fatalInfos, fatalWarnings: fatalWarnings, + format: format, ); } } finally { @@ -132,94 +135,28 @@ Future _runPlugins( required Directory workingDirectory, required bool fatalInfos, required bool fatalWarnings, + required OutputFormatEnum format, }) async { final lints = await runner.getLints(reload: reload); - _renderLints( + renderLints( lints, log: log, progress: progress, workingDirectory: workingDirectory, fatalInfos: fatalInfos, fatalWarnings: fatalWarnings, + format: format, ); } -void _renderLints( - List lints, { - required Logger log, - required Progress progress, - required Directory workingDirectory, - required bool fatalInfos, - required bool fatalWarnings, -}) { - var errors = lints.expand((lint) => lint.errors); - - // Sort errors by severity, file, line, column, code, message - errors = errors.sorted((a, b) { - final severityCompare = -AnalysisErrorSeverity.VALUES - .indexOf(a.severity) - .compareTo(AnalysisErrorSeverity.VALUES.indexOf(b.severity)); - if (severityCompare != 0) return severityCompare; - - final fileCompare = _relativeFilePath(a.location.file, workingDirectory) - .compareTo(_relativeFilePath(b.location.file, workingDirectory)); - if (fileCompare != 0) return fileCompare; - - final lineCompare = a.location.startLine.compareTo(b.location.startLine); - if (lineCompare != 0) return lineCompare; - - final columnCompare = - a.location.startColumn.compareTo(b.location.startColumn); - if (columnCompare != 0) return columnCompare; - - final codeCompare = a.code.compareTo(b.code); - if (codeCompare != 0) return codeCompare; - - return a.message.compareTo(b.message); - }); - - // Finish progress and display duration (only when ANSI is supported) - progress.finish(showTiming: true); - - // Separate progress from results - log.stdout(''); - if (errors.isEmpty) { - log.stdout('No issues found!'); - return; - } - - var hasErrors = false; - var hasWarnings = false; - var hasInfos = false; - for (final error in errors) { - log.stdout( - ' ${_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; - } - - // Display a summary separated from the lints - log.stdout(''); - final errorCount = errors.length; - log.stdout('$errorCount issue${errorCount > 1 ? 's' : ''} found.'); - - if (hasErrors || (fatalWarnings && hasWarnings) || (fatalInfos && hasInfos)) { - exitCode = 1; - return; - } -} - Future _startWatchMode( CustomLintRunner runner, { required Logger log, required Directory workingDirectory, required bool fatalInfos, required bool fatalWarnings, + required OutputFormatEnum format, }) async { if (stdin.hasTerminal) { stdin @@ -245,6 +182,7 @@ Future _startWatchMode( workingDirectory: workingDirectory, fatalInfos: fatalInfos, fatalWarnings: fatalWarnings, + format: format, ); break; case 'q': @@ -255,10 +193,3 @@ Future _startWatchMode( } } } - -String _relativeFilePath(String file, Directory fromDir) { - return p.relative( - file, - from: fromDir.absolute.path, - ); -} diff --git a/packages/custom_lint/lib/src/output/default_output_format.dart b/packages/custom_lint/lib/src/output/default_output_format.dart new file mode 100644 index 00000000..1dc48c6c --- /dev/null +++ b/packages/custom_lint/lib/src/output/default_output_format.dart @@ -0,0 +1,31 @@ +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:cli_util/cli_logging.dart'; + +import 'output_format.dart'; +import 'render_lints.dart'; + +/// The default output format. +class DefaultOutputFormat implements OutputFormat { + @override + void render({ + required Iterable errors, + required Logger log, + }) { + if (errors.isEmpty) { + log.stdout('No issues found!'); + return; + } + + for (final error in errors) { + log.stdout( + ' ${error.location.relativePath}:${error.location.startLine}:${error.location.startColumn}' + ' • ${error.message} • ${error.code} • ${error.severity.name}', + ); + } + + // Display a summary separated from the lints + log.stdout(''); + final errorCount = errors.length; + log.stdout('$errorCount issue${errorCount > 1 ? 's' : ''} found.'); + } +} diff --git a/packages/custom_lint/lib/src/output/json_output_format.dart b/packages/custom_lint/lib/src/output/json_output_format.dart new file mode 100644 index 00000000..11d7359f --- /dev/null +++ b/packages/custom_lint/lib/src/output/json_output_format.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; + +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:cli_util/cli_logging.dart'; + +import 'output_format.dart'; + +/// The JSON output format. +/// +/// Code is an adaption of the original Dart SDK JSON format. +/// See: https://github.com/dart-lang/sdk/blob/main/pkg/dartdev/lib/src/commands/analyze.dart +class JsonOutputFormat implements OutputFormat { + @override + void render({ + required Iterable errors, + required Logger log, + }) { + final diagnostics = >[]; + for (final error in errors) { + final contextMessages = >[]; + if (error.contextMessages != null) { + for (final contextMessage in error.contextMessages!) { + final startOffset = contextMessage.location.offset; + contextMessages.add({ + 'location': _location( + file: contextMessage.location.file, + range: _range( + start: _position( + offset: startOffset, + line: contextMessage.location.startLine, + column: contextMessage.location.startColumn, + ), + end: _position( + offset: startOffset + contextMessage.location.length, + line: contextMessage.location.endLine, + column: contextMessage.location.endColumn, + ), + ), + ), + 'message': contextMessage.message, + }); + } + } + final startOffset = error.location.offset; + diagnostics.add({ + 'code': error.code, + 'severity': error.severity, + 'type': error.type, + 'location': _location( + file: error.location.file, + range: _range( + start: _position( + offset: startOffset, + line: error.location.startLine, + column: error.location.startColumn, + ), + end: _position( + offset: startOffset + error.location.length, + line: error.location.endLine, + column: error.location.endColumn, + ), + ), + ), + 'problemMessage': error.message, + if (error.correction != null) 'correctionMessage': error.correction, + if (contextMessages.isNotEmpty) 'contextMessages': contextMessages, + if (error.url != null) 'documentation': error.url, + }); + } + log.stdout( + json.encode({ + 'version': 1, + 'diagnostics': diagnostics, + }), + ); + } + + Map _location({ + required String file, + required Map range, + }) { + return { + 'file': file, + 'range': range, + }; + } + + Map _position({ + int? offset, + int? line, + int? column, + }) { + return { + 'offset': offset, + 'line': line, + 'column': column, + }; + } + + Map _range({ + required Map start, + required Map end, + }) { + return { + 'start': start, + 'end': end, + }; + } +} diff --git a/packages/custom_lint/lib/src/output/output_format.dart b/packages/custom_lint/lib/src/output/output_format.dart new file mode 100644 index 00000000..fe661462 --- /dev/null +++ b/packages/custom_lint/lib/src/output/output_format.dart @@ -0,0 +1,35 @@ +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:cli_util/cli_logging.dart'; + +/// An enum for the output format. +enum OutputFormatEnum { + /// The default output format. + plain._('default'), + + /// Dart SDK like JSON output format. + json._('json'); + + const OutputFormatEnum._(this.name); + + /// The name of the format. + final String name; + + /// Returns the [OutputFormatEnum] for the given [name]. + static OutputFormatEnum fromName(String name) { + for (final format in OutputFormatEnum.values) { + if (format.name == name) { + return format; + } + } + return plain; + } +} + +/// An abstract class for outputting lints +abstract class OutputFormat { + /// Renders lints according to the format and flags. + void render({ + required Iterable errors, + required Logger log, + }); +} diff --git a/packages/custom_lint/lib/src/output/render_lints.dart b/packages/custom_lint/lib/src/output/render_lints.dart new file mode 100644 index 00000000..f9c43040 --- /dev/null +++ b/packages/custom_lint/lib/src/output/render_lints.dart @@ -0,0 +1,97 @@ +import 'dart:io'; +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:cli_util/cli_logging.dart'; +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as p; + +import 'default_output_format.dart'; +import 'json_output_format.dart'; +import 'output_format.dart'; + +/// Renders lints according to the given format and flags. +void renderLints( + List lints, { + required Logger log, + required Progress progress, + required Directory workingDirectory, + required bool fatalInfos, + required bool fatalWarnings, + required OutputFormatEnum format, +}) { + final OutputFormat outputFormat; + switch (format) { + case OutputFormatEnum.json: + outputFormat = JsonOutputFormat(); + break; + case OutputFormatEnum.plain: + default: + outputFormat = DefaultOutputFormat(); + } + + var errors = lints.expand((lint) => lint.errors); + + var fatal = false; + for (final error in errors) { + error.location.relativePath = p.relative( + error.location.file, + from: workingDirectory.absolute.path, + ); + fatal = fatal || + error.severity == AnalysisErrorSeverity.ERROR || + (fatalWarnings && error.severity == AnalysisErrorSeverity.WARNING) || + (fatalInfos && error.severity == AnalysisErrorSeverity.INFO); + } + + // Sort errors by severity, file, line, column, code, message + // if the output format requires it + errors = errors.sorted((a, b) { + final severityCompare = -AnalysisErrorSeverity.VALUES + .indexOf(a.severity) + .compareTo(AnalysisErrorSeverity.VALUES.indexOf(b.severity)); + if (severityCompare != 0) return severityCompare; + + final fileCompare = + a.location.relativePath.compareTo(b.location.relativePath); + if (fileCompare != 0) return fileCompare; + + final lineCompare = a.location.startLine.compareTo(b.location.startLine); + if (lineCompare != 0) return lineCompare; + + final columnCompare = + a.location.startColumn.compareTo(b.location.startColumn); + if (columnCompare != 0) return columnCompare; + + final codeCompare = a.code.compareTo(b.code); + if (codeCompare != 0) return codeCompare; + + return a.message.compareTo(b.message); + }); + + // Finish progress and display duration (only when ANSI is supported) + progress.finish(showTiming: true); + + // Separate progress from results + log.stdout(''); + + outputFormat.render( + errors: errors, + log: log, + ); + + if (fatal) { + exitCode = 1; + return; + } +} + +final _locationRelativePath = Expando('locationRelativePath'); + +/// A helper extension to set/get +/// the working directory relative path of a [Location]. +extension LocationRelativePath on Location { + /// The working directory relative path of this [Location]. + String get relativePath => _locationRelativePath[this]! as String; + + set relativePath(String path) => _locationRelativePath[this] = path; +} diff --git a/packages/custom_lint/test/cli_process_test.dart b/packages/custom_lint/test/cli_process_test.dart index 1b5de699..6d9722bf 100644 --- a/packages/custom_lint/test/cli_process_test.dart +++ b/packages/custom_lint/test/cli_process_test.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:custom_lint/src/output/output_format.dart'; import 'package:custom_lint/src/package_utils.dart'; import 'package:test/test.dart'; import 'package:test_process/test_process.dart'; @@ -147,28 +148,69 @@ No issues found! }, ); - test( - 'found lints', - () async { - final plugin = createPlugin(name: 'test_lint', main: oyPluginSource); - - final app = createLintUsage( - name: 'test_app', - source: { - 'lib/main.dart': 'void fn() {}', - 'lib/another.dart': 'void fail() {}', - }, - plugins: {'test_lint': plugin.uri}, - ); - - final process = await Process.run( - 'dart', - [customLintBinPath], - workingDirectory: app.path, - ); - - expect(trimDependencyOverridesWarning(process.stderr), isEmpty); - expect(process.stdout, ''' + for (final format in OutputFormatEnum.values.map((e) => e.name)) { + test( + 'found lints format: $format', + () async { + final plugin = createPlugin(name: 'test_lint', main: oyPluginSource); + + final app = createLintUsage( + name: 'test_app', + source: { + 'lib/main.dart': 'void fn() {}', + 'lib/another.dart': 'void fail() {}', + }, + plugins: {'test_lint': plugin.uri}, + ); + + final process = await Process.run( + 'dart', + [ + customLintBinPath, + '--format', + format, + ], + workingDirectory: app.path, + ); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + + if (format == 'json') { + final dir = Directory(app.path).resolveSymbolicLinksSync(); + final json = jsonEncode({ + 'version': 1, + 'diagnostics': [ + { + 'code': 'oy', + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '$dir/lib/another.dart', + 'range': { + 'start': {'offset': 5, 'line': 1, 'column': 6}, + 'end': {'offset': 9, 'line': 1, 'column': 10}, + }, + }, + 'problemMessage': 'Oy', + }, + { + 'code': 'oy', + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '$dir/lib/main.dart', + 'range': { + 'start': {'offset': 5, 'line': 1, 'column': 6}, + 'end': {'offset': 7, 'line': 1, 'column': 8}, + }, + }, + 'problemMessage': 'Oy', + } + ], + }); + expect(process.stdout, 'Analyzing...\n\n$json\n'); + } else { + expect(process.stdout, ''' Analyzing... lib/another.dart:1:6 • Oy • oy • INFO @@ -176,9 +218,11 @@ Analyzing... 2 issues found. '''); - expect(process.exitCode, 1); - }, - ); + } + expect(process.exitCode, 1); + }, + ); + } test( 'missing package_config.json', diff --git a/packages/custom_lint/test/cli_test.dart b/packages/custom_lint/test/cli_test.dart index 7bd2017f..caf80b3a 100644 --- a/packages/custom_lint/test/cli_test.dart +++ b/packages/custom_lint/test/cli_test.dart @@ -1,6 +1,8 @@ +import 'dart:convert'; import 'dart:io'; import 'package:analyzer/error/error.dart'; +import 'package:custom_lint/src/output/output_format.dart'; import 'package:test/test.dart'; import '../bin/custom_lint.dart' as cli; @@ -29,73 +31,173 @@ Pattern progressMessage({required bool supportsAnsiEscapes}) { return r'Analyzing\.\.\..*'; } +String jsonLints(String dir) { + return jsonEncode({ + 'version': 1, + 'diagnostics': [ + { + 'code': 'hello_world', + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '$dir/lib/another.dart', + 'range': { + 'start': { + 'offset': 5, + 'line': 1, + 'column': 6, + }, + 'end': { + 'offset': 9, + 'line': 1, + 'column': 10, + }, + }, + }, + 'problemMessage': 'Hello world', + }, + { + 'code': 'oy', + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '$dir/lib/another.dart', + 'range': { + 'start': { + 'offset': 5, + 'line': 1, + 'column': 6, + }, + 'end': { + 'offset': 9, + 'line': 1, + 'column': 10, + }, + }, + }, + 'problemMessage': 'Oy', + }, + { + 'code': 'hello_world', + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '$dir/lib/main.dart', + 'range': { + 'start': { + 'offset': 5, + 'line': 1, + 'column': 6, + }, + 'end': { + 'offset': 7, + 'line': 1, + 'column': 8, + }, + }, + }, + 'problemMessage': 'Hello world', + }, + { + 'code': 'oy', + 'severity': 'INFO', + 'type': 'LINT', + 'location': { + 'file': '$dir/lib/main.dart', + 'range': { + 'start': { + 'offset': 5, + 'line': 1, + 'column': 6, + }, + 'end': { + 'offset': 7, + 'line': 1, + 'column': 8, + }, + }, + }, + 'problemMessage': 'Oy', + } + ], + }); +} + void main() { // Run 2 tests, one with ANSI escapes and one without // One test has no lints, the other has some, this should be enough. - for (final supportsAnsiEscapes in [true, false]) { - group('With${supportsAnsiEscapes ? '' : 'out'} ANSI escapes', () { - test('exits with 0 when no lint and no error are found', () async { - final plugin = createPlugin(name: 'test_lint', main: emptyPluginSource); - - 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(); - - expect(exitCode, 0); - expect( - out.join(), - completion( - allOf( - matches( - progressMessage( - supportsAnsiEscapes: supportsAnsiEscapes, + for (final ansi in [true, false]) { + for (final format in OutputFormatEnum.values.map((e) => e.name)) { + group('With ANSI: $ansi and format: $format', () { + test('exits with 0 when no lint and no error are found', () async { + final plugin = + createPlugin(name: 'test_lint', main: emptyPluginSource); + + 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(['--format', format]); + + expect(exitCode, 0); + expect( + out.join(), + completion( + allOf( + matches( + progressMessage( + supportsAnsiEscapes: ansi, + ), ), + format == 'json' + ? endsWith('{"version":1,"diagnostics":[]}\n') + : endsWith('No issues found!\n'), ), - endsWith('No issues found!\n'), ), - ), - ); - expect(err, emitsDone); - }, - currentDirectory: app, - supportsAnsiEscapes: supportsAnsiEscapes, - ); - }); - - 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); - - final app = createLintUsage( - source: { - 'lib/main.dart': 'void fn() {}', - 'lib/another.dart': 'void fail() {}', - }, - plugins: {'test_lint': plugin.uri, 'test_lint2': plugin2.uri}, - name: 'test_app', - ); - - await runWithIOOverride( - (out, err) async { - await cli.entrypoint(); - - expect(err, emitsDone); - expect( - out.join(), - completion( - allOf( - matches( - progressMessage( - supportsAnsiEscapes: supportsAnsiEscapes, + ); + expect(err, emitsDone); + }, + currentDirectory: app, + supportsAnsiEscapes: ansi, + ); + }); + + 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); + + final app = createLintUsage( + source: { + 'lib/main.dart': 'void fn() {}', + 'lib/another.dart': 'void fail() {}', + }, + plugins: {'test_lint': plugin.uri, 'test_lint2': plugin2.uri}, + name: 'test_app', + ); + + await runWithIOOverride( + (out, err) async { + await cli.entrypoint(['--format', format]); + + final dir = IOOverrides.current!.getCurrentDirectory().path; + expect(err, emitsDone); + expect( + out.join(), + completion( + allOf( + matches( + progressMessage( + supportsAnsiEscapes: ansi, + ), ), - ), - endsWith(''' + format == 'json' + ? endsWith('${jsonLints(dir)}\n') + : endsWith(''' lib/another.dart:1:6 • Hello world • hello_world • INFO lib/another.dart:1:6 • Oy • oy • INFO lib/main.dart:1:6 • Hello world • hello_world • INFO @@ -103,16 +205,17 @@ void main() { 4 issues found. '''), + ), ), - ), - ); - expect(exitCode, 1); - }, - currentDirectory: app, - supportsAnsiEscapes: supportsAnsiEscapes, - ); + ); + expect(exitCode, 1); + }, + currentDirectory: app, + supportsAnsiEscapes: ansi, + ); + }); }); - }); + } } test('exits with 1 if only an error but no lint are found', () async { @@ -228,43 +331,6 @@ Analyzing... ); }); - 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); - - final app = createLintUsage( - source: { - 'lib/main.dart': 'void fn() {}', - 'lib/another.dart': 'void fail() {}', - }, - plugins: {'test_lint': plugin.uri, 'test_lint2': plugin2.uri}, - name: 'test_app', - ); - - await runWithIOOverride( - (out, err) async { - await cli.entrypoint(); - - expect(err, emitsDone); - expect( - out.join(), - completion(''' -Analyzing... - - lib/another.dart:1:6 • Hello world • hello_world • INFO - lib/another.dart:1:6 • Oy • oy • INFO - lib/main.dart:1:6 • Hello world • hello_world • INFO - lib/main.dart:1:6 • Oy • oy • INFO - -4 issues found. -'''), - ); - expect(exitCode, 1); - }, - currentDirectory: app, - ); - }); - test('supports plugins that do not compile', () async { final plugin = createPlugin(name: 'test_lint', main: helloWordPluginSource); final plugin2 = createPlugin(