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 {