blob: 5773045e7189769959c686a4b41ec30b3d20a97c [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 'dart:async';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/driver.dart'
show AnalysisDriver, AnalysisDriverGeneric, AnalysisDriverScheduler;
import 'package:analyzer/src/dart/analysis/file_byte_store.dart';
import 'package:analyzer/src/dart/analysis/file_state.dart';
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer_plugin/channel/channel.dart';
import 'package:analyzer_plugin/protocol/protocol.dart';
import 'package:analyzer_plugin/protocol/protocol_common.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 SDK manager used to manage SDKs.
*/
DartSdkManager _sdkManager;
/**
* 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(ResourceProvider provider)
: resourceProvider = provider ?? PhysicalResourceProvider.INSTANCE {
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 SDK manager used to manage SDKs.
*/
DartSdkManager get sdkManager => _sdkManager;
/**
* 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);
/**
* Return the driver being used to analyze the file with the given [path].
*/
AnalysisDriverGeneric driverForPath(String path) {
ContextRoot contextRoot = contextRootContaining(path);
if (contextRoot == null) {
return null;
}
return driverMap[contextRoot];
}
/**
* Return the result of analyzing the file with the given [path].
*
* Throw a [RequestFailure] is the file cannot be analyzed or if the driver
* associated with the file is not an [AnalysisDriver].
*/
Future<ResolveResult> getResolveResult(String path) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
AnalysisDriverGeneric driver = driverForPath(path);
if (driver is! AnalysisDriver) {
// Return an error from the request.
throw new RequestFailure(
RequestErrorFactory.pluginError('Failed to analyze $path', null));
}
ResolveResult result = await (driver as AnalysisDriver).getResult(path);
ResultState state = result.state;
if (state != ResultState.VALID) {
// Return an error from the request.
throw new RequestFailure(
RequestErrorFactory.pluginError('Failed to analyze $path', null));
}
return result;
}
/**
* Handle an 'analysis.getNavigation' request.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<AnalysisGetNavigationResult> handleAnalysisGetNavigation(
AnalysisGetNavigationParams params) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return new AnalysisGetNavigationResult(
<String>[], <NavigationTarget>[], <NavigationRegion>[]);
}
/**
* Handle an 'analysis.handleWatchEvents' request.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<AnalysisHandleWatchEventsResult> handleAnalysisHandleWatchEvents(
AnalysisHandleWatchEventsParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
for (WatchEvent event in parameters.events) {
switch (event.type) {
case WatchEventType.ADD:
// TODO(brianwilkerson) Handle the event.
break;
case WatchEventType.MODIFY:
contentChanged(event.path);
break;
case WatchEventType.REMOVE:
// TODO(brianwilkerson) Handle the event.
break;
default:
// Ignore unhandled watch event types.
break;
}
}
return new AnalysisHandleWatchEventsResult();
}
/**
* Handle an 'analysis.setContextRoots' request.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<AnalysisSetContextRootsResult> handleAnalysisSetContextRoots(
AnalysisSetContextRootsParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
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;
_addFilesToDriver(
driver,
resourceProvider.getResource(contextRoot.root),
contextRoot.exclude);
}
}
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.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<AnalysisSetPriorityFilesResult> handleAnalysisSetPriorityFiles(
AnalysisSetPriorityFilesParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
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.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<AnalysisSetSubscriptionsResult> handleAnalysisSetSubscriptions(
AnalysisSetSubscriptionsParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
Map<AnalysisService, List<String>> subscriptions = parameters.subscriptions;
Map<String, List<AnalysisService>> newSubscriptions =
subscriptionManager.setSubscriptions(subscriptions);
sendNotificationsForSubscriptions(newSubscriptions);
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.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<AnalysisUpdateContentResult> handleAnalysisUpdateContent(
AnalysisUpdateContentParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
Map<String, Object> files = parameters.files;
files.forEach((String filePath, Object overlay) {
if (overlay is AddContentOverlay) {
fileContentOverlay[filePath] = overlay.content;
} else if (overlay is ChangeContentOverlay) {
String oldContents = fileContentOverlay[filePath];
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(
RequestErrorFactory.invalidOverlayChangeNoContent());
}
try {
newContents = SourceEdit.applySequence(oldContents, overlay.edits);
} on RangeError {
throw new RequestFailure(
RequestErrorFactory.invalidOverlayChangeInvalidEdit());
}
fileContentOverlay[filePath] = newContents;
} else if (overlay is RemoveContentOverlay) {
fileContentOverlay[filePath] = null;
}
contentChanged(filePath);
});
return new AnalysisUpdateContentResult();
}
/**
* Handle a 'completion.getSuggestions' request.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<CompletionGetSuggestionsResult> handleCompletionGetSuggestions(
CompletionGetSuggestionsParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return new CompletionGetSuggestionsResult(
-1, -1, const <CompletionSuggestion>[]);
}
/**
* Handle an 'edit.getAssists' request.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<EditGetAssistsResult> handleEditGetAssists(
EditGetAssistsParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return 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].
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<EditGetAvailableRefactoringsResult> handleEditGetAvailableRefactorings(
EditGetAvailableRefactoringsParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return new EditGetAvailableRefactoringsResult(const <RefactoringKind>[]);
}
/**
* Handle an 'edit.getFixes' request.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<EditGetFixesResult> handleEditGetFixes(
EditGetFixesParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return new EditGetFixesResult(const <AnalysisErrorFixes>[]);
}
/**
* Handle an 'edit.getRefactoring' request.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<EditGetRefactoringResult> handleEditGetRefactoring(
EditGetRefactoringParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
return await null;
}
/**
* Handle a 'kythe.getKytheEntries' request.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<KytheGetKytheEntriesResult> handleKytheGetKytheEntries(
KytheGetKytheEntriesParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
return await 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.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<PluginShutdownResult> handlePluginShutdown(
PluginShutdownParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return new PluginShutdownResult();
}
/**
* Handle a 'plugin.versionCheck' request.
*
* Throw a [RequestFailure] if the request could not be handled.
*/
Future<PluginVersionCheckResult> handlePluginVersionCheck(
PluginVersionCheckParams parameters) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
String byteStorePath = parameters.byteStorePath;
String sdkPath = parameters.sdkPath;
String versionString = parameters.version;
Version serverVersion = new Version.parse(versionString);
_byteStore = new MemoryCachingByteStore(
new FileByteStore(byteStorePath,
tempNameSuffix:
new DateTime.now().millisecondsSinceEpoch.toString()),
64 * M);
_sdkManager = new DartSdkManager(sdkPath, true);
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) {}
/**
* If the plugin provides folding information, send a folding notification
* for the file with the given [path] to the server.
*/
Future<void> sendFoldingNotification(String path) {
return new Future.value();
}
/**
* If the plugin provides highlighting information, send a highlights
* notification for the file with the given [path] to the server.
*/
Future<void> sendHighlightsNotification(String path) {
return new Future.value();
}
/**
* If the plugin provides navigation information, send a navigation
* notification for the file with the given [path] to the server.
*/
Future<void> sendNavigationNotification(String path) {
return new Future.value();
}
/**
* Send notifications for the services subscribed to for the file with the
* given [path].
*
* This is a convenience method that subclasses can use to send notifications
* after analysis has been performed on a file.
*/
void sendNotificationsForFile(String path) {
for (AnalysisService service in subscriptionManager.servicesForFile(path)) {
_sendNotificationForFile(path, service);
}
}
/**
* Send notifications corresponding to the given description of
* [subscriptions]. The map is keyed by the path of each file for which
* notifications should be sent and has values representing the list of
* services associated with the notifications to send.
*
* This method is used when the set of subscribed notifications has been
* changed and notifications need to be sent even when the specified files
* have already been analyzed.
*/
void sendNotificationsForSubscriptions(
Map<String, List<AnalysisService>> subscriptions) {
subscriptions.forEach((String path, List<AnalysisService> services) {
for (AnalysisService service in services) {
_sendNotificationForFile(path, service);
}
});
}
/**
* If the plugin provides occurrences information, send an occurrences
* notification for the file with the given [path] to the server.
*/
Future<void> sendOccurrencesNotification(String path) {
return new Future.value();
}
/**
* If the plugin provides outline information, send an outline notification
* for the file with the given [path] to the server.
*/
Future<void> sendOutlineNotification(String path) {
return new Future.value();
}
/**
* Start this plugin by listening to the given communication [channel].
*/
void start(PluginCommunicationChannel channel) {
_channel = channel;
_channel.listen(_onRequest, onError: onError, onDone: onDone);
}
/**
* Add all of the files contained in the given [resource] that are not in the
* list of [excluded] resources to the given [driver].
*/
void _addFilesToDriver(
AnalysisDriverGeneric driver, Resource resource, List<String> excluded) {
String path = resource.path;
if (excluded.contains(path)) {
return;
}
if (resource is File) {
driver.addFile(path);
} else if (resource is Folder) {
try {
for (Resource child in resource.getChildren()) {
_addFilesToDriver(driver, child, excluded);
}
} on FileSystemException {
// The folder does not exist, so ignore it.
}
}
}
/**
* Compute the response that should be returned for the given [request], or
* `null` if the response has already been sent.
*/
Future<Response> _getResponse(Request request, int requestTime) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
ResponseResult result = null;
switch (request.method) {
case ANALYSIS_REQUEST_GET_NAVIGATION:
var params = new AnalysisGetNavigationParams.fromRequest(request);
result = await handleAnalysisGetNavigation(params);
break;
case ANALYSIS_REQUEST_HANDLE_WATCH_EVENTS:
var params = new AnalysisHandleWatchEventsParams.fromRequest(request);
result = await handleAnalysisHandleWatchEvents(params);
break;
case ANALYSIS_REQUEST_SET_CONTEXT_ROOTS:
var params = new AnalysisSetContextRootsParams.fromRequest(request);
result = await handleAnalysisSetContextRoots(params);
break;
case ANALYSIS_REQUEST_SET_PRIORITY_FILES:
var params = new AnalysisSetPriorityFilesParams.fromRequest(request);
result = await handleAnalysisSetPriorityFiles(params);
break;
case ANALYSIS_REQUEST_SET_SUBSCRIPTIONS:
var params = new AnalysisSetSubscriptionsParams.fromRequest(request);
result = await handleAnalysisSetSubscriptions(params);
break;
case ANALYSIS_REQUEST_UPDATE_CONTENT:
var params = new AnalysisUpdateContentParams.fromRequest(request);
result = await handleAnalysisUpdateContent(params);
break;
case COMPLETION_REQUEST_GET_SUGGESTIONS:
var params = new CompletionGetSuggestionsParams.fromRequest(request);
result = await handleCompletionGetSuggestions(params);
break;
case EDIT_REQUEST_GET_ASSISTS:
var params = new EditGetAssistsParams.fromRequest(request);
result = await handleEditGetAssists(params);
break;
case EDIT_REQUEST_GET_AVAILABLE_REFACTORINGS:
var params =
new EditGetAvailableRefactoringsParams.fromRequest(request);
result = await handleEditGetAvailableRefactorings(params);
break;
case EDIT_REQUEST_GET_FIXES:
var params = new EditGetFixesParams.fromRequest(request);
result = await handleEditGetFixes(params);
break;
case EDIT_REQUEST_GET_REFACTORING:
var params = new EditGetRefactoringParams.fromRequest(request);
result = await handleEditGetRefactoring(params);
break;
case KYTHE_REQUEST_GET_KYTHE_ENTRIES:
var params = new KytheGetKytheEntriesParams.fromRequest(request);
result = await handleKytheGetKytheEntries(params);
break;
case PLUGIN_REQUEST_SHUTDOWN:
var params = new PluginShutdownParams();
result = await handlePluginShutdown(params);
_channel.sendResponse(result.toResponse(request.id, requestTime));
_channel.close();
return null;
case PLUGIN_REQUEST_VERSION_CHECK:
var params = new PluginVersionCheckParams.fromRequest(request);
result = await handlePluginVersionCheck(params);
break;
}
if (result == null) {
return new Response(request.id, requestTime,
error: RequestErrorFactory.unknownRequest(request.method));
}
return result.toResponse(request.id, requestTime);
}
/**
* The method that is called when a [request] is received from the analysis
* server.
*/
Future<void> _onRequest(Request request) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
int requestTime = new DateTime.now().millisecondsSinceEpoch;
String id = request.id;
Response response;
try {
response = await _getResponse(request, requestTime);
} on RequestFailure catch (exception) {
response = new Response(id, requestTime, error: exception.error);
} catch (exception, stackTrace) {
response = new Response(id, requestTime,
error: new RequestError(
RequestErrorCode.PLUGIN_ERROR, exception.toString(),
stackTrace: stackTrace.toString()));
}
if (response != null) {
_channel.sendResponse(response);
}
}
/**
* Send a notification for the file at the given [path] corresponding to the
* given [service].
*/
void _sendNotificationForFile(String path, AnalysisService service) {
switch (service) {
case AnalysisService.FOLDING:
sendFoldingNotification(path);
break;
case AnalysisService.HIGHLIGHTS:
sendHighlightsNotification(path);
break;
case AnalysisService.NAVIGATION:
sendNavigationNotification(path);
break;
case AnalysisService.OCCURRENCES:
sendOccurrencesNotification(path);
break;
case AnalysisService.OUTLINE:
sendOutlineNotification(path);
break;
}
}
}