// 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_state.dart'; import 'package:analyzer/src/generated/source.dart'; import 'package:analyzer_plugin/channel/channel.dart'; import 'package:analyzer_plugin/protocol/generated_protocol.dart'; import 'package:analyzer_plugin/protocol/protocol.dart'; import 'package:analyzer_plugin/protocol/protocol_constants.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'; /** * 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 { /** * 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. */ final ByteStore byteStore = new MemoryByteStore(); /** * 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; /** * 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 parameters) => null; /** * Handle an 'analysis.reanalyze' request. */ AnalysisReanalyzeResult handleAnalysisReanalyze( Map parameters) => null; /** * Handle an 'analysis.setContextBuilderOptions' request. */ AnalysisSetContextBuilderOptionsResult handleAnalysisSetContextBuilderOptions( Map parameters) => null; /** * Handle an 'analysis.setContextRoots' request. */ AnalysisSetContextRootsResult handleAnalysisSetContextRoots( Map 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 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 parameters) { Map> 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 parameters) { Map 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 parameters) => new CompletionGetSuggestionsResult( -1, -1, const []); /** * Handle an 'edit.getAssists' request. */ EditGetAssistsResult handleEditGetAssists(Map parameters) => new EditGetAssistsResult(const []); /** * 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 parameters) => new EditGetAvailableRefactoringsResult(const []); /** * Handle an 'edit.getFixes' request. */ EditGetFixesResult handleEditGetFixes(Map parameters) => new EditGetFixesResult(const []); /** * Handle an 'edit.getRefactoring' request. */ EditGetRefactoringResult handleEditGetRefactoring( Map 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 parameters) => new PluginShutdownResult(); /** * Handle a 'plugin.versionCheck' request. */ PluginVersionCheckResult handlePluginVersionCheck( Map parameters); /** * 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/**/( Map 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.id) { 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); } } }