blob: 1750ad6098af3b142125c83ae373b84cd9974ef0 [file] [log] [blame]
// 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:dartdev/src/sdk.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 _analyzeVerboseUsageText =
'Usage: dart [vm-options] 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');
}
''';
const String _todoAsWarningAnalysisOptions = '''
analyzer:
errors:
# Increase the severity of TODOs.
todo: warning
fixme: warning
''';
const String _todoAsWarningCodeSnippet = '''
void main() {
// TODO: Implement this
// FIXME: Fix this
}
''';
/// The exit code of the analysis server when the highest severity issue is a
/// warning.
const int _warningExitCode = 2;
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));
});
});
group('sorting', () {
test('severity', () {
var errors = <AnalysisError>[
AnalysisError({
'severity': 'INFO',
'location': {
'file': 'a.dart',
}
}),
AnalysisError({
'severity': 'WARNING',
'location': {
'file': 'a.dart',
}
}),
AnalysisError({
'severity': 'ERROR',
'location': {
'file': 'a.dart',
}
})
];
errors.sort();
expect(errors, hasLength(3));
expect(errors[0].isError, isTrue);
expect(errors[1].isWarning, isTrue);
expect(errors[2].isInfo, isTrue);
});
test('file', () {
var errors = <AnalysisError>[
AnalysisError({
'severity': 'INFO',
'location': {
'file': 'c.dart',
}
}),
AnalysisError({
'severity': 'INFO',
'location': {
'file': 'b.dart',
}
}),
AnalysisError({
'severity': 'INFO',
'location': {
'file': 'a.dart',
}
})
];
errors.sort();
expect(errors, hasLength(3));
expect(errors[0].file, equals('a.dart'));
expect(errors[1].file, equals('b.dart'));
expect(errors[2].file, equals('c.dart'));
});
test('offset', () {
var errors = <AnalysisError>[
AnalysisError({
'severity': 'INFO',
'location': {'file': 'a.dart', 'offset': 8}
}),
AnalysisError({
'severity': 'INFO',
'location': {'file': 'a.dart', 'offset': 6}
}),
AnalysisError({
'severity': 'INFO',
'location': {'file': 'a.dart', 'offset': 4}
})
];
errors.sort();
expect(errors, hasLength(3));
expect(errors[0].offset, equals(4));
expect(errors[1].offset, equals(6));
expect(errors[2].offset, equals(8));
});
test('message', () {
var errors = <AnalysisError>[
AnalysisError({
'severity': 'INFO',
'location': {'file': 'a.dart', 'offset': 8},
'message': 'C'
}),
AnalysisError({
'severity': 'INFO',
'location': {'file': 'a.dart', 'offset': 6},
'message': 'B'
}),
AnalysisError({
'severity': 'INFO',
'location': {'file': 'a.dart', 'offset': 4},
'message': 'A'
})
];
errors.sort();
expect(errors, hasLength(3));
expect(errors[0].message, equals('A'));
expect(errors[1].message, equals('B'));
expect(errors[2].message, equals('C'));
});
});
}
void defineAnalyze() {
late TestProject p;
tearDown(() async => await p.dispose());
test('--help', () async {
p = project();
var result = await p.run(['analyze', '--help']);
expect(result.exitCode, 0);
expect(result.stderr, isEmpty);
expect(result.stdout, contains(_analyzeDescriptionText));
expect(result.stdout, contains(_analyzeUsageText));
});
test('--help --verbose', () async {
p = project();
var result = await p.run(['analyze', '--help', '--verbose']);
expect(result.exitCode, 0);
expect(result.stderr, isEmpty);
expect(result.stdout, contains(_analyzeDescriptionText));
expect(result.stdout, contains(_analyzeVerboseUsageText));
});
group('multiple items', () {
late TestProject secondProject;
tearDown(() async => await secondProject.dispose());
test('folder and file', () async {
p = project(mainSrc: "int get foo => 'str';\n");
secondProject = project(mainSrc: "int get foo => 'str';\n");
var result = await p.run(['analyze', p.dirPath, secondProject.mainPath]);
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('2 issues found.'));
});
test('two folders', () async {
p = project(mainSrc: "int get foo => 'str';\n");
secondProject = project(mainSrc: "int get foo => 'str';\n");
var result = await p.run(['analyze', p.dirPath, secondProject.dirPath]);
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('2 issues found.'));
});
});
test('no such directory', () async {
p = project();
var result = await p.run(['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', () async {
p = project(mainSrc: 'int get foo => 1;\n');
var result = await p.run(['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', () async {
p = project(mainSrc: 'int get foo => 1;\n');
var result = await p.run(['analyze', p.dirPath]);
expect(result.exitCode, 0);
expect(result.stderr, isEmpty);
expect(result.stdout, contains('No issues found!'));
});
test('one error', () async {
p = project(mainSrc: "int get foo => 'str';\n");
var result = await p.run(['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', () async {
p = project(mainSrc: "int get foo => 'str';\nint get bar => 'str';\n");
var result = await p.run(['analyze', p.dirPath]);
expect(result.exitCode, 3);
expect(result.stderr, isEmpty);
expect(result.stdout, contains('2 issues found.'));
});
});
group('single file', () {
test('no errors', () async {
p = project(mainSrc: 'int get foo => 1;\n');
var result = await p.run(['analyze', p.mainPath]);
expect(result.exitCode, 0);
expect(result.stderr, isEmpty);
expect(result.stdout, contains('No issues found!'));
});
test('one error', () async {
p = project(mainSrc: "int get foo => 'str';\n");
var result = await p.run(['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', () async {
p = project(
mainSrc: _unusedImportCodeSnippet,
analysisOptions: _unusedImportAnalysisOptions);
var result = await p.run(['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', () async {
p = project(
mainSrc: _unusedImportCodeSnippet,
analysisOptions: _unusedImportAnalysisOptions);
var result = await p.run(['analyze', p.dirPath]);
expect(result.exitCode, equals(2));
expect(result.stderr, isEmpty);
expect(result.stdout, contains('1 issue found.'));
});
test('warning --no-fatal-warnings', () async {
p = project(
mainSrc: _unusedImportCodeSnippet,
analysisOptions: _unusedImportAnalysisOptions);
var result = await p.run(['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', () async {
p = project(mainSrc: '${dartVersionFilePrefix2_9}String foo() {}');
var result = await p.run(['analyze', p.dirPath]);
expect(result.exitCode, 0);
expect(result.stderr, isEmpty);
expect(result.stdout, contains('1 issue found.'));
});
test('info --fatal-infos', () async {
p = project(mainSrc: '${dartVersionFilePrefix2_9}String foo() {}');
var result = await p.run(['analyze', '--fatal-infos', p.dirPath]);
expect(result.exitCode, 1);
expect(result.stderr, isEmpty);
expect(result.stdout, contains('1 issue found.'));
});
test('TODOs hidden by default', () async {
p = project(
mainSrc: _todoAsWarningCodeSnippet,
);
var result = await p.run(['analyze', p.dirPath]);
expect(result.exitCode, equals(0));
expect(result.stderr, isEmpty);
expect(result.stdout, contains('No issues found!'));
});
test('TODOs shown if > INFO', () async {
p = project(
mainSrc: _todoAsWarningCodeSnippet,
analysisOptions: _todoAsWarningAnalysisOptions,
);
var result = await p.run(['analyze', p.dirPath]);
expect(result.exitCode, equals(_warningExitCode));
expect(result.stderr, isEmpty);
expect(result.stdout, contains('lib/main.dart:2:6 '));
expect(result.stdout, contains('TODO: Implement this - todo'));
expect(result.stdout, contains('lib/main.dart:3:6 '));
expect(result.stdout, contains('FIXME: Fix this - fixme'));
expect(result.stdout, contains('2 issues found.'));
});
test('--sdk-path value does not exist', () async {
p = project();
var result = await p.run(['analyze', '--sdk-path=bad']);
expect(result.exitCode, 64);
expect(result.stderr, contains('Invalid Dart SDK path: bad'));
expect(result.stderr, contains(_analyzeUsageText));
});
test('--sdk-path', () async {
var sdkPath = sdk.sdkPath;
p = project();
var result = await p.run(['analyze', '--sdk-path=$sdkPath']);
expect(result.exitCode, 0);
expect(result.stdout, contains('No issues found!'));
expect(result.stderr, isEmpty);
});
test('--enable-experiment with a bad experiment', () async {
p = project();
var result = await p.run(['analyze', '--enable-experiment=bad']);
expect(result.exitCode, 64);
expect(result.stdout, isEmpty);
expect(result.stderr, contains("Unknown experiment(s): 'bad'"));
});
test('--verbose', () async {
p = project(mainSrc: '''
int f() {
var result = one + 2;
var one = 1;
return result;
}''');
var result = await p.run(['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('--packages', () {
test('existing', () async {
final foo = project(name: 'foo');
foo.file('lib/foo.dart', 'var my_foo = 0;');
p = project(mainSrc: '''
import 'package:foo/foo.dart';
void f() {
my_foo;
}''');
p.file('my_packages.json', '''
{
"configVersion": 2,
"packages": [
{
"name": "foo",
"rootUri": "file://${foo.dirPath}",
"packageUri": "lib/",
"languageVersion": "2.12"
}
]
}
''');
var result = await p.run([
'analyze',
'--packages=${p.findFile('my_packages.json')!.path}',
p.dirPath,
]);
expect(result.exitCode, 0);
expect(result.stderr, isEmpty);
expect(result.stdout, contains('No issues found!'));
});
test('not existing', () async {
p = project();
var result = await p.run([
'analyze',
'--packages=no.such.file',
p.dirPath,
]);
expect(result.exitCode, 64);
expect(result.stderr, contains("The file doesn't exist: no.such.file"));
expect(result.stderr, contains(_analyzeUsageText));
});
});
test('--cache', () async {
var cache = project(name: 'cache');
p = project(mainSrc: 'var v = 0;');
var result = await p.run([
'analyze',
'--cache=${cache.dirPath}',
p.mainPath,
]);
expect(result.exitCode, 0);
expect(result.stderr, isEmpty);
expect(result.stdout, contains('No issues found!'));
expect(cache.findDirectory('.analysis-driver'), isNotNull);
});
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/diagnostics/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/diagnostics/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);
}
}