// 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 'dart:async'; import 'package:analysis_server/lsp_protocol/protocol.dart'; import 'package:analysis_server/src/analysis_server.dart' hide MessageType; import 'package:analysis_server/src/lsp/constants.dart'; import 'package:analysis_server/src/lsp/server_capabilities_computer.dart'; import 'package:analysis_server/src/plugin/plugin_manager.dart'; import 'package:analysis_server/src/scheduler/message_scheduler.dart'; import 'package:analyzer/file_system/memory_file_system.dart'; import 'package:analyzer/src/util/file_paths.dart' as file_paths; import 'package:language_server_protocol/json_parsing.dart'; import 'package:test/test.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; import 'change_workspace_folders_test.dart'; import 'server_abstract.dart'; void main() { defineReflectiveSuite(() { defineReflectiveTests(InitializationTest); defineReflectiveTests(SlowInitializationTest); }); } @reflectiveTest class InitializationTest extends AbstractLspAnalysisServerTest { /// The default value of [MessageScheduler.allowOverlappingHandlers] before /// and test sets it (so that [tearDown] can revert it). final allowOverlappingHandlersDefault = MessageScheduler.allowOverlappingHandlers; /// Waits for any in-progress analysis context rebuild. /// /// Pumps the event queue before and after, to ensure any server code that /// runs after the rebuild has had chance to run. Future get contextRebuildComplete async { await pumpEventQueue(times: 5000); await server.analysisContextsRebuilt; await pumpEventQueue(times: 5000); } Future assertDynamicRegistration( String name, Set expectedResult, ) async { setTextDocumentDynamicRegistration(name); setWorkspaceDynamicRegistration(name); // Check that when the server calls client/registerCapability it only includes // the items we advertised dynamic registration support for. var registrations = []; await monitorDynamicRegistrations(registrations, initialize); var registeredMethods = registrations.map((registration) => registration.method).toSet(); var result = expectedResult.map((method) => method.toJson()).toSet(); expect(registeredMethods, equals(result)); } /// Expects successful initialization, but a window/showMessage warning /// [expectedMessage] for the given [experimentalCapabilities]. Future expectInvalidExperimentalParams( Map experimentalCapabilities, String expectedMessage, ) async { var warning = showMessageNotifications.firstWhere( (m) => m.type == MessageType.Warning, ); await initialize(experimentalCapabilities: experimentalCapabilities); var message = (await warning).message; expect( message, allOf( contains('errors parsing your experimental client capabilities'), contains(expectedMessage), ), ); } TextDocumentRegistrationOptions registrationOptionsFor( List registrations, Method method, ) { var options = registrationFor(registrations, method)?.registerOptions; if (options == null) { throw 'Registration options for $method were not found. ' 'Perhaps dynamicRegistration is missing from ' 'setAllSupportedTextDocumentDynamicRegistrations?'; } return TextDocumentRegistrationOptions.fromJson( options as Map, ); } @override Future tearDown() async { await super.tearDown(); MessageScheduler.allowOverlappingHandlers = allowOverlappingHandlersDefault; } Future test_blazeWorkspace() async { var workspacePath = '/home/user/ws'; // Make it a Blaze workspace. newFile('$workspacePath/${file_paths.blazeWorkspaceMarker}', ''); var packagePath = '$workspacePath/team/project1'; // Make it a Blaze project. newFile(convertPath('$packagePath/BUILD'), ''); var file1 = convertPath('$packagePath/lib/file1.dart'); newFile(file1, ''); await initialize(allowEmptyRootUri: true); // Expect that context manager includes a whole package. await openFile(pathContext.toUri(file1), ''); expect( server.contextManager.includedPaths, equals([convertPath(packagePath)]), ); } Future test_completionRegistrations_triggerCharacters() async { // Support dynamic registration for everything we support. setAllSupportedTextDocumentDynamicRegistrations(); setAllSupportedWorkspaceDynamicRegistrations(); var registrations = []; var initResponse = await monitorDynamicRegistrations( registrations, () => initialize(), ); var initResult = InitializeResult.fromJson( initResponse.result as Map, ); expect(initResult.capabilities, isNotNull); // Check Dart-only registration. var dartRegistration = registrationForDart( registrations, Method.textDocument_completion, ); var dartOptions = CompletionRegistrationOptions.fromJson( dartRegistration.registerOptions as Map, ); expect(dartOptions.documentSelector, hasLength(1)); expect(dartOptions.documentSelector![0].language, dartLanguageId); expect(dartOptions.triggerCharacters, isNotEmpty); // Check non-Dart registration. var nonDartRegistration = registrations.singleWhere( (r) => r.method == Method.textDocument_completion.toJson() && r != dartRegistration, ); var nonDartOptions = CompletionRegistrationOptions.fromJson( nonDartRegistration.registerOptions as Map, ); var otherLanguages = nonDartOptions.documentSelector! .map((selector) => selector.language) .toList(); expect(otherLanguages, isNot(contains('dart'))); expect(nonDartOptions.triggerCharacters, isNull); } Future test_completionRegistrations_withDartPlugin() async { if (!AnalysisServer.supportsPlugins) return; // This tests for a bug that occurred with an analysis server plugin // that works on Dart files. When computing completion registrations we // usually have separate registrations for Dart + non-Dart to account for // different trigger characters. However, the plugin types were all being // included in the non-Dart registration even if they included Dart. // // The result was two registrations including Dart, which caused duplicate // requests for Dart completions, which resulted in duplicate items // appearing in the editor. setAllSupportedTextDocumentDynamicRegistrations(); // Track all current registrations. var registrations = []; // Perform normal registration (without plugins) to get the initial set. await monitorDynamicRegistrations(registrations, initialize); // Expect only a single registration that includes Dart files. expect( registrationsForDart(registrations, Method.textDocument_completion), hasLength(1), ); // Monitor the unregistration/new registrations during the plugin activation. await monitorDynamicReregistration(registrations, () async { var plugin = configureTestPlugin(); plugin.currentSession = PluginSession(plugin) ..interestingFiles = ['*.dart']; pluginManager.pluginsChangedController.add(null); }); // Expect that there is still only a single registration for Dart. expect( registrationsForDart(registrations, Method.textDocument_completion), hasLength(1), ); } Future test_dynamicRegistration_areNotInterleaved() async { if (!AnalysisServer.supportsPlugins) return; // Some of the issues in https://github.com/dart-lang/sdk/issues/47851 // (duplicate hovers/code actions/etc.) were caused by duplicate // registrations. This happened when we tried to rebuild registrations // concurrently as the code would: // // 1. compute the new set of registrations // 2. compute which had changed // 3. send _and await_ the unregistration // 4. send _and await_ the new registration // // If the code was triggered multiple times concurrently, it was possible // for step 3 in second request to occur before step 4 in the first, which // would result in unregistrations being sent for registrations that had // not been sent. They would be silently dropped, and then we'd end up with // multiple registrations. // // This test triggers the rebuild via pluginManager.pluginsChangedController // multiple times and ensures the events all arrive in the correct order, // that is, no unregistration ever contains the ID of something that is not // currently registered. setAllSupportedTextDocumentDynamicRegistrations(); var registrations = []; await monitorDynamicRegistrations(registrations, initialize); var knownRegistrationsIds = registrations.map((registration) => registration.id).toSet(); var numberOfReregistrations = 10; // Listen to incoming registrations, ensure they're not in the set, and add // them. var registrationsDone = requestsFromServer .where((n) => n.method == Method.client_registerCapability) .take(numberOfReregistrations) .listen((request) { respondTo(request, null); var registrations = RegistrationParams.fromJson( request.params as Map, ).registrations; for (var registration in registrations) { var id = registration.id; if (!knownRegistrationsIds.add(id)) { throw 'Registration $id was already in the existing set!'; } } }) .asFuture(); // Listen to incoming unregistrations, verify they're in the set, and remove // them. var unregistrationsDone = requestsFromServer .where((n) => n.method == Method.client_unregisterCapability) .take(numberOfReregistrations) .listen((request) { respondTo(request, null); var unregistrations = UnregistrationParams.fromJson( request.params as Map, ).unregisterations; for (var unregistration in unregistrations) { var id = unregistration.id; if (!knownRegistrationsIds.remove(id)) { throw 'Registration $id was not in the existing set!'; } } }) .asFuture(); // Trigger multiple plugin events that will rebuild the registrations. for (var i = 0; i < numberOfReregistrations; i++) { var plugin = configureTestPlugin(); plugin.currentSession = PluginSession(plugin); // Ensure they have different file types so the registrations change, // otherwise they will be optimised out as not changing. plugin.currentSession!.interestingFiles = ['*.foo$i']; pluginManager.pluginsChangedController.add(null); await null; // Allow the server to begin processing the change. } // Wait for both streams to have handled all of the numberOfReregistrations // expected events (without throwing). await Future.wait([registrationsDone, unregistrationsDone]); } Future test_dynamicRegistration_config_allHierarchy() => assertDynamicRegistration('callHierarchy', { Method.textDocument_prepareCallHierarchy, }); Future test_dynamicRegistration_config_codeAction() => assertDynamicRegistration('codeAction', {Method.textDocument_codeAction}); Future test_dynamicRegistration_config_codeLens() => assertDynamicRegistration('codeLens', {Method.textDocument_codeLens}); Future test_dynamicRegistration_config_colorProvider() => assertDynamicRegistration('colorProvider', { Method.textDocument_documentColor, }); Future test_dynamicRegistration_config_completion() => assertDynamicRegistration('completion', {Method.textDocument_completion}); Future test_dynamicRegistration_config_definition() => assertDynamicRegistration('definition', {Method.textDocument_definition}); Future test_dynamicRegistration_config_didChangeConfiguration() => assertDynamicRegistration('didChangeConfiguration', { Method.workspace_didChangeConfiguration, }); Future test_dynamicRegistration_config_documentHighlight() => assertDynamicRegistration('documentHighlight', { Method.textDocument_documentHighlight, }); Future test_dynamicRegistration_config_documentLink() => assertDynamicRegistration('documentLink', { Method.textDocument_documentLink, }); Future test_dynamicRegistration_config_documentSymbol() => assertDynamicRegistration('documentSymbol', { Method.textDocument_documentSymbol, }); Future test_dynamicRegistration_config_fileOperations() => assertDynamicRegistration('fileOperations', { Method.workspace_willRenameFiles, }); Future test_dynamicRegistration_config_foldingRange() => assertDynamicRegistration('foldingRange', { Method.textDocument_foldingRange, }); Future test_dynamicRegistration_config_formatting() => assertDynamicRegistration('formatting', {Method.textDocument_formatting}); Future test_dynamicRegistration_config_hover() => assertDynamicRegistration('hover', {Method.textDocument_hover}); Future test_dynamicRegistration_config_implementation() => assertDynamicRegistration('implementation', { Method.textDocument_implementation, }); Future test_dynamicRegistration_config_inlayHint() => assertDynamicRegistration('inlayHint', {Method.textDocument_inlayHint}); Future test_dynamicRegistration_config_onTypeFormatting() => assertDynamicRegistration('onTypeFormatting', { Method.textDocument_onTypeFormatting, }); Future test_dynamicRegistration_config_rangeFormatting() => assertDynamicRegistration('rangeFormatting', { Method.textDocument_rangeFormatting, }); Future test_dynamicRegistration_config_references() => assertDynamicRegistration('references', {Method.textDocument_references}); Future test_dynamicRegistration_config_rename() => assertDynamicRegistration('rename', {Method.textDocument_rename}); Future test_dynamicRegistration_config_selectionRange() => assertDynamicRegistration('selectionRange', { Method.textDocument_selectionRange, }); Future test_dynamicRegistration_config_semanticTokens() => assertDynamicRegistration('semanticTokens', { CustomMethods.semanticTokenDynamicRegistration, }); Future test_dynamicRegistration_config_signatureHelp() => assertDynamicRegistration('signatureHelp', { Method.textDocument_signatureHelp, }); Future test_dynamicRegistration_config_synchronization() => assertDynamicRegistration('synchronization', { Method.textDocument_didOpen, Method.textDocument_didChange, Method.textDocument_didClose, }); Future test_dynamicRegistration_config_typeDefinition() => assertDynamicRegistration('typeDefinition', { Method.textDocument_typeDefinition, }); Future test_dynamicRegistration_config_typeHierarchy() => assertDynamicRegistration('typeHierarchy', { Method.textDocument_prepareTypeHierarchy, }); Future test_dynamicRegistration_containsAppropriateSettings() async { // Support file operations. setFileOperationDynamicRegistration(); // Support dynamic registration for both text sync + hovers. setTextSyncDynamicRegistration(); setHoverDynamicRegistration(); // Basic check that the server responds with the capabilities we'd expect, // for ex including analysis_options.yaml in text synchronization but not // for hovers. var registrations = []; var initResponse = await monitorDynamicRegistrations( registrations, initialize, ); // Because we support dynamic registration for synchronization, we won't send // static registrations for them. // https://github.com/dart-lang/sdk/issues/38490 var initResult = InitializeResult.fromJson( initResponse.result as Map, ); expect(initResult.serverInfo!.name, 'Dart SDK LSP Analysis Server'); expect(initResult.serverInfo!.version, isNotNull); expect(initResult.capabilities, isNotNull); expect(initResult.capabilities.textDocumentSync, isNull); // Should contain Hover, DidOpen, DidClose, DidChange, WillRenameFiles. expect(registrations, hasLength(5)); var hover = registrationOptionsFor( registrations, Method.textDocument_hover, ); var change = registrationOptionsFor( registrations, Method.textDocument_didChange, ); var rename = FileOperationRegistrationOptions.fromJson( registrationFor( registrations, Method.workspace_willRenameFiles, )?.registerOptions as Map, ); expect( registrationOptionsFor(registrations, Method.textDocument_didOpen), isNotNull, ); expect( registrationOptionsFor(registrations, Method.textDocument_didClose), isNotNull, ); // The hover capability should only specific Dart. expect(hover, isNotNull); expect(hover.documentSelector, hasLength(1)); expect(hover.documentSelector!.single.language, equals('dart')); // didChange should also include pubspec + analysis_options. expect(change, isNotNull); expect(change.documentSelector, hasLength(greaterThanOrEqualTo(3))); expect(change.documentSelector!.any((ds) => ds.language == 'dart'), isTrue); expect( change.documentSelector!.any((ds) => ds.pattern == '**/pubspec.yaml'), isTrue, ); expect( change.documentSelector!.any( (ds) => ds.pattern == '**/analysis_options.yaml', ), isTrue, ); expect(rename, equals(fileOperationRegistrationOptions)); } Future test_dynamicRegistration_notSupportedByClient() async { // If the client doesn't send any dynamicRegistration settings then there // should be no `client/registerCapability` calls. // Set a flag if any registerCapability request comes through. var didGetRegisterCapabilityRequest = false; requestsFromServer .where((n) => n.method == Method.client_registerCapability) .listen((_) => didGetRegisterCapabilityRequest = true); // Initialize with no dynamic registrations advertised. var initResponse = await initialize(); await pumpEventQueue(); var initResult = InitializeResult.fromJson( initResponse.result as Map, ); expect(initResult.capabilities, isNotNull); // When dynamic registration is not supported, we will always statically // request text document open/close and incremental updates. expect(initResult.capabilities.textDocumentSync, isNotNull); initResult.capabilities.textDocumentSync!.map( (_) => throw 'Expected textDocumentSync capabilities to be a TextDocumentSyncOptions', (options) { expect(options.openClose, isTrue); expect(options.change, equals(TextDocumentSyncKind.Incremental)); }, ); expect(initResult.capabilities.callHierarchyProvider, isNotNull); expect(initResult.capabilities.completionProvider, isNotNull); expect(initResult.capabilities.hoverProvider, isNotNull); expect(initResult.capabilities.signatureHelpProvider, isNotNull); expect(initResult.capabilities.referencesProvider, isNotNull); expect(initResult.capabilities.colorProvider, isNotNull); expect(initResult.capabilities.documentHighlightProvider, isNotNull); expect(initResult.capabilities.documentFormattingProvider, isNotNull); expect(initResult.capabilities.documentOnTypeFormattingProvider, isNotNull); expect(initResult.capabilities.documentRangeFormattingProvider, isNotNull); expect(initResult.capabilities.definitionProvider, isNotNull); expect(initResult.capabilities.codeActionProvider, isNotNull); expect(initResult.capabilities.renameProvider, isNotNull); expect(initResult.capabilities.foldingRangeProvider, isNotNull); expect( initResult.capabilities.workspace!.fileOperations!.willRename, equals(fileOperationRegistrationOptions), ); expect(initResult.capabilities.selectionRangeProvider, isNotNull); expect(initResult.capabilities.semanticTokensProvider, isNotNull); expect(didGetRegisterCapabilityRequest, isFalse); } Future test_dynamicRegistration_suppressesStaticRegistration() async { // Support dynamic registration for everything we support. setAllSupportedTextDocumentDynamicRegistrations(); setAllSupportedWorkspaceDynamicRegistrations(); // If the client sends dynamicRegistration settings then there // should not be static registrations for the same capabilities. var registrations = []; var initResponse = await monitorDynamicRegistrations( registrations, initialize, ); var initResult = InitializeResult.fromJson( initResponse.result as Map, ); expect(initResult.capabilities, isNotNull); // Ensure no static registrations. This list should include all server equivalents // of the dynamic registrations listed in `ClientDynamicRegistrations.supported`. expect(initResult.capabilities.textDocumentSync, isNull); expect(initResult.capabilities.callHierarchyProvider, isNull); expect(initResult.capabilities.completionProvider, isNull); expect(initResult.capabilities.hoverProvider, isNull); expect(initResult.capabilities.inlayHintProvider, isNull); expect(initResult.capabilities.signatureHelpProvider, isNull); expect(initResult.capabilities.referencesProvider, isNull); expect(initResult.capabilities.colorProvider, isNull); expect(initResult.capabilities.documentHighlightProvider, isNull); expect(initResult.capabilities.documentFormattingProvider, isNull); expect(initResult.capabilities.documentOnTypeFormattingProvider, isNull); expect(initResult.capabilities.documentRangeFormattingProvider, isNull); expect(initResult.capabilities.definitionProvider, isNull); expect(initResult.capabilities.codeActionProvider, isNull); expect(initResult.capabilities.renameProvider, isNull); expect(initResult.capabilities.foldingRangeProvider, isNull); expect(initResult.capabilities.workspace!.fileOperations, isNull); expect(initResult.capabilities.selectionRangeProvider, isNull); expect(initResult.capabilities.semanticTokensProvider, isNull); // Ensure all expected dynamic registrations. for (var expectedRegistration in ClientDynamicRegistrations.supported) { // We have two completion registrations (to handle different trigger // characters), so exclude that here and check it manually below. if (expectedRegistration == Method.textDocument_completion) { continue; } var registration = registrationOptionsFor( registrations, expectedRegistration, ); expect( registration, isNotNull, reason: 'Missing dynamic registration for $expectedRegistration', ); } // Check the were two completion registrations. var completionRegistrations = registrations.where( (reg) => reg.method == Method.textDocument_completion.toJson(), ); expect(completionRegistrations, hasLength(2)); } Future test_dynamicRegistration_unregistersOutdatedAfterChange() async { if (!AnalysisServer.supportsPlugins) return; // Initialize by supporting dynamic registrations everywhere setAllSupportedTextDocumentDynamicRegistrations(); var registrations = []; await monitorDynamicRegistrations(registrations, initialize); var unregisterRequest = await expectRequest( Method.client_unregisterCapability, () { var plugin = configureTestPlugin(); plugin.currentSession = PluginSession(plugin) ..interestingFiles = ['*.foo']; pluginManager.pluginsChangedController.add(null); }, ); var unregistrations = UnregistrationParams.fromJson( unregisterRequest.params as Map, ).unregisterations; // folding method should have been unregistered as the server now supports // *.foo files for it as well. var registrationIdForFolding = registrations .singleWhere((r) => r.method == 'textDocument/foldingRange') .id; expect( unregistrations, contains( isA() .having((r) => r.method, 'method', 'textDocument/foldingRange') .having((r) => r.id, 'id', registrationIdForFolding), ), ); } Future test_dynamicRegistration_updatesWithPlugins() async { if (!AnalysisServer.supportsPlugins) return; setTextDocumentDynamicRegistration('foldingRange'); await initialize(); // The server will send an unregister request followed by another register // request to change document filter on folding. We need to respond to the // unregister request as the server awaits that. // This is set up as a future callback and should not be awaited here. unawaited( requestsFromServer .firstWhere((r) => r.method == Method.client_unregisterCapability) .then((request) { respondTo(request, null); }), ); var request = await expectRequest(Method.client_registerCapability, () { var plugin = configureTestPlugin(); plugin.currentSession = PluginSession(plugin) ..interestingFiles = ['*.sql']; pluginManager.pluginsChangedController.add(null); }); var registrations = RegistrationParams.fromJson( request.params as Map, ).registrations; var documentFilterSql = TextDocumentFilterScheme( scheme: 'file', pattern: '**/*.sql', ); var documentFilterDart = TextDocumentFilterScheme( language: 'dart', scheme: 'file', ); expect( registrations, contains( isA() .having((r) => r.method, 'method', 'textDocument/foldingRange') .having( (r) => TextDocumentRegistrationOptions.fromJson( r.registerOptions as Map, ).documentSelector, 'registerOptions.documentSelector', containsAll([documentFilterSql, documentFilterDart]), ), ), ); } /// Tests that when there are no explicit analysis roots (instead they are /// implied by open files), requests for open files are successful even if /// sent _immediately_ after opening the file. /// /// https://github.com/Dart-Code/Dart-Code/issues/3929 /// /// This test uses getDocumentSymbols which requires a resolved result. Future test_emptyAnalysisRoots_handlesFileRequestsImmediately_resolved() async { const content = 'void f() {}'; var file1 = join(projectFolderPath, 'file1.dart'); var file1Uri = pathContext.toUri(file1); newFile(file1, content); newPubspecYamlFile(projectFolderPath, ''); await initialize(allowEmptyRootUri: true); unawaited(openFile(file1Uri, content)); // Don't wait var result = await getDocumentSymbols(file1Uri); var symbols = result.map( (docSymbols) => docSymbols, (symbolInfos) => symbolInfos, ); expect(symbols, hasLength(1)); } /// Tests that when there are no explicit analysis roots (instead they are /// implied by open files), requests for open files are successful even if /// sent _immediately_ after opening the file. /// /// https://github.com/Dart-Code/Dart-Code/issues/3929 /// /// This test uses getSelectionRanges which requires only a parsed result. Future test_emptyAnalysisRoots_handlesFileRequestsImmediately_unresolved() async { const content = 'void f() {}'; var file1 = join(projectFolderPath, 'file1.dart'); var file1Uri = pathContext.toUri(file1); newFile(file1, content); newPubspecYamlFile(projectFolderPath, ''); await initialize(allowEmptyRootUri: true); unawaited(openFile(file1Uri, content)); // Don't wait var result = await getSelectionRanges(file1Uri, [startOfDocPos]); expect(result, hasLength(1)); } Future test_emptyAnalysisRoots_multipleOpenFiles() async { var file1 = join(projectFolderPath, 'file1.dart'); var file1Uri = pathContext.toUri(file1); newFile(file1, ''); var file2 = join(projectFolderPath, 'file2.dart'); var file2Uri = pathContext.toUri(file2); newFile(file2, ''); newPubspecYamlFile(projectFolderPath, ''); await initialize(allowEmptyRootUri: true); // Opening both files should only add the project folder once. await openFile(file1Uri, ''); await openFile(file2Uri, ''); await contextRebuildComplete; expect(server.contextManager.includedPaths, equals([projectFolderPath])); // Closing only one of the files should not remove the project folder // since there are still open files. resetContextBuildCounter(); await closeFile(file1Uri); await pumpEventQueue(times: 5000); expect(server.contextManager.includedPaths, equals([projectFolderPath])); expectNoContextBuilds(); // Closing the last file should remove the project folder and remove // the context. resetContextBuildCounter(); await closeFile(file2Uri); await contextRebuildComplete; expect(server.contextManager.includedPaths, equals([])); expect(server.contextManager.driverMap, hasLength(0)); expectContextBuilds(); } Future test_emptyAnalysisRoots_projectWithoutPubspec() async { projectFolderPath = convertPath('/home/empty'); var nestedFilePath = join( projectFolderPath, 'nested', 'deeply', 'in', 'folders', 'test.dart', ); var nestedFileUri = pathContext.toUri(nestedFilePath); newFile(nestedFilePath, ''); // The project folder shouldn't be added to start with. await initialize(allowEmptyRootUri: true); expect(server.contextManager.includedPaths, equals([])); // Opening the file will add a root for it. await openFile(nestedFileUri, ''); expect(server.contextManager.includedPaths, equals([nestedFilePath])); } Future test_emptyAnalysisRoots_projectWithPubspec() async { var nestedFilePath = join( projectFolderPath, 'nested', 'deeply', 'in', 'folders', 'test.dart', ); var nestedFileUri = pathContext.toUri(nestedFilePath); newFile(nestedFilePath, ''); newPubspecYamlFile(projectFolderPath, ''); // The project folder shouldn't be added to start with. await initialize(allowEmptyRootUri: true); expect(server.contextManager.includedPaths, equals([])); // Opening a file nested within the project should add the project folder. await openFile(nestedFileUri, ''); await contextRebuildComplete; expect(server.contextManager.includedPaths, equals([projectFolderPath])); // Ensure the file was cached in each driver. This happens as a result of // adding to priority files, but if that's done before the file is in an // analysis root it will not occur. // https://github.com/dart-lang/sdk/issues/37338 for (var driver in server.driverMap.values) { expect(driver.getCachedResolvedUnit(nestedFilePath), isNotNull); } // Closing the file should remove it. await closeFile(nestedFileUri); expect(server.contextManager.includedPaths, equals([])); } Future test_excludedFolders_absolute() async { var excludedFolderPath = join(projectFolderPath, 'excluded'); await provideConfig( initialize, // Exclude the folder with a relative path. { 'analysisExcludedFolders': [excludedFolderPath], }, ); expect(server.contextManager.includedPaths, equals([projectFolderPath])); expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); } Future test_excludedFolders_nonList() async { var excludedFolderPath = join(projectFolderPath, 'excluded'); await provideConfig( initialize, // Include a single string instead of an array since it's an easy mistake // to make without editor validation of settings. {'analysisExcludedFolders': 'excluded'}, ); expect(server.contextManager.includedPaths, equals([projectFolderPath])); expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); } Future test_excludedFolders_relative() async { var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb'); await provideConfig( initialize, // Exclude the folder with a relative path. { 'analysisExcludedFolders': ['aaa', 'bbb'].join(pathContext.separator), }, ); expect(server.contextManager.includedPaths, equals([projectFolderPath])); expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); } /// Check exclusions using explicit posix paths even if we're running /// on Windows. It's common for shared configs in VS Code to always use /// forward slashes in paths. Future test_excludedFolders_relative_posix() async { var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb'); await provideConfig( initialize, // Exclude the folder with a relative path using forward slashes. { 'analysisExcludedFolders': ['aaa', 'bbb'].join('/'), }, ); expect(server.contextManager.includedPaths, equals([projectFolderPath])); expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); } Future test_excludedFolders_trailingSlash() async { var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb'); await provideConfig( initialize, // Exclude the folder with a trailing slash. { 'analysisExcludedFolders': [ 'aaa', 'bbb', '', ].join(pathContext.separator), }, ); expect(server.contextManager.includedPaths, equals([projectFolderPath])); expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); } /// Check exclusions using explicit posix paths even if we're running /// on Windows. It's common for shared configs in VS Code to always use /// forward slashes in paths. Future test_excludedFolders_trailingSlash_posix() async { var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb'); await provideConfig( initialize, // Exclude the folder with a forward trailing slash. { 'analysisExcludedFolders': ['aaa', 'bbb', ''].join('/'), }, ); expect(server.contextManager.includedPaths, equals([projectFolderPath])); expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); } /// Tests that requests that requires a unit result are handled correctly even /// if sent immediately after the `initialized` notification and do not result /// in "File not Analyzed"-style errors because roots are set asynchronously. Future test_immediateRequests() async { newFile(mainFilePath, 'void f() {}'); late Future, List>> result; await provideConfig( () => initialize( immediatelyAfterInitialized: () { result = getDocumentSymbols(mainFileUri); }, ), {}, ); var symbols = (await result).map( (docSymbols) => docSymbols, (symbolInfos) => symbolInfos, ); expect(symbols, hasLength(1)); } Future test_initialize() async { var response = await initialize(); expect(response, isNotNull); expect(response.error, isNull); expect(response.result, isNotNull); expect( InitializeResult.canParse(response.result, nullLspJsonReporter), isTrue, ); var result = InitializeResult.fromJson( response.result as Map, ); expect(result.capabilities, isNotNull); // Check some basic capabilities that are unlikely to change. expect(result.capabilities.textDocumentSync, isNotNull); result.capabilities.textDocumentSync!.map( (_) => throw 'Expected textDocumentSync capabilities to be a TextDocumentSyncOptions', (options) { // We'll always request open/closed notifications and incremental updates. expect(options.openClose, isTrue); expect(options.change, equals(TextDocumentSyncKind.Incremental)); }, ); } Future test_initialize_invalidParams() async { var params = {'processId': 'invalid'}; var request = RequestMessage( id: Either2.t1(1), method: Method.initialize, params: params, jsonrpc: jsonRpcVersion, ); var response = await sendRequestToServer(request); expect(response.id, equals(request.id)); expect(response.error, isNotNull); expect(response.error!.code, equals(ErrorCodes.InvalidParams)); expect(response.result, isNull); } Future test_initialize_onlyAllowedOnce() async { await initialize(); var response = await initialize(throwOnFailure: false); expect(response, isNotNull); expect(response.result, isNull); expect(response.error, isNotNull); expect( response.error!.code, equals(ServerErrorCodes.ServerAlreadyInitialized), ); } Future test_initialize_rootPath() async { await initialize(rootPath: projectFolderPath); expect(server.contextManager.includedPaths, equals([projectFolderPath])); } Future test_initialize_rootPath_trailingSlash() async { await initialize(rootPath: withTrailingSlash(projectFolderPath)); expect(server.contextManager.includedPaths, equals([projectFolderPath])); } Future test_initialize_rootUri() async { await initialize(rootUri: projectFolderUri); expect(server.contextManager.includedPaths, equals([projectFolderPath])); } Future test_initialize_rootUri_encodedDriveLetterColon() async { await initialize(rootUri: withEncodedDriveLetterColon(projectFolderUri)); expect(server.contextManager.includedPaths, equals([projectFolderPath])); } Future test_initialize_rootUri_trailingSlash() async { await initialize(rootUri: withTrailingSlashUri(projectFolderUri)); expect(server.contextManager.includedPaths, equals([projectFolderPath])); } Future test_initialize_workspaceFolders() async { await initialize(workspaceFolders: [projectFolderUri]); expect(server.contextManager.includedPaths, equals([projectFolderPath])); } Future test_initialize_workspaceFolders_encodedDriveLetterColon() async { await initialize( workspaceFolders: [withEncodedDriveLetterColon(projectFolderUri)], ); expect(server.contextManager.includedPaths, equals([projectFolderPath])); } Future test_initialize_workspaceFolders_trailingSlash() async { await initialize( workspaceFolders: [withTrailingSlashUri(projectFolderUri)], ); expect(server.contextManager.includedPaths, equals([projectFolderPath])); } Future test_interleavedRequests_default() async { await initialize(rootUri: projectFolderUri, initializationOptions: {}); expect( MessageScheduler.allowOverlappingHandlers, allowOverlappingHandlersDefault, ); } Future test_interleavedRequests_explicitFalse() async { await initialize( rootUri: projectFolderUri, initializationOptions: {'allowOverlappingHandlers': false}, ); expect(MessageScheduler.allowOverlappingHandlers, isFalse); } Future test_interleavedRequests_explicitTrue() async { await initialize( rootUri: projectFolderUri, initializationOptions: {'allowOverlappingHandlers': true}, ); expect(MessageScheduler.allowOverlappingHandlers, isTrue); } Future test_invalidExperimental_commands() async { await expectInvalidExperimentalParams({ 'commands': 1, }, 'ClientCapabilities.experimental.commands must be a List?'); } Future test_invalidExperimental_dartCodeAction() async { await expectInvalidExperimentalParams( {'dartCodeAction': 1}, 'ClientCapabilities.experimental.dartCodeAction must be a Map?', ); } Future test_invalidExperimental_dartCodeAction_commandParameterSupport() async { await expectInvalidExperimentalParams( { 'dartCodeAction': {'commandParameterSupport': 1}, }, 'ClientCapabilities.experimental.dartCodeAction.commandParameterSupport must be a Map?', ); } Future test_invalidExperimental_dartCodeAction_commandParameterSupport_supportedKinds() async { await expectInvalidExperimentalParams( { 'dartCodeAction': { 'commandParameterSupport': {'supportedKinds': 1}, }, }, 'ClientCapabilities.experimental.dartCodeAction.commandParameterSupport.supportedKinds must be a List?', ); } Future test_invalidExperimental_snippetTextEdit() async { await expectInvalidExperimentalParams({ 'snippetTextEdit': 1, }, 'ClientCapabilities.experimental.snippetTextEdit must be a bool?'); } Future test_invalidExperimental_supportsDartTextDocumentContentProvider() async { await expectInvalidExperimentalParams( {'supportsDartTextDocumentContentProvider': 1}, 'ClientCapabilities.experimental.supportsDartTextDocumentContentProvider must be a bool?', ); } Future test_nonFileScheme_rootUri() async { // We expect an error notification about the invalid file we try to open. failTestOnAnyErrorNotification = false; var rootUri = Uri.parse('vsls://'); var fileUri = rootUri.replace(path: '/file1.dart'); await initialize(rootUri: rootUri); expect(server.contextManager.includedPaths, equals([])); // Also open a non-file file to ensure it doesn't cause the root to be added. await openFile(fileUri, ''); expect(server.contextManager.includedPaths, equals([])); } /// Verifies a workspace that contains non-file workspace folders is handled. /// /// Related tests for didChangeWorkspaceFolders are in /// [ChangeWorkspaceFoldersTest]. Future test_nonFileScheme_workspaceFolders() async { // We expect an error notification about the invalid file we try to open. failTestOnAnyErrorNotification = false; newPubspecYamlFile(projectFolderPath, ''); var rootUri = Uri.parse('vsls://'); var fileUri = rootUri.replace(path: '/file1.dart'); await initialize( workspaceFolders: [rootUri, pathContext.toUri(projectFolderPath)], ); expect(server.contextManager.includedPaths, equals([projectFolderPath])); // Also open a non-file file to ensure it doesn't cause the root to be added. await openFile(fileUri, ''); expect(server.contextManager.includedPaths, equals([projectFolderPath])); } Future test_nonProjectFiles_basicWorkspace() async { var file1 = convertPath('/home/nonProject/file1.dart'); newFile(file1, ''); await initialize(allowEmptyRootUri: true); // Because the file is not in a project, it should be added itself. await openFile(pathContext.toUri(file1), ''); expect(server.contextManager.includedPaths, equals([file1])); } Future test_nonProjectFiles_blazeWorkspace() async { var file1 = convertPath('/home/nonProject/file1.dart'); newFile(file1, ''); // Make /home a Blaze workspace. newFile('/home/${file_paths.blazeWorkspaceMarker}', ''); await initialize(allowEmptyRootUri: true); // Because the file is not in a project, it should be added itself. await openFile(pathContext.toUri(file1), ''); expect(server.contextManager.includedPaths, equals([file1])); } Future test_onlyAnalyzeProjectsWithOpenFiles_fullyInitializes() async { // Enable some dynamic registrations, else registerCapability will not // be called. setAllSupportedTextDocumentDynamicRegistrations(); // Ensure when we use onlyAnalyzeProjectsWithOpenFiles that we still // fully initialize (eg. capabilities are registered). projectFolderPath = convertPath('/home/empty'); await expectRequest( Method.client_registerCapability, () => initialize( rootUri: projectFolderUri, initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true}, ), ); } Future test_onlyAnalyzeProjectsWithOpenFiles_multipleFiles() async { var file1 = join(projectFolderPath, 'file1.dart'); var file1Uri = pathContext.toUri(file1); newFile(file1, ''); var file2 = join(projectFolderPath, 'file2.dart'); var file2Uri = pathContext.toUri(file2); newFile(file2, ''); newPubspecYamlFile(projectFolderPath, ''); await initialize( rootUri: projectFolderUri, initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true}, ); // Opening both files should only add the project folder once. await openFile(file1Uri, ''); await openFile(file2Uri, ''); await contextRebuildComplete; expect(server.contextManager.includedPaths, equals([projectFolderPath])); expect(server.contextManager.driverMap, hasLength(1)); // Closing only one of the files should not remove the root or rebuild the context. resetContextBuildCounter(); await closeFile(file1Uri); await pumpEventQueue(times: 5000); expect(server.contextManager.includedPaths, equals([projectFolderPath])); expect(server.contextManager.driverMap, hasLength(1)); expectNoContextBuilds(); // Closing the last file should remove the project folder and remove // the context. resetContextBuildCounter(); await closeFile(file2Uri); await contextRebuildComplete; expect(server.contextManager.includedPaths, equals([])); expect(server.contextManager.driverMap, hasLength(0)); expectContextBuilds(); } Future test_onlyAnalyzeProjectsWithOpenFiles_withoutPubpsec() async { projectFolderPath = convertPath('/home/empty'); var nestedFilePath = join( projectFolderPath, 'nested', 'deeply', 'in', 'folders', 'test.dart', ); var nestedFileUri = pathContext.toUri(nestedFilePath); newFile(nestedFilePath, ''); // The project folder shouldn't be added to start with. await initialize( rootUri: projectFolderUri, initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true}, ); expect(server.contextManager.includedPaths, equals([])); // Opening the file should trigger it to be added. await openFile(nestedFileUri, ''); expect(server.contextManager.includedPaths, equals([nestedFilePath])); } Future test_onlyAnalyzeProjectsWithOpenFiles_withPubpsec() async { var nestedFilePath = join( projectFolderPath, 'nested', 'deeply', 'in', 'folders', 'test.dart', ); var nestedFileUri = pathContext.toUri(nestedFilePath); newFile(nestedFilePath, ''); newPubspecYamlFile(projectFolderPath, ''); // The project folder shouldn't be added to start with. await initialize( rootUri: projectFolderUri, initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true}, ); expect(server.contextManager.includedPaths, equals([])); // Opening a file nested within the project should cause the project folder // to be added await openFile(nestedFileUri, ''); await contextRebuildComplete; expect(server.contextManager.includedPaths, equals([projectFolderPath])); // Ensure the file was cached in each driver. This happens as a result of // adding to priority files, but if that's done before the file is in an // analysis root it will not occur. // https://github.com/dart-lang/sdk/issues/37338 for (var driver in server.driverMap.values) { expect(driver.getCachedResolvedUnit(nestedFilePath), isNotNull); } // Closing the file should remove it. await closeFile(nestedFileUri); expect(server.contextManager.includedPaths, equals([])); } Future test_survey_alwaysEnabledForLsp() async { await initialize(); expect(server.surveyManager, isNotNull); } Future test_uninitialized_dropsNotifications() async { var notification = makeNotification( Method.fromJson('randomNotification'), null, ); var nextNotification = errorNotificationsFromServer.first; channel.sendNotificationToServer(notification); // Wait up to 1sec to ensure no error/log notifications were sent back. var didTimeout = false; var notificationFromServer = await nextNotification .then((notification) => notification) .timeout( const Duration(seconds: 1), onTimeout: () { didTimeout = true; return null; }, ); expect(notificationFromServer, isNull); expect(didTimeout, isTrue); } Future test_uninitialized_rejectsRequests() async { var request = makeRequest(Method.fromJson('randomRequest'), null); var response = await channel.sendRequestToServer(request); expect(response.id, equals(request.id)); expect(response.result, isNull); expect(response.error, isNotNull); expect(response.error!.code, ErrorCodes.ServerNotInitialized); } } /// Runs all initialization tests with a resource provider that slowly /// initializes watchers to simulate delays in real fs watchers. @reflectiveTest class SlowInitializationTest extends InitializationTest { @override MemoryResourceProvider resourceProvider = MemoryResourceProvider( // Force the in-memory file watchers to be slowly initialized to emulate // the physical watchers (for test_concurrentContextRebuilds). delayWatcherInitialization: Duration(milliseconds: 1), ); }