58dc586929
Context messages are already displayed in a way that makes it easy for the user to locate the line of code in question; it's unnecessary to include the line number in the context message itself. See discussion at https://buganizer.corp.google.com/issues/179782591#comment9. Change-Id: I2d1383c4cee0ece0d302449302d1f1902fbf136c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/192480 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Paul Berry <paulberry@google.com>
400 lines
12 KiB
Dart
400 lines
12 KiB
Dart
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
import 'package:cli_util/cli_logging.dart';
|
|
import 'package:dartdev/src/analysis_server.dart';
|
|
import 'package:dartdev/src/commands/analyze.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import '../utils.dart';
|
|
|
|
void main() {
|
|
group('analysisError', defineAnalysisError, timeout: longTimeout);
|
|
group('analyze', defineAnalyze, timeout: longTimeout);
|
|
}
|
|
|
|
const String _analyzeDescriptionText = 'Analyze Dart code in a directory.';
|
|
|
|
const String _analyzeUsageText =
|
|
'Usage: dart analyze [arguments] [<directory>]';
|
|
|
|
const String _unusedImportAnalysisOptions = '''
|
|
analyzer:
|
|
errors:
|
|
# Increase the severity of several hints.
|
|
unused_import: warning
|
|
''';
|
|
|
|
const String _unusedImportCodeSnippet = '''
|
|
import 'dart:convert';
|
|
|
|
void main() {
|
|
print('hello world');
|
|
}
|
|
''';
|
|
|
|
void defineAnalysisError() {
|
|
group('contextMessages', () {
|
|
test('none', () {
|
|
var error = AnalysisError({});
|
|
expect(error.contextMessages, isEmpty);
|
|
});
|
|
test('one', () {
|
|
var error = AnalysisError({
|
|
'contextMessages': [<String, dynamic>{}],
|
|
});
|
|
expect(error.contextMessages, hasLength(1));
|
|
});
|
|
test('two', () {
|
|
var error = AnalysisError({
|
|
'contextMessages': [<String, dynamic>{}, <String, dynamic>{}],
|
|
});
|
|
expect(error.contextMessages, hasLength(2));
|
|
});
|
|
});
|
|
}
|
|
|
|
void defineAnalyze() {
|
|
TestProject p;
|
|
|
|
setUp(() => p = null);
|
|
|
|
tearDown(() => p?.dispose());
|
|
|
|
test('--help', () {
|
|
p = project();
|
|
var result = p.runSync(['analyze', '--help']);
|
|
|
|
expect(result.exitCode, 0);
|
|
expect(result.stderr, isEmpty);
|
|
expect(result.stdout, contains(_analyzeDescriptionText));
|
|
expect(result.stdout, contains(_analyzeUsageText));
|
|
});
|
|
|
|
test('multiple directories', () {
|
|
p = project();
|
|
var result = p.runSync(['analyze', '/no/such/dir1/', '/no/such/dir2/']);
|
|
|
|
expect(result.exitCode, 64);
|
|
expect(result.stdout, isEmpty);
|
|
expect(result.stderr, contains('Only one directory or file is expected.'));
|
|
expect(result.stderr, contains(_analyzeUsageText));
|
|
});
|
|
|
|
test('no such directory', () {
|
|
p = project();
|
|
var result = p.runSync(['analyze', '/no/such/dir1/']);
|
|
|
|
expect(result.exitCode, 64);
|
|
expect(result.stdout, isEmpty);
|
|
expect(result.stderr,
|
|
contains("Directory or file doesn't exist: /no/such/dir1/"));
|
|
expect(result.stderr, contains(_analyzeUsageText));
|
|
});
|
|
|
|
test('current working directory', () {
|
|
p = project(mainSrc: 'int get foo => 1;\n');
|
|
|
|
var result = p.runSync(['analyze'], workingDir: p.dirPath);
|
|
|
|
expect(result.exitCode, 0);
|
|
expect(result.stderr, isEmpty);
|
|
expect(result.stdout, contains('No issues found!'));
|
|
});
|
|
|
|
group('single directory', () {
|
|
test('no errors', () {
|
|
p = project(mainSrc: 'int get foo => 1;\n');
|
|
var result = p.runSync(['analyze', p.dirPath]);
|
|
|
|
expect(result.exitCode, 0);
|
|
expect(result.stderr, isEmpty);
|
|
expect(result.stdout, contains('No issues found!'));
|
|
});
|
|
|
|
test('one error', () {
|
|
p = project(mainSrc: "int get foo => 'str';\n");
|
|
var result = p.runSync(['analyze', p.dirPath]);
|
|
|
|
expect(result.exitCode, 3);
|
|
expect(result.stderr, isEmpty);
|
|
expect(result.stdout, contains('A value of type '));
|
|
expect(result.stdout, contains('lib/main.dart:1:16 '));
|
|
expect(result.stdout, contains('return_of_invalid_type'));
|
|
expect(result.stdout, contains('1 issue found.'));
|
|
});
|
|
|
|
test('two errors', () {
|
|
p = project(mainSrc: "int get foo => 'str';\nint get bar => 'str';\n");
|
|
var result = p.runSync(['analyze', p.dirPath]);
|
|
|
|
expect(result.exitCode, 3);
|
|
expect(result.stderr, isEmpty);
|
|
expect(result.stdout, contains('2 issues found.'));
|
|
});
|
|
});
|
|
|
|
group('single file', () {
|
|
test('no errors', () {
|
|
p = project(mainSrc: 'int get foo => 1;\n');
|
|
var result = p.runSync(['analyze', p.mainPath]);
|
|
|
|
expect(result.exitCode, 0);
|
|
expect(result.stderr, isEmpty);
|
|
expect(result.stdout, contains('No issues found!'));
|
|
});
|
|
|
|
test('one error', () {
|
|
p = project(mainSrc: "int get foo => 'str';\n");
|
|
var result = p.runSync(['analyze', p.mainPath]);
|
|
|
|
expect(result.exitCode, 3);
|
|
expect(result.stderr, isEmpty);
|
|
expect(result.stdout, contains('A value of type '));
|
|
expect(result.stdout, contains('main.dart:1:16 '));
|
|
expect(result.stdout, contains('return_of_invalid_type'));
|
|
expect(result.stdout, contains('1 issue found.'));
|
|
});
|
|
});
|
|
|
|
test('warning --fatal-warnings', () {
|
|
p = project(
|
|
mainSrc: _unusedImportCodeSnippet,
|
|
analysisOptions: _unusedImportAnalysisOptions);
|
|
var result = p.runSync(['analyze', '--fatal-warnings', p.dirPath]);
|
|
|
|
expect(result.exitCode, equals(2));
|
|
expect(result.stderr, isEmpty);
|
|
expect(result.stdout, contains('1 issue found.'));
|
|
});
|
|
|
|
test('warning implicit --fatal-warnings', () {
|
|
p = project(
|
|
mainSrc: _unusedImportCodeSnippet,
|
|
analysisOptions: _unusedImportAnalysisOptions);
|
|
var result = p.runSync(['analyze', p.dirPath]);
|
|
|
|
expect(result.exitCode, equals(2));
|
|
expect(result.stderr, isEmpty);
|
|
expect(result.stdout, contains('1 issue found.'));
|
|
});
|
|
|
|
test('warning --no-fatal-warnings', () {
|
|
p = project(
|
|
mainSrc: _unusedImportCodeSnippet,
|
|
analysisOptions: _unusedImportAnalysisOptions);
|
|
var result = p.runSync(['analyze', '--no-fatal-warnings', p.dirPath]);
|
|
|
|
expect(result.exitCode, 0);
|
|
expect(result.stderr, isEmpty);
|
|
expect(result.stdout, contains('1 issue found.'));
|
|
});
|
|
|
|
test('info implicit no --fatal-infos', () {
|
|
p = project(mainSrc: dartVersionFilePrefix2_9 + 'String foo() {}');
|
|
var result = p.runSync(['analyze', p.dirPath]);
|
|
|
|
expect(result.exitCode, 0);
|
|
expect(result.stderr, isEmpty);
|
|
expect(result.stdout, contains('1 issue found.'));
|
|
});
|
|
|
|
test('info --fatal-infos', () {
|
|
p = project(mainSrc: dartVersionFilePrefix2_9 + 'String foo() {}');
|
|
var result = p.runSync(['analyze', '--fatal-infos', p.dirPath]);
|
|
|
|
expect(result.exitCode, 1);
|
|
expect(result.stderr, isEmpty);
|
|
expect(result.stdout, contains('1 issue found.'));
|
|
});
|
|
|
|
test('--verbose', () {
|
|
p = project(mainSrc: '''
|
|
int f() {
|
|
var result = one + 2;
|
|
var one = 1;
|
|
return result;
|
|
}''');
|
|
var result = p.runSync(['analyze', '--verbose', p.dirPath]);
|
|
|
|
expect(result.exitCode, 3);
|
|
expect(result.stderr, isEmpty);
|
|
var stdout = result.stdout;
|
|
expect(stdout, contains("The declaration of 'one' is here"));
|
|
expect(
|
|
stdout, contains('Try moving the declaration to before the first use'));
|
|
expect(stdout, contains('https://dart.dev'));
|
|
expect(stdout, contains('referenced_before_declaration'));
|
|
});
|
|
|
|
group('display mode', () {
|
|
final sampleInfoJson = {
|
|
'severity': 'INFO',
|
|
'type': 'TODO',
|
|
'code': 'dead_code',
|
|
'location': {
|
|
'endLine': 16,
|
|
'endColumn': 12,
|
|
'file': 'lib/test.dart',
|
|
'offset': 362,
|
|
'length': 72,
|
|
'startLine': 15,
|
|
'startColumn': 4
|
|
},
|
|
'message': 'Foo bar baz.',
|
|
'hasFix': false,
|
|
};
|
|
final fullDiagnosticJson = {
|
|
'severity': 'ERROR',
|
|
'type': 'COMPILE_TIME_ERROR',
|
|
'location': {
|
|
'file': 'lib/test.dart',
|
|
'offset': 19,
|
|
'length': 1,
|
|
'startLine': 2,
|
|
'startColumn': 9
|
|
},
|
|
'message':
|
|
"Local variable 's' can't be referenced before it is declared.",
|
|
'correction':
|
|
"Try moving the declaration to before the first use, or renaming the local variable so that it doesn't hide a name from an enclosing scope.",
|
|
'code': 'referenced_before_declaration',
|
|
'url':
|
|
'https:://dart.dev/tools/diagnostic-messages#referenced_before_declaration',
|
|
'contextMessages': [
|
|
{
|
|
'message': "The declaration of 's' is on line 3.",
|
|
'location': {
|
|
'file': 'lib/test.dart',
|
|
'offset': 29,
|
|
'length': 1,
|
|
'startLine': 3,
|
|
'startColumn': 7
|
|
}
|
|
}
|
|
],
|
|
'hasFix': false
|
|
};
|
|
|
|
test('default', () {
|
|
final logger = TestLogger(false);
|
|
final errors = [AnalysisError(sampleInfoJson)];
|
|
|
|
AnalyzeCommand.emitDefaultFormat(logger, errors);
|
|
|
|
expect(logger.stderrBuffer, isEmpty);
|
|
final stdout = logger.stdoutBuffer.toString().trim();
|
|
expect(stdout, contains('info'));
|
|
expect(stdout, contains('lib/test.dart:15:4'));
|
|
expect(stdout, contains('Foo bar baz.'));
|
|
expect(stdout, contains('dead_code'));
|
|
});
|
|
|
|
group('json', () {
|
|
test('short', () {
|
|
final logger = TestLogger(false);
|
|
final errors = [AnalysisError(sampleInfoJson)];
|
|
|
|
AnalyzeCommand.emitJsonFormat(logger, errors);
|
|
|
|
expect(logger.stderrBuffer, isEmpty);
|
|
final stdout = logger.stdoutBuffer.toString().trim();
|
|
expect(
|
|
stdout,
|
|
'{"version":1,"diagnostics":[{"code":"dead_code","severity":"INFO",'
|
|
'"type":"TODO","location":{"file":"lib/test.dart","range":{'
|
|
'"start":{"offset":362,"line":15,"column":4},"end":{"offset":434,'
|
|
'"line":16,"column":12}}},"problemMessage":"Foo bar baz."}]}');
|
|
});
|
|
test('full', () {
|
|
final logger = TestLogger(false);
|
|
final errors = [AnalysisError(fullDiagnosticJson)];
|
|
|
|
AnalyzeCommand.emitJsonFormat(logger, errors);
|
|
|
|
expect(logger.stderrBuffer, isEmpty);
|
|
final stdout = logger.stdoutBuffer.toString().trim();
|
|
expect(
|
|
stdout,
|
|
'{"version":1,"diagnostics":[{'
|
|
'"code":"referenced_before_declaration","severity":"ERROR",'
|
|
'"type":"COMPILE_TIME_ERROR","location":{"file":"lib/test.dart",'
|
|
'"range":{"start":{"offset":19,"line":2,"column":9},"end":{'
|
|
'"offset":20,"line":null,"column":null}}},"problemMessage":'
|
|
'"Local variable \'s\' can\'t be referenced before it is declared.",'
|
|
'"correctionMessage":"Try moving the declaration to before the'
|
|
' first use, or renaming the local variable so that it doesn\'t hide'
|
|
' a name from an enclosing scope.","contextMessages":[{"location":{'
|
|
'"file":"lib/test.dart","range":{"start":{"offset":29,"line":3,'
|
|
'"column":7},"end":{"offset":30,"line":null,"column":null}}},'
|
|
'"message":"The declaration of \'s\' is on line 3."}],'
|
|
'"documentation":'
|
|
'"https:://dart.dev/tools/diagnostic-messages#referenced_before_declaration"}]}');
|
|
});
|
|
});
|
|
|
|
test('machine', () {
|
|
final logger = TestLogger(false);
|
|
final errors = [AnalysisError(sampleInfoJson)];
|
|
|
|
AnalyzeCommand.emitMachineFormat(logger, errors);
|
|
|
|
expect(logger.stderrBuffer, isEmpty);
|
|
expect(
|
|
logger.stdoutBuffer.toString().trim(),
|
|
'INFO|TODO|DEAD_CODE|lib/test.dart|15|4|72|Foo bar baz.',
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
class TestLogger implements Logger {
|
|
final stdoutBuffer = StringBuffer();
|
|
|
|
final stderrBuffer = StringBuffer();
|
|
|
|
@override
|
|
final bool isVerbose;
|
|
|
|
TestLogger(this.isVerbose);
|
|
|
|
@override
|
|
Ansi get ansi => Ansi(false);
|
|
|
|
@override
|
|
void flush() {}
|
|
|
|
@override
|
|
Progress progress(String message) {
|
|
return SimpleProgress(this, message);
|
|
}
|
|
|
|
@override
|
|
void stderr(String message) {
|
|
stderrBuffer.writeln(message);
|
|
}
|
|
|
|
@override
|
|
void stdout(String message) {
|
|
stdoutBuffer.writeln(message);
|
|
}
|
|
|
|
@override
|
|
void trace(String message) {
|
|
if (isVerbose) {
|
|
stdoutBuffer.writeln(message);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void write(String message) {
|
|
stdoutBuffer.write(message);
|
|
}
|
|
|
|
@override
|
|
void writeCharCode(int charCode) {
|
|
stdoutBuffer.writeCharCode(charCode);
|
|
}
|
|
}
|