blob: 263ca2ddf91d98ddf8aec2c4299dc206a550a88e [file] [log] [blame]
// 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);
}
}
}