Files
sdk/pkg/analysis_server/test/lsp/document_changes_test.dart
T
Danny Tuppeny 68f0d0e3a9 [analysis_server] Fix handling of edits that require sequential processing
This moves the check for whether we need to apply edits sequentially to above some of the validation checks (because they are only applicable if not applying sequentially) and tweaks the logic slightly.

It also removes a third copy of this function in analysis_server that was only used in one test, so we're down to two copies now (analyzer_plugin and analysis_server_client).

Fixes https://github.com/dart-lang/sdk/issues/61501
Fixes https://github.com/dart-lang/sdk/issues/61488

Change-Id: I89ac94e8fd65e904c4eddcd8f1ebba3bf1293487
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/450380
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
2025-09-18 09:43:06 -07:00

393 lines
13 KiB
Dart

// 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: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 {
await _initializeAndOpen();
await changeFile(2, mainFileUri, [
TextDocumentContentChangeEvent.t1(
TextDocumentContentChangePartial(
range: Range(
start: Position(line: 0, character: 6),
end: Position(line: 0, character: 9),
),
text: 'Bar',
),
),
TextDocumentContentChangeEvent.t1(
TextDocumentContentChangePartial(
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(
SourceEdit.applySequence(content, notifiedChanges.edits),
contentAfterUpdate,
);
}
Future<void> test_documentChange_updatesOverlay() async {
await _initializeAndOpen();
await changeFile(2, mainFileUri, [
TextDocumentContentChangeEvent.t1(
TextDocumentContentChangePartial(
range: Range(
start: Position(line: 0, character: 6),
end: Position(line: 0, character: 9),
),
text: 'Bar',
),
),
TextDocumentContentChangeEvent.t1(
TextDocumentContentChangePartial(
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 {
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),
]);
await pumpEventQueue(times: 50000);
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 {
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);
}
}