DAS plugins: support overlay changes This adds and tests support for adding file overlays, changing them, and removing them. Work towards https://github.com/dart-lang/sdk/issues/53402 Change-Id: I7939f8199639f7e336cd97515c2f7d48b1d777a0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/384305 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Commit-Queue: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analysis_server_plugin/lib/src/plugin_server.dart b/pkg/analysis_server_plugin/lib/src/plugin_server.dart index 9df1ebb..045be72 100644 --- a/pkg/analysis_server_plugin/lib/src/plugin_server.dart +++ b/pkg/analysis_server_plugin/lib/src/plugin_server.dart
@@ -69,6 +69,9 @@ /// The recent state of analysis reults, to be cleared on file changes. final _recentState = <String, _PluginState>{}; + /// The next modification stamp for a changed file in the [resourceProvider]. + int _overlayModificationStamp = 0; + PluginServer({ required ResourceProvider resourceProvider, required List<Plugin> plugins, @@ -79,29 +82,6 @@ } } - /// Handles an 'analysis.setContextRoots' request. - Future<protocol.AnalysisSetContextRootsResult> handleAnalysisSetContextRoots( - protocol.AnalysisSetContextRootsParams parameters) async { - var currentContextCollection = _contextCollection; - if (currentContextCollection != null) { - _contextCollection = null; - await currentContextCollection.dispose(); - } - - var includedPaths = parameters.roots.map((e) => e.root).toList(); - var contextCollection = AnalysisContextCollectionImpl( - resourceProvider: _resourceProvider, - includedPaths: includedPaths, - byteStore: _byteStore, - sdkPath: _sdkPath, - fileContentCache: FileContentCache(_resourceProvider), - ); - _contextCollection = contextCollection; - await _analyzeAllFilesInContextCollection( - contextCollection: contextCollection); - return protocol.AnalysisSetContextRootsResult(); - } - /// Handles an 'edit.getFixes' request. /// /// Throws a [RequestFailure] if the request could not be handled. @@ -193,8 +173,6 @@ /// This method is invoked when a new instance of [AnalysisContextCollection] /// is created, so the plugin can perform initial analysis of analyzed files. - /// - /// By default analyzes every [AnalysisContext] with [_analyzeFiles]. Future<void> _analyzeAllFilesInContextCollection({ required AnalysisContextCollection contextCollection, }) async { @@ -326,27 +304,36 @@ case protocol.ANALYSIS_REQUEST_GET_NAVIGATION: case protocol.ANALYSIS_REQUEST_HANDLE_WATCH_EVENTS: result = null; + case protocol.ANALYSIS_REQUEST_SET_CONTEXT_ROOTS: var params = protocol.AnalysisSetContextRootsParams.fromRequest(request); - result = await handleAnalysisSetContextRoots(params); + result = await _handleAnalysisSetContextRoots(params); + case protocol.ANALYSIS_REQUEST_SET_PRIORITY_FILES: case protocol.ANALYSIS_REQUEST_SET_SUBSCRIPTIONS: case protocol.ANALYSIS_REQUEST_UPDATE_CONTENT: + var params = protocol.AnalysisUpdateContentParams.fromRequest(request); + result = await _handleAnalysisUpdateContent(params); + case protocol.COMPLETION_REQUEST_GET_SUGGESTIONS: case protocol.EDIT_REQUEST_GET_ASSISTS: case protocol.EDIT_REQUEST_GET_AVAILABLE_REFACTORINGS: result = null; + case protocol.EDIT_REQUEST_GET_FIXES: var params = protocol.EditGetFixesParams.fromRequest(request); result = await handleEditGetFixes(params); + case protocol.EDIT_REQUEST_GET_REFACTORING: result = null; + case protocol.PLUGIN_REQUEST_SHUTDOWN: _channel.sendResponse(protocol.PluginShutdownResult() .toResponse(request.id, requestTime)); _channel.close(); return null; + case protocol.PLUGIN_REQUEST_VERSION_CHECK: var params = protocol.PluginVersionCheckParams.fromRequest(request); result = await handlePluginVersionCheck(params); @@ -358,6 +345,119 @@ return result.toResponse(request.id, requestTime); } + /// Handles files that might have been affected by a content change of + /// one or more files. The implementation may check if these files should + /// be analyzed, do such analysis, and send diagnostics. + /// + /// By default invokes [_analyzeFiles] only for files that are analyzed in + /// this [analysisContext]. + Future<void> _handleAffectedFiles({ + required AnalysisContext analysisContext, + required List<String> paths, + }) async { + var analyzedPaths = paths + .where(analysisContext.contextRoot.isAnalyzed) + .toList(growable: false); + + await _analyzeFiles( + analysisContext: analysisContext, + paths: analyzedPaths, + ); + } + + /// Handles an 'analysis.setContextRoots' request. + Future<protocol.AnalysisSetContextRootsResult> _handleAnalysisSetContextRoots( + protocol.AnalysisSetContextRootsParams parameters) async { + var currentContextCollection = _contextCollection; + if (currentContextCollection != null) { + _contextCollection = null; + await currentContextCollection.dispose(); + } + + var includedPaths = parameters.roots.map((e) => e.root).toList(); + var contextCollection = AnalysisContextCollectionImpl( + resourceProvider: _resourceProvider, + includedPaths: includedPaths, + byteStore: _byteStore, + sdkPath: _sdkPath, + fileContentCache: FileContentCache(_resourceProvider), + ); + _contextCollection = contextCollection; + await _analyzeAllFilesInContextCollection( + contextCollection: contextCollection); + return protocol.AnalysisSetContextRootsResult(); + } + + /// Handles an 'analysis.updateContent' request. + /// + /// Throws a [RequestFailure] if the request could not be handled. + Future<protocol.AnalysisUpdateContentResult> _handleAnalysisUpdateContent( + protocol.AnalysisUpdateContentParams parameters) async { + var changedPaths = <String>{}; + var paths = parameters.files; + paths.forEach((String path, Object overlay) { + // Prepare the old overlay contents. + String? oldContent; + try { + if (_resourceProvider.hasOverlay(path)) { + oldContent = _resourceProvider.getFile(path).readAsStringSync(); + } + } catch (_) { + // Leave `oldContent` empty. + } + + // Prepare the new contents. + String? newContent; + if (overlay is protocol.AddContentOverlay) { + newContent = overlay.content; + } else if (overlay is protocol.ChangeContentOverlay) { + if (oldContent == null) { + // The server should only send a ChangeContentOverlay if there is + // already an existing overlay for the source. + throw RequestFailure( + RequestErrorFactory.invalidOverlayChangeNoContent()); + } + try { + newContent = + protocol.SourceEdit.applySequence(oldContent, overlay.edits); + } on RangeError { + throw RequestFailure( + RequestErrorFactory.invalidOverlayChangeInvalidEdit()); + } + } else if (overlay is protocol.RemoveContentOverlay) { + newContent = null; + } + + if (newContent != null) { + _resourceProvider.setOverlay( + path, + content: newContent, + modificationStamp: _overlayModificationStamp++, + ); + } else { + _resourceProvider.removeOverlay(path); + } + + changedPaths.add(path); + }); + await _handleContentChanged(changedPaths.toList()); + return protocol.AnalysisUpdateContentResult(); + } + + /// Handles the fact that files with [paths] were changed. + Future<void> _handleContentChanged(List<String> paths) async { + if (_contextCollection case var contextCollection?) { + await _forAnalysisContexts(contextCollection, (analysisContext) async { + for (var path in paths) { + analysisContext.changeFile(path); + } + var affected = await analysisContext.applyPendingFileChanges(); + await _handleAffectedFiles( + analysisContext: analysisContext, paths: affected); + }); + } + } + Future<void> _handleRequest(Request request) async { var requestTime = DateTime.now().millisecondsSinceEpoch; var id = request.id;
diff --git a/pkg/analysis_server_plugin/test/src/plugin_server_test.dart b/pkg/analysis_server_plugin/test/src/plugin_server_test.dart index 2f36449..c2e2679 100644 --- a/pkg/analysis_server_plugin/test/src/plugin_server_test.dart +++ b/pkg/analysis_server_plugin/test/src/plugin_server_test.dart
@@ -12,6 +12,7 @@ import 'package:analyzer_plugin/protocol/protocol_generated.dart' as protocol; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; +import 'package:async/async.dart'; import 'package:test/test.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; @@ -24,6 +25,12 @@ @reflectiveTest class PluginServerTest extends PluginServerTestBase { + protocol.ContextRoot get contextRoot => protocol.ContextRoot(packagePath, []); + + String get filePath => join(packagePath, 'lib', 'test.dart'); + + String get packagePath => convertPath('/package1'); + @override Future<void> setUp() async { await super.setUp(); @@ -34,35 +41,20 @@ } Future<void> test_handleAnalysisSetContextRoots() async { - var packagePath = convertPath('/package1'); - var filePath = join(packagePath, 'lib', 'test.dart'); newFile(filePath, 'bool b = false;'); - var contextRoot = protocol.ContextRoot(packagePath, []); - await pluginServer.handleAnalysisSetContextRoots( - protocol.AnalysisSetContextRootsParams([contextRoot])); + await channel + .sendRequest(protocol.AnalysisSetContextRootsParams([contextRoot])); var notification = await channel.notifications.first; var params = protocol.AnalysisErrorsParams.fromNotification(notification); expect(params.file, convertPath('/package1/lib/test.dart')); expect(params.errors, hasLength(1)); - - expect( - params.errors.single, - isA<protocol.AnalysisError>() - .having((e) => e.severity, 'severity', - protocol.AnalysisErrorSeverity.INFO) - .having( - (e) => e.type, 'type', protocol.AnalysisErrorType.STATIC_WARNING) - .having((e) => e.message, 'message', 'No bools message'), - ); + _expectAnalysisError(params.errors.single, message: 'No bools message'); } Future<void> test_handleEditGetFixes() async { - var packagePath = convertPath('/package1'); - var filePath = join(packagePath, 'lib', 'test.dart'); newFile(filePath, 'bool b = false;'); - var contextRoot = protocol.ContextRoot(packagePath, []); - await pluginServer.handleAnalysisSetContextRoots( - protocol.AnalysisSetContextRootsParams([contextRoot])); + await channel + .sendRequest(protocol.AnalysisSetContextRootsParams([contextRoot])); var result = await pluginServer.handleEditGetFixes( protocol.EditGetFixesParams(filePath, 'bool b = '.length)); @@ -73,6 +65,101 @@ expect(fixes, hasLength(1)); expect(fixes[0].fixes, hasLength(1)); } + + Future<void> test_updateContent_addOverlay() async { + newFile(filePath, 'int b = 7;'); + await channel + .sendRequest(protocol.AnalysisSetContextRootsParams([contextRoot])); + + var notifications = StreamQueue(channel.notifications); + var notification = await notifications.next; + var params = protocol.AnalysisErrorsParams.fromNotification(notification); + expect(params.file, convertPath('/package1/lib/test.dart')); + expect(params.errors, isEmpty); + + await channel.sendRequest(protocol.AnalysisUpdateContentParams( + {filePath: protocol.AddContentOverlay('bool b = false;')})); + + notification = await notifications.next; + params = protocol.AnalysisErrorsParams.fromNotification(notification); + expect(params.file, convertPath('/package1/lib/test.dart')); + expect(params.errors, hasLength(1)); + _expectAnalysisError(params.errors.single, message: 'No bools message'); + } + + Future<void> test_updateContent_changeOverlay() async { + newFile(filePath, 'int b = 7;'); + await channel + .sendRequest(protocol.AnalysisSetContextRootsParams([contextRoot])); + + var notifications = StreamQueue(channel.notifications); + var notification = await notifications.next; + var params = protocol.AnalysisErrorsParams.fromNotification(notification); + expect(params.file, convertPath('/package1/lib/test.dart')); + expect(params.errors, isEmpty); + + await channel.sendRequest(protocol.AnalysisUpdateContentParams( + {filePath: protocol.AddContentOverlay('int b = 0;')})); + + notification = await notifications.next; + params = protocol.AnalysisErrorsParams.fromNotification(notification); + expect(params.file, convertPath('/package1/lib/test.dart')); + expect(params.errors, isEmpty); + + await channel.sendRequest(protocol.AnalysisUpdateContentParams({ + filePath: protocol.ChangeContentOverlay( + [protocol.SourceEdit(0, 9, 'bool b = false')]) + })); + + notification = await notifications.next; + params = protocol.AnalysisErrorsParams.fromNotification(notification); + expect(params.file, convertPath('/package1/lib/test.dart')); + expect(params.errors, hasLength(1)); + _expectAnalysisError(params.errors.single, message: 'No bools message'); + } + + Future<void> test_updateContent_removeOverlay() async { + newFile(filePath, 'bool b = false;'); + await channel + .sendRequest(protocol.AnalysisSetContextRootsParams([contextRoot])); + + var notifications = StreamQueue(channel.notifications); + var notification = await notifications.next; + var params = protocol.AnalysisErrorsParams.fromNotification(notification); + expect(params.file, convertPath('/package1/lib/test.dart')); + expect(params.errors, hasLength(1)); + _expectAnalysisError(params.errors.single, message: 'No bools message'); + + await channel.sendRequest(protocol.AnalysisUpdateContentParams( + {filePath: protocol.AddContentOverlay('int b = 7;')})); + + notification = await notifications.next; + params = protocol.AnalysisErrorsParams.fromNotification(notification); + expect(params.file, convertPath('/package1/lib/test.dart')); + expect(params.errors, isEmpty); + + await channel.sendRequest(protocol.AnalysisUpdateContentParams( + {filePath: protocol.RemoveContentOverlay()})); + + notification = await notifications.next; + params = protocol.AnalysisErrorsParams.fromNotification(notification); + expect(params.file, convertPath('/package1/lib/test.dart')); + expect(params.errors, hasLength(1)); + _expectAnalysisError(params.errors.single, message: 'No bools message'); + } + + void _expectAnalysisError(protocol.AnalysisError error, + {required String message}) { + expect( + error, + isA<protocol.AnalysisError>() + .having((e) => e.severity, 'severity', + protocol.AnalysisErrorSeverity.INFO) + .having( + (e) => e.type, 'type', protocol.AnalysisErrorType.STATIC_WARNING) + .having((e) => e.message, 'message', message), + ); + } } class _NoBoolsPlugin extends Plugin {