406 lines
14 KiB
Dart
406 lines
14 KiB
Dart
// Copyright (c) 2017, 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 'package:analyzer/file_system/file_system.dart';
|
|
import 'package:analyzer/src/dart/analysis/byte_store.dart';
|
|
import 'package:analyzer/src/dart/analysis/driver.dart'
|
|
show AnalysisDriverScheduler, PerformanceLog;
|
|
import 'package:analyzer/src/dart/analysis/file_byte_store.dart';
|
|
import 'package:analyzer/src/dart/analysis/file_state.dart';
|
|
import 'package:analyzer/src/generated/source.dart';
|
|
import 'package:analyzer_plugin/channel/channel.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol_constants.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol_generated.dart';
|
|
import 'package:analyzer_plugin/src/protocol/protocol_internal.dart';
|
|
import 'package:analyzer_plugin/src/utilities/null_string_sink.dart';
|
|
import 'package:analyzer_plugin/utilities/subscription_manager.dart';
|
|
import 'package:pub_semver/pub_semver.dart';
|
|
|
|
/**
|
|
* The abstract superclass of any class implementing a plugin for the analysis
|
|
* server.
|
|
*
|
|
* Clients may not implement or mix-in this class, but are expected to extend
|
|
* it.
|
|
*/
|
|
abstract class ServerPlugin {
|
|
/**
|
|
* A megabyte.
|
|
*/
|
|
static const int M = 1024 * 1024;
|
|
|
|
/**
|
|
* The communication channel being used to communicate with the analysis
|
|
* server.
|
|
*/
|
|
PluginCommunicationChannel _channel;
|
|
|
|
/**
|
|
* The resource provider used to access the file system.
|
|
*/
|
|
final ResourceProvider resourceProvider;
|
|
|
|
/**
|
|
* The object used to manage analysis subscriptions.
|
|
*/
|
|
final SubscriptionManager subscriptionManager = new SubscriptionManager();
|
|
|
|
/**
|
|
* The scheduler used by any analysis drivers that are created.
|
|
*/
|
|
AnalysisDriverScheduler analysisDriverScheduler;
|
|
|
|
/**
|
|
* The performance log used by any analysis drivers that are created.
|
|
*/
|
|
final PerformanceLog performanceLog =
|
|
new PerformanceLog(new NullStringSink());
|
|
|
|
/**
|
|
* The byte store used by any analysis drivers that are created, or `null` if
|
|
* the cache location isn't known because the 'plugin.version' request has not
|
|
* yet been received.
|
|
*/
|
|
ByteStore _byteStore;
|
|
|
|
/**
|
|
* The file content overlay used by any analysis drivers that are created.
|
|
*/
|
|
final FileContentOverlay fileContentOverlay = new FileContentOverlay();
|
|
|
|
/**
|
|
* Initialize a newly created analysis server plugin. If a resource [provider]
|
|
* is given, then it will be used to access the file system. Otherwise a
|
|
* resource provider that accesses the physical file system will be used.
|
|
*/
|
|
ServerPlugin(this.resourceProvider) {
|
|
analysisDriverScheduler = new AnalysisDriverScheduler(performanceLog);
|
|
}
|
|
|
|
/**
|
|
* Return the communication channel being used to communicate with the
|
|
* analysis server, or `null` if the plugin has not been started.
|
|
*/
|
|
PluginCommunicationChannel get channel => _channel;
|
|
|
|
/**
|
|
* Return the user visible information about how to contact the plugin authors
|
|
* with any problems that are found, or `null` if there is no contact info.
|
|
*/
|
|
String get contactInfo => null;
|
|
|
|
/**
|
|
* Return a list of glob patterns selecting the files that this plugin is
|
|
* interested in analyzing.
|
|
*/
|
|
List<String> get fileGlobsToAnalyze;
|
|
|
|
/**
|
|
* Return the user visible name of this plugin.
|
|
*/
|
|
String get name;
|
|
|
|
/**
|
|
* Return the version number of this plugin, encoded as a string.
|
|
*/
|
|
String get version;
|
|
|
|
/**
|
|
* Handle the fact that the file with the given [path] has been modified.
|
|
*/
|
|
void contentChanged(String path) {
|
|
// Ignore changes to files.
|
|
}
|
|
|
|
/**
|
|
* Handle an 'analysis.handleWatchEvents' request.
|
|
*/
|
|
AnalysisHandleWatchEventsResult handleAnalysisHandleWatchEvents(
|
|
Map<String, Object> parameters) =>
|
|
null;
|
|
|
|
/**
|
|
* Handle an 'analysis.reanalyze' request.
|
|
*/
|
|
AnalysisReanalyzeResult handleAnalysisReanalyze(
|
|
Map<String, Object> parameters) =>
|
|
null;
|
|
|
|
/**
|
|
* Handle an 'analysis.setContextBuilderOptions' request.
|
|
*/
|
|
AnalysisSetContextBuilderOptionsResult handleAnalysisSetContextBuilderOptions(
|
|
Map<String, Object> parameters) =>
|
|
null;
|
|
|
|
/**
|
|
* Handle an 'analysis.setContextRoots' request.
|
|
*/
|
|
AnalysisSetContextRootsResult handleAnalysisSetContextRoots(
|
|
Map<String, Object> parameters) {
|
|
// TODO(brianwilkerson) Implement this so that implementors don't have to
|
|
// figure out how to manage contexts.
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Handle an 'analysis.setPriorityFiles' request.
|
|
*/
|
|
AnalysisSetPriorityFilesResult handleAnalysisSetPriorityFiles(
|
|
Map<String, Object> parameters) =>
|
|
new AnalysisSetPriorityFilesResult();
|
|
|
|
/**
|
|
* Handle an 'analysis.setSubscriptions' request. Most subclasses should not
|
|
* override this method, but should instead use the [subscriptionManager] to
|
|
* access the list of subscriptions for any given file.
|
|
*/
|
|
AnalysisSetSubscriptionsResult handleAnalysisSetSubscriptions(
|
|
Map<String, Object> parameters) {
|
|
Map<AnalysisService, List<String>> subscriptions = validateParameter(
|
|
parameters,
|
|
ANALYSIS_REQUEST_SET_SUBSCRIPTIONS_SUBSCRIPTIONS,
|
|
'analysis.setSubscriptions');
|
|
subscriptionManager.setSubscriptions(subscriptions);
|
|
// TODO(brianwilkerson) Cause any newly subscribed for notifications to be sent.
|
|
return new AnalysisSetSubscriptionsResult();
|
|
}
|
|
|
|
/**
|
|
* Handle an 'analysis.updateContent' request. Most subclasses should not
|
|
* override this method, but should instead use the [contentCache] to access
|
|
* the current content of overlaid files.
|
|
*/
|
|
AnalysisUpdateContentResult handleAnalysisUpdateContent(
|
|
Map<String, Object> parameters) {
|
|
Map<String, Object> files = validateParameter(parameters,
|
|
ANALYSIS_REQUEST_UPDATE_CONTENT_FILES, 'analysis.updateContent');
|
|
files.forEach((String filePath, Object overlay) {
|
|
// We don't need to get the correct URI because only the full path is
|
|
// used by the contentCache.
|
|
Source source = resourceProvider.getFile(filePath).createSource();
|
|
if (overlay is AddContentOverlay) {
|
|
fileContentOverlay[source.fullName] = overlay.content;
|
|
} else if (overlay is ChangeContentOverlay) {
|
|
String fileName = source.fullName;
|
|
String oldContents = fileContentOverlay[fileName];
|
|
String newContents;
|
|
if (oldContents == null) {
|
|
// The server should only send a ChangeContentOverlay if there is
|
|
// already an existing overlay for the source.
|
|
throw new RequestFailure(new RequestError(
|
|
RequestErrorCode.INVALID_OVERLAY_CHANGE,
|
|
'Invalid overlay change: no content to change'));
|
|
}
|
|
try {
|
|
newContents = SourceEdit.applySequence(oldContents, overlay.edits);
|
|
} on RangeError {
|
|
throw new RequestFailure(new RequestError(
|
|
RequestErrorCode.INVALID_OVERLAY_CHANGE,
|
|
'Invalid overlay change: invalid edit'));
|
|
}
|
|
fileContentOverlay[fileName] = newContents;
|
|
} else if (overlay is RemoveContentOverlay) {
|
|
fileContentOverlay[source.fullName] = null;
|
|
}
|
|
contentChanged(filePath);
|
|
});
|
|
return new AnalysisUpdateContentResult();
|
|
}
|
|
|
|
/**
|
|
* Handle a 'completion.getSuggestions' request.
|
|
*/
|
|
CompletionGetSuggestionsResult handleCompletionGetSuggestions(
|
|
Map<String, Object> parameters) =>
|
|
new CompletionGetSuggestionsResult(
|
|
-1, -1, const <CompletionSuggestion>[]);
|
|
|
|
/**
|
|
* Handle an 'edit.getAssists' request.
|
|
*/
|
|
EditGetAssistsResult handleEditGetAssists(Map<String, Object> parameters) =>
|
|
new EditGetAssistsResult(const <PrioritizedSourceChange>[]);
|
|
|
|
/**
|
|
* Handle an 'edit.getAvailableRefactorings' request. Subclasses that override
|
|
* this method in order to participate in refactorings must also override the
|
|
* method [handleEditGetRefactoring].
|
|
*/
|
|
EditGetAvailableRefactoringsResult handleEditGetAvailableRefactorings(
|
|
Map<String, Object> parameters) =>
|
|
new EditGetAvailableRefactoringsResult(const <RefactoringKind>[]);
|
|
|
|
/**
|
|
* Handle an 'edit.getFixes' request.
|
|
*/
|
|
EditGetFixesResult handleEditGetFixes(Map<String, Object> parameters) =>
|
|
new EditGetFixesResult(const <AnalysisErrorFixes>[]);
|
|
|
|
/**
|
|
* Handle an 'edit.getRefactoring' request.
|
|
*/
|
|
EditGetRefactoringResult handleEditGetRefactoring(
|
|
Map<String, Object> parameters) =>
|
|
null;
|
|
|
|
/**
|
|
* Handle a 'plugin.shutdown' request. Subclasses can override this method to
|
|
* perform any required clean-up, but cannot prevent the plugin from shutting
|
|
* down.
|
|
*/
|
|
PluginShutdownResult handlePluginShutdown(Map<String, Object> parameters) =>
|
|
new PluginShutdownResult();
|
|
|
|
/**
|
|
* Handle a 'plugin.versionCheck' request.
|
|
*/
|
|
PluginVersionCheckResult handlePluginVersionCheck(
|
|
Map<String, Object> parameters) {
|
|
String byteStorePath = validateParameter(parameters,
|
|
PLUGIN_REQUEST_VERSION_CHECK_BYTESTOREPATH, 'plugin.versionCheck');
|
|
String versionString = validateParameter(parameters,
|
|
PLUGIN_REQUEST_VERSION_CHECK_VERSION, 'plugin.versionCheck');
|
|
Version serverVersion = new Version.parse(versionString);
|
|
_byteStore =
|
|
new MemoryCachingByteStore(new FileByteStore(byteStorePath), 64 * M);
|
|
return new PluginVersionCheckResult(
|
|
isCompatibleWith(serverVersion), name, version, fileGlobsToAnalyze,
|
|
contactInfo: contactInfo);
|
|
}
|
|
|
|
/**
|
|
* Return `true` if this plugin is compatible with an analysis server that is
|
|
* using the given version of the plugin API.
|
|
*/
|
|
bool isCompatibleWith(Version serverVersion) =>
|
|
serverVersion <= new Version.parse(version);
|
|
|
|
/**
|
|
* The method that is called when the analysis server closes the communication
|
|
* channel. This method will not be invoked under normal conditions because
|
|
* the server will send a shutdown request and the plugin will stop listening
|
|
* to the channel before the server closes the channel.
|
|
*/
|
|
void onDone() {}
|
|
|
|
/**
|
|
* The method that is called when an error has occurred in the analysis
|
|
* server. This method will not be invoked under normal conditions.
|
|
*/
|
|
void onError(Object exception, StackTrace stackTrace) {}
|
|
|
|
/**
|
|
* Start this plugin by listening to the given communication [channel].
|
|
*/
|
|
void start(PluginCommunicationChannel channel) {
|
|
this._channel = channel;
|
|
_channel.listen(_onRequest, onError: onError, onDone: onDone);
|
|
}
|
|
|
|
/**
|
|
* Validate that the value in the map of [parameters] at the given [key] is of
|
|
* the type [T]. If it is, return it. Otherwise throw a [RequestFailure] that
|
|
* will cause an error to be returned to the server.
|
|
*/
|
|
Object/*=T*/ validateParameter/*<T>*/(
|
|
Map<String, Object> parameters, String key, String requestName) {
|
|
Object value = parameters[key];
|
|
// ignore: type_annotation_generic_function_parameter
|
|
if (value is Object/*=T*/) {
|
|
return value;
|
|
}
|
|
String message;
|
|
if (value == null) {
|
|
message = 'Missing parameter $key in $requestName';
|
|
} else {
|
|
message = 'Invalid value for $key in $requestName (${value.runtimeType})';
|
|
}
|
|
throw new RequestFailure(
|
|
new RequestError(RequestErrorCode.INVALID_PARAMETER, message));
|
|
}
|
|
|
|
/**
|
|
* Compute the response that should be returned for the given [request], or
|
|
* `null` if the response has already been sent.
|
|
*/
|
|
Response _getResponse(Request request) {
|
|
ResponseResult result = null;
|
|
switch (request.method) {
|
|
case ANALYSIS_REQUEST_HANDLE_WATCH_EVENTS:
|
|
result = handleAnalysisHandleWatchEvents(request.params);
|
|
break;
|
|
case ANALYSIS_REQUEST_REANALYZE:
|
|
result = handleAnalysisReanalyze(request.params);
|
|
break;
|
|
case ANALYSIS_REQUEST_SET_CONTEXT_BUILDER_OPTIONS:
|
|
result = handleAnalysisSetContextBuilderOptions(request.params);
|
|
break;
|
|
case ANALYSIS_REQUEST_SET_CONTEXT_ROOTS:
|
|
result = handleAnalysisSetContextRoots(request.params);
|
|
break;
|
|
case ANALYSIS_REQUEST_SET_PRIORITY_FILES:
|
|
result = handleAnalysisSetPriorityFiles(request.params);
|
|
break;
|
|
case ANALYSIS_REQUEST_SET_SUBSCRIPTIONS:
|
|
result = handleAnalysisSetSubscriptions(request.params);
|
|
break;
|
|
case ANALYSIS_REQUEST_UPDATE_CONTENT:
|
|
result = handleAnalysisUpdateContent(request.params);
|
|
break;
|
|
case COMPLETION_REQUEST_GET_SUGGESTIONS:
|
|
result = handleCompletionGetSuggestions(request.params);
|
|
break;
|
|
case EDIT_REQUEST_GET_ASSISTS:
|
|
result = handleEditGetAssists(request.params);
|
|
break;
|
|
case EDIT_REQUEST_GET_AVAILABLE_REFACTORINGS:
|
|
result = handleEditGetAvailableRefactorings(request.params);
|
|
break;
|
|
case EDIT_REQUEST_GET_FIXES:
|
|
result = handleEditGetFixes(request.params);
|
|
break;
|
|
case EDIT_REQUEST_GET_REFACTORING:
|
|
result = handleEditGetRefactoring(request.params);
|
|
break;
|
|
case PLUGIN_REQUEST_SHUTDOWN:
|
|
result = handlePluginShutdown(request.params);
|
|
_channel.sendResponse(result.toResponse(request.id));
|
|
_channel.close();
|
|
return null;
|
|
case PLUGIN_REQUEST_VERSION_CHECK:
|
|
result = handlePluginVersionCheck(request.params);
|
|
break;
|
|
}
|
|
if (result == null) {
|
|
return new Response(request.id,
|
|
error: RequestErrorFactory.unknownRequest(request));
|
|
}
|
|
return result.toResponse(request.id);
|
|
}
|
|
|
|
/**
|
|
* The method that is called when a [request] is received from the analysis
|
|
* server.
|
|
*/
|
|
void _onRequest(Request request) {
|
|
String id = request.id;
|
|
Response response;
|
|
try {
|
|
response = _getResponse(request);
|
|
} on RequestFailure catch (exception) {
|
|
_channel.sendResponse(new Response(id, error: exception.error));
|
|
} catch (exception, stackTrace) {
|
|
response = new Response(id,
|
|
error: new RequestError(
|
|
RequestErrorCode.PLUGIN_ERROR, exception.toString(),
|
|
stackTrace: stackTrace.toString()));
|
|
}
|
|
if (response != null) {
|
|
_channel.sendResponse(response);
|
|
}
|
|
}
|
|
}
|