blob: 36eac286c2a4561e453444e4097f944f08918cac [file] [log] [blame]
// Copyright (c) 2019, 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 'dart:async';
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/protocol/protocol_internal.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' hide Position;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(DocumentChangesTest);
});
}
@reflectiveTest
class DocumentChangesTest extends AbstractLspAnalysisServerTest {
String get content => '''
class Foo {
String get bar => 'baz';
}
''';
String get contentAfterUpdate => '''
class Bar {
String get bar => 'updated';
}
''';
Future<void> test_documentChange_notifiesPlugins() async {
if (!AnalysisServer.supportsPlugins) return;
await _initializeAndOpen();
await changeFile(2, mainFileUri, [
TextDocumentContentChangeEvent.t1(TextDocumentContentChangeEvent1(
range: Range(
start: Position(line: 0, character: 6),
end: Position(line: 0, character: 9)),
text: 'Bar',
)),
TextDocumentContentChangeEvent.t1(TextDocumentContentChangeEvent1(
range: Range(
start: Position(line: 1, character: 21),
end: Position(line: 1, character: 24)),
text: 'updated',
)),
]);
var notifiedChanges = pluginManager.analysisUpdateContentParams!
.files[mainFilePath] as ChangeContentOverlay;
expect(
applySequenceOfEdits(content, notifiedChanges.edits),
contentAfterUpdate,
);
}
Future<void> test_documentChange_updatesOverlay() async {
await _initializeAndOpen();
await changeFile(2, mainFileUri, [
TextDocumentContentChangeEvent.t1(TextDocumentContentChangeEvent1(
range: Range(
start: Position(line: 0, character: 6),
end: Position(line: 0, character: 9)),
text: 'Bar',
)),
TextDocumentContentChangeEvent.t1(TextDocumentContentChangeEvent1(
range: Range(
start: Position(line: 1, character: 21),
end: Position(line: 1, character: 24)),
text: 'updated',
)),
]);
expect(server.resourceProvider.hasOverlay(mainFilePath), isTrue);
expect(server.resourceProvider.getFile(mainFilePath).readAsStringSync(),
equals(contentAfterUpdate));
}
Future<void> test_documentClose_deletesOverlay() async {
await _initializeAndOpen();
await closeFile(mainFileUri);
expect(server.resourceProvider.hasOverlay(mainFilePath), isFalse);
}
Future<void> test_documentClose_notifiesPlugins() async {
if (!AnalysisServer.supportsPlugins) return;
await _initializeAndOpen();
await closeFile(mainFileUri);
expect(pluginManager.analysisUpdateContentParams!.files,
equals({mainFilePath: RemoveContentOverlay()}));
}
Future<void>
test_documentOpen_addsOverlayOnlyToDriver_onlyIfInsideRoots() async {
// Ensures that opening a file doesn't add it to the driver if it's outside
// of the drivers root.
var fileInsideRootPath = mainFilePath;
var fileOutsideRootPath = convertPath('/home/unrelated/main.dart');
await initialize();
await openFile(pathContext.toUri(fileInsideRootPath), content);
await openFile(pathContext.toUri(fileOutsideRootPath), content);
// Expect both files return the same driver
var driverForInside = server.getAnalysisDriver(fileInsideRootPath)!;
var driverForOutside = server.getAnalysisDriver(fileOutsideRootPath)!;
expect(driverForInside, equals(driverForOutside));
// But that only the file inside the root was added.
expect(driverForInside.addedFiles, contains(fileInsideRootPath));
expect(driverForInside.addedFiles, isNot(contains(fileOutsideRootPath)));
}
Future<void> test_documentOpen_contentChanged_analysis() async {
failTestOnErrorDiagnostic = false;
const content = '// original content';
const newContent = 'new content'; // triggers diagnostic
newFile(mainFilePath, content);
await initialize();
await initialAnalysis;
await openFile(mainFileUri, newContent);
await pumpEventQueue(times: 50000);
// Expect diagnostics, because changing the content will have triggered
// analysis.
expect(diagnostics[mainFileUri], isNotEmpty);
}
Future<void> test_documentOpen_contentUnchanged_noAnalysis() async {
const content = '// original content';
newFile(mainFilePath, content);
await initialize();
await initialAnalysis;
await openFile(mainFileUri, content);
await pumpEventQueue(times: 5000);
// Expect no diagnostics because the file didn't actually change content
// when the overlay was created, so it should not have triggered analysis.
expect(diagnostics[mainFileUri], isNull);
}
Future<void> test_documentOpen_createsOverlay() async {
await _initializeAndOpen();
expect(server.resourceProvider.hasOverlay(mainFilePath), isTrue);
expect(server.resourceProvider.getFile(mainFilePath).readAsStringSync(),
equals(content));
}
/// Verify that calling open/close repeatedly produces no errors and ends
/// in the closed state.
Future<void> test_documentOpen_documentClose_repeatedly_endClosed() async {
await initialize(
// This forces roots to be rebuilt when the files open/close which
// increases the workload and makes any `await` in processing open/close
// files more likely to trigger the failure.
initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true},
);
await Future.wait([
for (var i = 0; i < 100; i++) ...[
openFile(mainFileUri, content),
closeFile(mainFileUri)
]
]);
expect(server.resourceProvider.hasOverlay(mainFilePath), isFalse);
}
/// Verify that calling open/close repeatedly produces no errors and ends
/// in the open state if the last entry was open.
Future<void> test_documentOpen_documentClose_repeatedly_endOpen() async {
await initialize(
// This forces roots to be rebuilt when the files open/close which
// increases the workload and makes any `await` in processing open/close
// files more likely to trigger the failure.
initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true},
);
await Future.wait([
for (var i = 0; i < 100; i++) ...[
openFile(mainFileUri, content),
closeFile(mainFileUri)
],
openFile(mainFileUri, content),
]);
expect(server.resourceProvider.hasOverlay(mainFilePath), isTrue);
}
/// Tests that deleting a file does not clear diagnostics while there's an
/// overlay, and that removing the overlay later clears the diagnostics.
///
/// https://github.com/dart-lang/sdk/issues/53475
Future<void> test_documentOpen_fileDeleted_documentClosed() async {
failTestOnErrorDiagnostic = false;
const content = 'error';
newFile(mainFilePath, content);
// Expect diagnostics after initial analysis because file has invalid
// content.
await initialize();
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFileUri], isNotEmpty);
// Expect diagnostics after opening the file with the same contents.
await openFile(mainFileUri, content);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFileUri], isNotEmpty);
// Expect diagnostics after deleting the file because the overlay is still
// active.
deleteFile(mainFilePath);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFileUri], isNotEmpty);
// Expect diagnostics to be removed after we close the file (which removes
// the overlay).
await closeFile(mainFileUri);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFileUri], isEmpty);
}
/// Tests that deleting and re-creating a file while an overlay is active
/// keeps the diagnotics when the overlay is then removed, then removes them
/// when the file is deleted.
///
/// https://github.com/dart-lang/sdk/issues/53475
Future<void>
test_documentOpen_fileDeleted_fileCreated_documentClosed_fileDeleted() async {
failTestOnErrorDiagnostic = false;
const content = 'error';
newFile(mainFilePath, content);
// Expect diagnostics after initial analysis because file has invalid
// content.
await initialize();
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFileUri], isNotEmpty);
// Expect diagnostics after opening the file with the same contents.
await openFile(mainFileUri, content);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFileUri], isNotEmpty);
// Expect diagnostics after deleting the file because the overlay is still
// active.
deleteFile(mainFilePath);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFileUri], isNotEmpty);
// Expect diagnostics remain after re-creating the file (the overlay is still
// active).
newFile(mainFilePath, content);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFileUri], isNotEmpty);
// Expect diagnostics remain after we close the file because the file still
//exists on disk.
await closeFile(mainFileUri);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFileUri], isNotEmpty);
// Finally, expect deleting the file clears the diagnostics.
deleteFile(mainFilePath);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFileUri], isEmpty);
}
Future<void> test_documentOpen_notifiesPlugins() async {
if (!AnalysisServer.supportsPlugins) return;
await _initializeAndOpen();
expect(pluginManager.analysisUpdateContentParams!.files,
equals({mainFilePath: AddContentOverlay(content)}));
}
/// Verifies the fix for a race condition where an overlay would not be
/// processed if the file was created on disk before the overlay was processed
/// (but the watch event had also not yet been processed).
///
/// https://github.com/dart-lang/sdk/issues/51159
Future<void> test_documentOpen_processesOverlay_dartSdk_issue51159() async {
failTestOnErrorDiagnostic = false;
var binFolder = convertPath(join(projectFolderPath, 'bin'));
var binMainFilePath = convertPath(join(binFolder, 'main.dart'));
var binMainFileUri = pathContext.toUri(binMainFilePath);
var fooFilePath = convertPath(join(binFolder, 'foo.dart'));
var fooUri = pathContext.toUri(fooFilePath);
const binMainContent = '''
import 'foo.dart';
Foo? f;
''';
const fooContent = '''
class Foo {}
''';
newFolder(binFolder);
newFile(binMainFilePath, binMainContent);
await initialize();
await initialAnalysis;
// Expect diagnostics because 'foo.dart' doesn't exist.
expect(diagnostics[binMainFileUri], isNotEmpty);
// Create the file and _immediately_ open it, so the file exists when the
// overlay is created, even though the watcher event has not been processed.
newFile(fooFilePath, fooContent);
await Future.wait([
openFile(fooUri, fooContent),
waitForAnalysisComplete(),
]);
// Expect the diagnostics have gone.
expect(diagnostics[binMainFileUri], isEmpty);
}
Future<void> test_documentOpen_setsPriorityFileIfEarly() async {
setConfigurationSupport();
// When initializing with config support, the server will call back to the client
// which can delay analysis roots being configured. This can result in files
// being opened before analysis roots are set which has previously caused the
// files not to be marked as priority on the created drivers.
// https://github.com/Dart-Code/Dart-Code/issues/2438
// https://github.com/dart-lang/sdk/issues/42994
// Initialize the server, but delay providing the configuration until after
// we've opened the file.
var completer = Completer<void>();
// Send the initialize request but do not await it.
var initResponse = initialize();
// When asked for config, delay the response until we have sent the openFile notification.
var config = provideConfig(
() => initResponse,
completer.future.then((_) => {'dart.foo': false}),
);
// Wait for initialization to finish, open the file, then allow config to complete.
await initResponse;
await openFile(mainFileUri, content);
completer.complete();
await config;
await pumpEventQueue(times: 5000);
// Ensure the opened file is in the priority list.
expect(server.getAnalysisDriver(mainFilePath)!.priorityFiles,
equals([mainFilePath]));
}
Future<void> _initializeAndOpen() async {
await initialize();
await openFile(mainFileUri, content);
}
}