dart-sdk/pkg/analysis_server/test/lsp/diagnostic_test.dart
Brian Wilkerson 9db372e6f4 Make Location.endLine and Location.endColumn optional fields in the protocol
In https://dart-review.googlesource.com/c/sdk/+/191862 we added two new
required fields to `Location`. Unfortunately this was a breaking change
because plugins using an older version of the `analyzer_plugin` produce
location objects without those fields, leading to deserialization failures.

This CL makes those fields optional in order to fix the deserialization
issue.

Unfortunately, the `analyzer_plugin` package was published after the
required fields were added. Making them optional is a breaking change
because the constructor parameters go from being positional to being
named parameters.

We also neglected to update the version number of the protocol as part
of the previous CL. Technically this is also a breaking change for clients
of the analysis server, but given that they had no way to test to see
whether these fields existed they would need to have been written as if
the fields were optional in order to reference them at all, so I think
that from a practical standpoint it isn't a breaking change. That does,
however, raise the question of whether we should increment the version
numbers as part of this CL.

Change-Id: I35fc1f8e950669a3d8dd33cee6b81890261b5c47
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/206942
Reviewed-by: Danny Tuppeny <danny@tuppeny.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
2021-07-15 17:35:55 +00:00

443 lines
16 KiB
Dart

// Copyright (c) 2018, 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:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(DiagnosticTest);
});
}
@reflectiveTest
class DiagnosticTest extends AbstractLspAnalysisServerTest {
Future<void> checkPluginErrorsForFile(String pluginAnalyzedFilePath) async {
final pluginAnalyzedUri = Uri.file(pluginAnalyzedFilePath);
newFile(pluginAnalyzedFilePath, content: '''String a = "Test";
String b = "Test";
''');
await initialize();
final diagnosticsUpdate = waitForDiagnostics(pluginAnalyzedUri);
final pluginError = plugin.AnalysisError(
plugin.AnalysisErrorSeverity.ERROR,
plugin.AnalysisErrorType.STATIC_TYPE_WARNING,
plugin.Location(pluginAnalyzedFilePath, 0, 6, 0, 0,
endLine: 0, endColumn: 6),
'Test error from plugin',
'ERR1',
contextMessages: [
plugin.DiagnosticMessage(
'Related error',
plugin.Location(pluginAnalyzedFilePath, 31, 4, 1, 12,
endLine: 1, endColumn: 16))
],
);
final pluginResult =
plugin.AnalysisErrorsParams(pluginAnalyzedFilePath, [pluginError]);
configureTestPlugin(notification: pluginResult.toNotification());
final diagnostics = await diagnosticsUpdate;
expect(diagnostics, hasLength(1));
final err = diagnostics!.first;
expect(err.severity, DiagnosticSeverity.Error);
expect(err.message, equals('Test error from plugin'));
expect(err.code, equals('ERR1'));
expect(err.range.start.line, equals(0));
expect(err.range.start.character, equals(0));
expect(err.range.end.line, equals(0));
expect(err.range.end.character, equals(6));
expect(err.relatedInformation, hasLength(1));
final related = err.relatedInformation![0];
expect(related.message, equals('Related error'));
expect(related.location.range.start.line, equals(1));
expect(related.location.range.start.character, equals(12));
expect(related.location.range.end.line, equals(1));
expect(related.location.range.end.character, equals(16));
}
Future<void> test_afterDocumentEdits() async {
const initialContents = 'int a = 1;';
newFile(mainFilePath, content: initialContents);
final firstDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
await initialize();
final initialDiagnostics = await firstDiagnosticsUpdate;
expect(initialDiagnostics, hasLength(0));
await openFile(mainFileUri, initialContents);
final secondDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
await replaceFile(222, mainFileUri, 'String a = 1;');
final updatedDiagnostics = await secondDiagnosticsUpdate;
expect(updatedDiagnostics, hasLength(1));
}
Future<void> test_analysisOptionsFile() async {
newFile(analysisOptionsPath, content: '''
linter:
rules:
- invalid_lint_rule_name
''').path;
final firstDiagnosticsUpdate = waitForDiagnostics(analysisOptionsUri);
await initialize();
final initialDiagnostics = await firstDiagnosticsUpdate;
expect(initialDiagnostics, hasLength(1));
expect(initialDiagnostics!.first.severity, DiagnosticSeverity.Warning);
expect(initialDiagnostics.first.code, 'undefined_lint_warning');
}
@FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/43926')
Future<void> test_analysisOptionsFile_packageInclude() async {
newFile(analysisOptionsPath, content: '''
include: package:pedantic/analysis_options.yaml
''').path;
// Verify there's an error for the import.
final firstDiagnosticsUpdate = waitForDiagnostics(analysisOptionsUri);
await initialize();
final initialDiagnostics = await firstDiagnosticsUpdate;
expect(initialDiagnostics, hasLength(1));
expect(initialDiagnostics!.first.severity, DiagnosticSeverity.Warning);
expect(initialDiagnostics.first.code, 'include_file_not_found');
// TODO(scheglov) The server does not handle the file change.
throw 'Times out';
// // Write a package file that allows resolving the include.
// final secondDiagnosticsUpdate = waitForDiagnostics(analysisOptionsUri);
// writePackageConfig(projectFolderPath, pedantic: true);
//
// // Ensure the error disappeared.
// final updatedDiagnostics = await secondDiagnosticsUpdate;
// expect(updatedDiagnostics, hasLength(0));
}
Future<void> test_contextMessage() async {
newFile(mainFilePath, content: '''
void f() {
x = 0;
int x;
print(x);
}
''');
final diagnosticsUpdate = waitForDiagnostics(mainFileUri);
await initialize();
final diagnostics = await diagnosticsUpdate;
expect(diagnostics, hasLength(1));
final diagnostic = diagnostics!.first;
expect(diagnostic.relatedInformation, hasLength(1));
}
Future<void> test_correction() async {
newFile(mainFilePath, content: '''
void f() {
x = 0;
}
''');
final diagnosticsUpdate = waitForDiagnostics(mainFileUri);
await initialize();
final diagnostics = await diagnosticsUpdate;
expect(diagnostics, hasLength(1));
final diagnostic = diagnostics!.first;
expect(diagnostic.message, contains('\nTry'));
}
Future<void> test_deletedFile() async {
newFile(mainFilePath, content: 'String a = 1;');
final firstDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
await initialize();
final originalDiagnostics = await firstDiagnosticsUpdate;
expect(originalDiagnostics, hasLength(1));
// Deleting the file should result in an update to remove the diagnostics.
final secondDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
deleteFile(mainFilePath);
final updatedDiagnostics = await secondDiagnosticsUpdate;
expect(updatedDiagnostics, hasLength(0));
}
Future<void> test_diagnosticTag_deprecated() async {
newFile(mainFilePath, content: '''
@deprecated
int dep;
void main() => print(dep);
''');
final diagnosticsUpdate = waitForDiagnostics(mainFileUri);
await initialize(
textDocumentCapabilities: withDiagnosticTagSupport(
emptyTextDocumentClientCapabilities, [DiagnosticTag.Deprecated]));
final diagnostics = await diagnosticsUpdate;
expect(diagnostics, hasLength(1));
final diagnostic = diagnostics!.first;
expect(diagnostic.code, equals('deprecated_member_use_from_same_package'));
expect(diagnostic.tags, contains(DiagnosticTag.Deprecated));
}
Future<void> test_diagnosticTag_notSupported() async {
newFile(mainFilePath, content: '''
@deprecated
int dep;
void main() => print(dep);
''');
final diagnosticsUpdate = waitForDiagnostics(mainFileUri);
await initialize();
final diagnostics = await diagnosticsUpdate;
expect(diagnostics, hasLength(1));
final diagnostic = diagnostics!.first;
expect(diagnostic.code, equals('deprecated_member_use_from_same_package'));
expect(diagnostic.tags, isNull);
}
Future<void> test_diagnosticTag_unnecessary() async {
newFile(mainFilePath, content: '''
void main() {
return;
print('unreachable');
}
''');
final diagnosticsUpdate = waitForDiagnostics(mainFileUri);
await initialize(
textDocumentCapabilities: withDiagnosticTagSupport(
emptyTextDocumentClientCapabilities, [DiagnosticTag.Unnecessary]));
final diagnostics = await diagnosticsUpdate;
expect(diagnostics, hasLength(1));
final diagnostic = diagnostics!.first;
expect(diagnostic.code, equals('dead_code'));
expect(diagnostic.tags, contains(DiagnosticTag.Unnecessary));
}
Future<void> test_documentationUrl() async {
newFile(mainFilePath, content: '''
// ignore: unused_import
import 'dart:async' as import; // produces BUILT_IN_IDENTIFIER_IN_DECLARATION
''');
final diagnosticsUpdate = waitForDiagnostics(mainFileUri);
await initialize(
textDocumentCapabilities: withDiagnosticCodeDescriptionSupport(
emptyTextDocumentClientCapabilities));
final diagnostics = await diagnosticsUpdate;
expect(diagnostics, hasLength(1));
final diagnostic = diagnostics!.first;
expect(diagnostic.code, equals('built_in_identifier_in_declaration'));
expect(
diagnostic.codeDescription!.href,
equals('https://dart.dev/diagnostics/built_in_identifier_in_declaration'),
);
}
Future<void> test_documentationUrl_notSupported() async {
newFile(mainFilePath, content: '''
// ignore: unused_import
import 'dart:async' as import; // produces BUILT_IN_IDENTIFIER_IN_DECLARATION
''');
final diagnosticsUpdate = waitForDiagnostics(mainFileUri);
await initialize();
final diagnostics = await diagnosticsUpdate;
expect(diagnostics, hasLength(1));
final diagnostic = diagnostics!.first;
expect(diagnostic.code, equals('built_in_identifier_in_declaration'));
expect(diagnostic.codeDescription, isNull);
}
Future<void> test_dotFilesExcluded() async {
var dotFolderFilePath =
join(projectFolderPath, '.dart_tool', 'tool_file.dart');
var dotFolderFileUri = Uri.file(dotFolderFilePath);
newFile(dotFolderFilePath, content: 'String a = 1;');
List<Diagnostic>? diagnostics;
waitForDiagnostics(dotFolderFileUri).then((d) => diagnostics = d);
// Send a request for a hover.
await initialize();
await getHover(dotFolderFileUri, Position(line: 0, character: 0));
// Ensure that as part of responding to getHover, diagnostics were not
// transmitted.
expect(diagnostics, isNull);
}
Future<void> test_fixDataFile() async {
var fixDataPath = join(projectFolderPath, 'lib', 'fix_data.yaml');
var fixDataUri = Uri.file(fixDataPath);
newFile(fixDataPath, content: '''
version: latest
''').path;
final firstDiagnosticsUpdate = waitForDiagnostics(fixDataUri);
await initialize();
final initialDiagnostics = await firstDiagnosticsUpdate;
expect(initialDiagnostics, hasLength(1));
expect(initialDiagnostics!.first.severity, DiagnosticSeverity.Error);
expect(initialDiagnostics.first.code, 'invalid_value');
}
Future<void> test_fromPlugins_dartFile() async {
await checkPluginErrorsForFile(mainFilePath);
}
Future<void> test_fromPlugins_dartFile_combined() async {
// Check that if code has both a plugin and a server error, that when the
// plugin produces an error, it comes through _with_ the server-produced
// error.
// https://github.com/dart-lang/sdk/issues/45678
//
final serverErrorMessage =
"A value of type 'int' can't be assigned to a variable of type 'String'";
final pluginErrorMessage = 'Test error from plugin';
newFile(mainFilePath, content: 'String a = 1;');
final initialDiagnosticsFuture = waitForDiagnostics(mainFileUri);
await initialize();
final initialDiagnostics = await initialDiagnosticsFuture;
expect(initialDiagnostics, hasLength(1));
expect(initialDiagnostics!.first.message, contains(serverErrorMessage));
final pluginTriggeredDiagnosticFuture = waitForDiagnostics(mainFileUri);
final pluginError = plugin.AnalysisError(
plugin.AnalysisErrorSeverity.ERROR,
plugin.AnalysisErrorType.STATIC_TYPE_WARNING,
plugin.Location(mainFilePath, 0, 1, 0, 0, endLine: 0, endColumn: 1),
pluginErrorMessage,
'ERR1',
);
final pluginResult =
plugin.AnalysisErrorsParams(mainFilePath, [pluginError]);
configureTestPlugin(notification: pluginResult.toNotification());
final pluginTriggeredDiagnostics = await pluginTriggeredDiagnosticFuture;
expect(
pluginTriggeredDiagnostics!.map((error) => error.message),
containsAll([
pluginErrorMessage,
contains(serverErrorMessage),
]));
}
Future<void> test_fromPlugins_nonDartFile() async {
await checkPluginErrorsForFile(join(projectFolderPath, 'lib', 'foo.sql'));
}
Future<void> test_initialAnalysis() async {
newFile(mainFilePath, content: 'String a = 1;');
final diagnosticsUpdate = waitForDiagnostics(mainFileUri);
await initialize();
final diagnostics = await diagnosticsUpdate;
expect(diagnostics, hasLength(1));
final diagnostic = diagnostics!.first;
expect(diagnostic.code, equals('invalid_assignment'));
expect(diagnostic.range.start.line, equals(0));
expect(diagnostic.range.start.character, equals(11));
expect(diagnostic.range.end.line, equals(0));
expect(diagnostic.range.end.character, equals(12));
}
Future<void> test_looseFile_withoutPubpsec() async {
await initialize(allowEmptyRootUri: true);
// Opening the file should trigger diagnostics.
{
final diagnosticsUpdate = waitForDiagnostics(mainFileUri);
await openFile(mainFileUri, 'final a = Bad();');
final diagnostics = await diagnosticsUpdate;
expect(diagnostics, hasLength(1));
final diagnostic = diagnostics!.first;
expect(diagnostic.message, contains("The function 'Bad' isn't defined"));
}
// Closing the file should remove the diagnostics.
{
final diagnosticsUpdate = waitForDiagnostics(mainFileUri);
await closeFile(mainFileUri);
final diagnostics = await diagnosticsUpdate;
expect(diagnostics, hasLength(0));
}
}
Future<void> test_todos() async {
// TODOs only show up if there's also some code in the file.
const contents = '''
// TODO: This
String a = "";
''';
newFile(mainFilePath, content: contents);
final firstDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
await provideConfig(
() => initialize(
workspaceCapabilities:
withConfigurationSupport(emptyWorkspaceClientCapabilities)),
{'showTodos': true},
);
final initialDiagnostics = await firstDiagnosticsUpdate;
expect(initialDiagnostics, hasLength(1));
}
Future<void> test_todos_disabled() async {
const contents = '''
// TODO: This
String a = "";
''';
newFile(mainFilePath, content: contents);
final firstDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
// TODOs are disabled by default so we don't need to send any config.
await initialize();
final initialDiagnostics = await firstDiagnosticsUpdate;
expect(initialDiagnostics, hasLength(0));
}
Future<void> test_todos_enabledAfterAnalysis() async {
const contents = '''
// TODO: This
String a = "";
''';
final initialAnalysis = waitForAnalysisComplete();
final firstDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
await provideConfig(
() => initialize(
workspaceCapabilities:
withConfigurationSupport(emptyWorkspaceClientCapabilities)),
{},
);
await openFile(mainFileUri, contents);
final initialDiagnostics = await firstDiagnosticsUpdate;
expect(initialDiagnostics, hasLength(0));
// Ensure initial analysis completely finished before we continue.
await initialAnalysis;
// Enable showTodos and update the file to ensure TODOs now come through.
final secondDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
await updateConfig({'showTodos': true});
await replaceFile(222, mainFileUri, contents);
final updatedDiagnostics = await secondDiagnosticsUpdate;
expect(updatedDiagnostics, hasLength(1));
}
}