Skip to content

Commit

Permalink
Add --format to the CLI (#198)
Browse files Browse the repository at this point in the history
  • Loading branch information
kuhnroyal authored Nov 13, 2023
1 parent b1bda6f commit e3d97eb
Show file tree
Hide file tree
Showing 9 changed files with 548 additions and 211 deletions.
4 changes: 4 additions & 0 deletions packages/custom_lint/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
20 changes: 20 additions & 0 deletions packages/custom_lint/bin/custom_lint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> entrypoint([List<String> args = const []]) async {
final parser = ArgParser()
Expand All @@ -16,6 +17,23 @@ Future<void> entrypoint([List<String> 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",
Expand All @@ -39,12 +57,14 @@ Future<void> entrypoint([List<String> 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),
);
}

Expand Down
93 changes: 12 additions & 81 deletions packages/custom_lint/lib/custom_lint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -41,6 +39,7 @@ Future<void> customLint({
required Directory workingDirectory,
bool fatalInfos = true,
bool fatalWarnings = true,
OutputFormatEnum format = OutputFormatEnum.plain,
}) async {
// Reset the code
exitCode = 0;
Expand All @@ -53,6 +52,7 @@ Future<void> customLint({
workingDirectory: workingDirectory,
fatalInfos: fatalInfos,
fatalWarnings: fatalWarnings,
format: format,
);
} catch (_) {
exitCode = 1;
Expand All @@ -67,6 +67,7 @@ Future<void> _runServer(
required Directory workingDirectory,
required bool fatalInfos,
required bool fatalWarnings,
required OutputFormatEnum format,
}) async {
final customLintServer = await CustomLintServer.start(
sendPort: channel.receivePort.sendPort,
Expand Down Expand Up @@ -101,6 +102,7 @@ Future<void> _runServer(
workingDirectory: workingDirectory,
fatalInfos: fatalInfos,
fatalWarnings: fatalWarnings,
format: format,
);

if (watchMode) {
Expand All @@ -110,6 +112,7 @@ Future<void> _runServer(
workingDirectory: workingDirectory,
fatalInfos: fatalInfos,
fatalWarnings: fatalWarnings,
format: format,
);
}
} finally {
Expand All @@ -132,94 +135,28 @@ Future<void> _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<AnalysisErrorsParams> 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<void> _startWatchMode(
CustomLintRunner runner, {
required Logger log,
required Directory workingDirectory,
required bool fatalInfos,
required bool fatalWarnings,
required OutputFormatEnum format,
}) async {
if (stdin.hasTerminal) {
stdin
Expand All @@ -245,6 +182,7 @@ Future<void> _startWatchMode(
workingDirectory: workingDirectory,
fatalInfos: fatalInfos,
fatalWarnings: fatalWarnings,
format: format,
);
break;
case 'q':
Expand All @@ -255,10 +193,3 @@ Future<void> _startWatchMode(
}
}
}

String _relativeFilePath(String file, Directory fromDir) {
return p.relative(
file,
from: fromDir.absolute.path,
);
}
31 changes: 31 additions & 0 deletions packages/custom_lint/lib/src/output/default_output_format.dart
Original file line number Diff line number Diff line change
@@ -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<AnalysisError> 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.');
}
}
109 changes: 109 additions & 0 deletions packages/custom_lint/lib/src/output/json_output_format.dart
Original file line number Diff line number Diff line change
@@ -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<AnalysisError> errors,
required Logger log,
}) {
final diagnostics = <Map<String, Object?>>[];
for (final error in errors) {
final contextMessages = <Map<String, Object?>>[];
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<String, Object?> _location({
required String file,
required Map<String, Object?> range,
}) {
return {
'file': file,
'range': range,
};
}

Map<String, Object?> _position({
int? offset,
int? line,
int? column,
}) {
return {
'offset': offset,
'line': line,
'column': column,
};
}

Map<String, Object?> _range({
required Map<String, Object?> start,
required Map<String, Object?> end,
}) {
return {
'start': start,
'end': end,
};
}
}
Loading

1 comment on commit e3d97eb

@vercel
Copy link

@vercel vercel bot commented on e3d97eb Nov 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.