blob: a6475ff41bd8559834040c0dbf71c835723a57f3 [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 AnalysisDriverGeneric, 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/subscriptions/subscription_manager.dart';
import 'package:path/src/context.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;
/**
* A table mapping the current context roots to the analysis driver created
* for that root.
*/
final Map<ContextRoot, AnalysisDriverGeneric> driverMap =
<ContextRoot, AnalysisDriverGeneric>{};
/**
* 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);
analysisDriverScheduler.start();
}
/**
* Return 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 get byteStore => _byteStore;
/**
* 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.
}
/**
* Return the context root containing the file at the given [filePath].
*/
ContextRoot contextRootContaining(String filePath) {
Context pathContext = resourceProvider.pathContext;
/**
* Return `true` if the given [child] is either the same as or within the
* given [parent].
*/
bool isOrWithin(String parent, String child) {
return parent == child || pathContext.isWithin(parent, child);
}
/**
* Return `true` if the given context [root] contains the target [file].
*/
bool ownsFile(ContextRoot root) {
if (isOrWithin(root.root, filePath)) {
List<String> excludedPaths = root.exclude;
for (String excludedPath in excludedPaths) {
if (isOrWithin(excludedPath, filePath)) {
return false;
}
}
return true;
}
return false;
}
for (ContextRoot root in driverMap.keys) {
if (ownsFile(root)) {
return root;
}
}
return null;
}
/**
* Create an analysis driver that can analyze the files within the given
* [contextRoot].
*/
AnalysisDriverGeneric createAnalysisDriver(ContextRoot contextRoot);
/**
* Handle an 'analysis.handleWatchEvents' request.
*/
AnalysisHandleWatchEventsResult handleAnalysisHandleWatchEvents(
AnalysisHandleWatchEventsParams parameters) =>
null;
/**
* Handle an 'analysis.reanalyze' request.
*/
AnalysisReanalyzeResult handleAnalysisReanalyze(
AnalysisReanalyzeParams parameters) =>
null;
/**
* Handle an 'analysis.setContextBuilderOptions' request.
*/
AnalysisSetContextBuilderOptionsResult handleAnalysisSetContextBuilderOptions(
AnalysisSetContextBuilderOptionsParams parameters) =>
null;
/**
* Handle an 'analysis.setContextRoots' request.
*/
AnalysisSetContextRootsResult handleAnalysisSetContextRoots(
AnalysisSetContextRootsParams parameters) {
List<ContextRoot> contextRoots = parameters.roots;
List<ContextRoot> oldRoots = driverMap.keys.toList();
for (ContextRoot contextRoot in contextRoots) {
if (!oldRoots.remove(contextRoot)) {
// The context is new, so we create a driver for it. Creating the driver
// has the side-effect of adding it to the analysis driver scheduler.
AnalysisDriverGeneric driver = createAnalysisDriver(contextRoot);
driverMap[contextRoot] = driver;
}
}
for (ContextRoot contextRoot in oldRoots) {
// The context has been removed, so we remove its driver.
AnalysisDriverGeneric driver = driverMap.remove(contextRoot);
// The `dispose` method has the side-effect of removing the driver from
// the analysis driver scheduler.
driver.dispose();
}
return new AnalysisSetContextRootsResult();
}
/**
* Handle an 'analysis.setPriorityFiles' request.
*/
AnalysisSetPriorityFilesResult handleAnalysisSetPriorityFiles(
AnalysisSetPriorityFilesParams parameters) {
List<String> files = parameters.files;
Map<AnalysisDriverGeneric, List<String>> filesByDriver =
<AnalysisDriverGeneric, List<String>>{};
for (String file in files) {
ContextRoot contextRoot = contextRootContaining(file);
if (contextRoot != null) {
// TODO(brianwilkerson) Which driver should we use if there is no context root?
AnalysisDriverGeneric driver = driverMap[contextRoot];
filesByDriver.putIfAbsent(driver, () => <String>[]).add(file);
}
}
filesByDriver.forEach((AnalysisDriverGeneric driver, List<String> files) {
driver.priorityFiles = files;
});
return 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(
AnalysisSetSubscriptionsParams parameters) {
Map<AnalysisService, List<String>> subscriptions = parameters.subscriptions;
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(
AnalysisUpdateContentParams parameters) {
Map<String, Object> files = parameters.files;
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(
CompletionGetSuggestionsParams parameters) =>
new CompletionGetSuggestionsResult(
-1, -1, const <CompletionSuggestion>[]);
/**
* Handle an 'edit.getAssists' request.
*/
EditGetAssistsResult handleEditGetAssists(EditGetAssistsParams 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(
EditGetAvailableRefactoringsParams parameters) =>
new EditGetAvailableRefactoringsResult(const <RefactoringKind>[]);
/**
* Handle an 'edit.getFixes' request.
*/
EditGetFixesResult handleEditGetFixes(EditGetFixesParams parameters) =>
new EditGetFixesResult(const <AnalysisErrorFixes>[]);
/**
* Handle an 'edit.getRefactoring' request.
*/
EditGetRefactoringResult handleEditGetRefactoring(
EditGetRefactoringParams 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(PluginShutdownParams parameters) =>
new PluginShutdownResult();
/**
* Handle a 'plugin.versionCheck' request.
*/
PluginVersionCheckResult handlePluginVersionCheck(
PluginVersionCheckParams parameters) {
String byteStorePath = parameters.byteStorePath;
String versionString = parameters.version;
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) {
_channel = channel;
_channel.listen(_onRequest, onError: onError, onDone: onDone);
}
/**
* 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:
var params = new AnalysisHandleWatchEventsParams.fromRequest(request);
result = handleAnalysisHandleWatchEvents(params);
break;
case ANALYSIS_REQUEST_REANALYZE:
var params = new AnalysisReanalyzeParams.fromRequest(request);
result = handleAnalysisReanalyze(params);
break;
case ANALYSIS_REQUEST_SET_CONTEXT_BUILDER_OPTIONS:
var params =
new AnalysisSetContextBuilderOptionsParams.fromRequest(request);
result = handleAnalysisSetContextBuilderOptions(params);
break;
case ANALYSIS_REQUEST_SET_CONTEXT_ROOTS:
var params = new AnalysisSetContextRootsParams.fromRequest(request);
result = handleAnalysisSetContextRoots(params);
break;
case ANALYSIS_REQUEST_SET_PRIORITY_FILES:
var params = new AnalysisSetPriorityFilesParams.fromRequest(request);
result = handleAnalysisSetPriorityFiles(params);
break;
case ANALYSIS_REQUEST_SET_SUBSCRIPTIONS:
var params = new AnalysisSetSubscriptionsParams.fromRequest(request);
result = handleAnalysisSetSubscriptions(params);
break;
case ANALYSIS_REQUEST_UPDATE_CONTENT:
var params = new AnalysisUpdateContentParams.fromRequest(request);
result = handleAnalysisUpdateContent(params);
break;
case COMPLETION_REQUEST_GET_SUGGESTIONS:
var params = new CompletionGetSuggestionsParams.fromRequest(request);
result = handleCompletionGetSuggestions(params);
break;
case EDIT_REQUEST_GET_ASSISTS:
var params = new EditGetAssistsParams.fromRequest(request);
result = handleEditGetAssists(params);
break;
case EDIT_REQUEST_GET_AVAILABLE_REFACTORINGS:
var params =
new EditGetAvailableRefactoringsParams.fromRequest(request);
result = handleEditGetAvailableRefactorings(params);
break;
case EDIT_REQUEST_GET_FIXES:
var params = new EditGetFixesParams.fromRequest(request);
result = handleEditGetFixes(params);
break;
case EDIT_REQUEST_GET_REFACTORING:
var params = new EditGetRefactoringParams.fromRequest(request);
result = handleEditGetRefactoring(params);
break;
case PLUGIN_REQUEST_SHUTDOWN:
var params = new PluginShutdownParams();
result = handlePluginShutdown(params);
_channel.sendResponse(result.toResponse(request.id));
_channel.close();
return null;
case PLUGIN_REQUEST_VERSION_CHECK:
var params = new PluginVersionCheckParams.fromRequest(request);
result = handlePluginVersionCheck(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);
}
}
}