// 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 test_documentChange_notifiesPlugins() async { if (!AnalysisServer.supportsPlugins) return; 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( applySequenceOfEdits(content, notifiedChanges.edits), contentAfterUpdate, ); } Future 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 test_documentClose_deletesOverlay() async { await _initializeAndOpen(); await closeFile(mainFileUri); expect(server.resourceProvider.hasOverlay(mainFilePath), isFalse); } Future test_documentClose_notifiesPlugins() async { if (!AnalysisServer.supportsPlugins) return; await _initializeAndOpen(); await closeFile(mainFileUri); expect( pluginManager.analysisUpdateContentParams!.files, equals({mainFilePath: RemoveContentOverlay()}), ); } Future 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 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 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 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 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 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 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 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 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 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 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(); // 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 _initializeAndOpen() async { await initialize(); await openFile(mainFileUri, content); } }