| // 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); |
| } |
| } |
| } |