blob: cee35085e5efb16e989cefc6ab7c0ba23c891c1f [file] [log] [blame]
// Copyright (c) 2014, 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.
library analysis.server;
import 'dart:async';
import 'dart:collection';
import 'dart:core' hide Resource;
import 'dart:math' show max;
import 'package:analysis_server/plugin/analyzed_files.dart';
import 'package:analysis_server/src/analysis_logger.dart';
import 'package:analysis_server/src/channel/channel.dart';
import 'package:analysis_server/src/context_manager.dart';
import 'package:analysis_server/src/operation/operation.dart';
import 'package:analysis_server/src/operation/operation_analysis.dart';
import 'package:analysis_server/src/operation/operation_queue.dart';
import 'package:analysis_server/src/plugin/server_plugin.dart';
import 'package:analysis_server/src/protocol.dart' hide Element;
import 'package:analysis_server/src/services/correction/namespace.dart';
import 'package:analysis_server/src/services/index/index.dart';
import 'package:analysis_server/src/services/search/search_engine.dart';
import 'package:analysis_server/uri/resolver_provider.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:analyzer/source/pub_package_map_provider.dart';
import 'package:analyzer/src/generated/ast.dart';
import 'package:analyzer/src/generated/element.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/java_engine.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/source_io.dart';
import 'package:analyzer/src/generated/utilities_general.dart';
import 'package:plugin/plugin.dart';
typedef void OptionUpdater(AnalysisOptionsImpl options);
/**
* Enum representing reasons why analysis might be done for a given file.
*/
class AnalysisDoneReason {
/**
* Analysis of the file completed successfully.
*/
static const AnalysisDoneReason COMPLETE =
const AnalysisDoneReason._('COMPLETE');
/**
* Analysis of the file was aborted because the context was removed.
*/
static const AnalysisDoneReason CONTEXT_REMOVED =
const AnalysisDoneReason._('CONTEXT_REMOVED');
/**
* Textual description of this [AnalysisDoneReason].
*/
final String text;
const AnalysisDoneReason._(this.text);
}
/**
* Instances of the class [AnalysisServer] implement a server that listens on a
* [CommunicationChannel] for analysis requests and process them.
*/
class AnalysisServer {
/**
* The version of the analysis server. The value should be replaced
* automatically during the build.
*/
static final String VERSION = '1.9.0';
/**
* The number of milliseconds to perform operations before inserting
* a 1 millisecond delay so that the VM and dart:io can deliver content
* to stdin. This should be removed once the underlying problem is fixed.
*/
static int performOperationDelayFreqency = 25;
/**
* The options of this server instance.
*/
AnalysisServerOptions options;
/**
* The channel from which requests are received and to which responses should
* be sent.
*/
final ServerCommunicationChannel channel;
/**
* The [ResourceProvider] using which paths are converted into [Resource]s.
*/
final ResourceProvider resourceProvider;
/**
* The [Index] for this server, may be `null` if indexing is disabled.
*/
final Index index;
/**
* The [SearchEngine] for this server, may be `null` if indexing is disabled.
*/
final SearchEngine searchEngine;
/**
* The plugin associated with this analysis server.
*/
final ServerPlugin serverPlugin;
/**
* The [ContextManager] that handles the mapping from analysis roots to
* context directories.
*/
ContextManager contextManager;
/**
* A flag indicating whether the server is running. When false, contexts
* will no longer be added to [contextWorkQueue], and [performOperation] will
* discard any tasks it finds on [contextWorkQueue].
*/
bool running;
/**
* A flag indicating the value of the 'analyzing' parameter sent in the last
* status message to the client.
*/
bool statusAnalyzing = false;
/**
* A list of the request handlers used to handle the requests sent to this
* server.
*/
List<RequestHandler> handlers;
/**
* The current default [DartSdk].
*/
final DartSdk defaultSdk;
/**
* The instrumentation service that is to be used by this analysis server.
*/
final InstrumentationService instrumentationService;
/**
* A table mapping [Folder]s to the [AnalysisContext]s associated with them.
*/
final Map<Folder, AnalysisContext> folderMap =
new HashMap<Folder, AnalysisContext>();
/**
* A queue of the operations to perform in this server.
*/
ServerOperationQueue operationQueue;
/**
* True if there is a pending future which will execute [performOperation].
*/
bool performOperationPending = false;
/**
* A set of the [ServerService]s to send notifications for.
*/
Set<ServerService> serverServices = new HashSet<ServerService>();
/**
* A set of the [GeneralAnalysisService]s to send notifications for.
*/
Set<GeneralAnalysisService> generalAnalysisServices =
new HashSet<GeneralAnalysisService>();
/**
* A table mapping [AnalysisService]s to the file paths for which these
* notifications should be sent.
*/
Map<AnalysisService, Set<String>> analysisServices =
new HashMap<AnalysisService, Set<String>>();
/**
* A table mapping [AnalysisContext]s to the completers that should be
* completed when analysis of this context is finished.
*/
Map<AnalysisContext,
Completer<AnalysisDoneReason>> contextAnalysisDoneCompleters =
new HashMap<AnalysisContext, Completer<AnalysisDoneReason>>();
/**
* Performance information before initial analysis is complete.
*/
ServerPerformance performanceDuringStartup = new ServerPerformance();
/**
* Performance information after initial analysis is complete
* or `null` if the initial analysis is not yet complete
*/
ServerPerformance performanceAfterStartup;
/**
* The class into which performance information is currently being recorded.
* During startup, this will be the same as [performanceDuringStartup]
* and after startup is complete, this switches to [performanceAfterStartup].
*/
ServerPerformance _performance;
/**
* The option possibly set from the server initialization which disables error notifications.
*/
bool _noErrorNotification;
/**
* The [Completer] that completes when analysis is complete.
*/
Completer _onAnalysisCompleteCompleter;
/**
* The [Completer] that completes when the next operation is performed.
*/
Completer _test_onOperationPerformedCompleter;
/**
* The controller that is notified when analysis is started.
*/
StreamController<AnalysisContext> _onAnalysisStartedController;
/**
* The controller that is notified when a single file has been analyzed.
*/
StreamController<ChangeNotice> _onFileAnalyzedController;
/**
* The controller used to notify others when priority sources change.
*/
StreamController<PriorityChangeEvent> _onPriorityChangeController;
/**
* True if any exceptions thrown by analysis should be propagated up the call
* stack.
*/
bool rethrowExceptions;
/**
* The next time (milliseconds since epoch) after which the analysis server
* should pause so that pending requests can be fetched by the system.
*/
// Add 1 sec to prevent delay from impacting short running tests
int _nextPerformOperationDelayTime =
new DateTime.now().millisecondsSinceEpoch + 1000;
/**
* The current state of overlays from the client. This is used as the
* content cache for all contexts.
*/
final ContentCache overlayState = new ContentCache();
/**
* The plugins that are defined outside the analysis_server package.
*/
List<Plugin> userDefinedPlugins;
/**
* If the "analysis.analyzedFiles" notification is currently being subscribed
* to (see [generalAnalysisServices]), and at least one such notification has
* been sent since the subscription was enabled, the set of analyzed files
* that was delivered in the most recently sent notification. Otherwise
* `null`.
*/
Set<String> prevAnalyzedFiles;
/**
* The default options used to create new analysis contexts.
*/
AnalysisOptionsImpl defaultContextOptions = new AnalysisOptionsImpl();
/**
* The controller for sending [ContextsChangedEvent]s.
*/
StreamController<ContextsChangedEvent> _onContextsChangedController =
new StreamController<ContextsChangedEvent>.broadcast();
/**
* Initialize a newly created server to receive requests from and send
* responses to the given [channel].
*
* If [rethrowExceptions] is true, then any exceptions thrown by analysis are
* propagated up the call stack. The default is true to allow analysis
* exceptions to show up in unit tests, but it should be set to false when
* running a full analysis server.
*/
AnalysisServer(
this.channel,
this.resourceProvider,
PubPackageMapProvider packageMapProvider,
Index _index,
this.serverPlugin,
this.options,
this.defaultSdk,
this.instrumentationService,
{ResolverProvider packageResolverProvider: null,
this.rethrowExceptions: true})
: index = _index,
searchEngine = _index != null ? createSearchEngine(_index) : null {
_performance = performanceDuringStartup;
operationQueue = new ServerOperationQueue();
contextManager = new ContextManagerImpl(resourceProvider,
packageResolverProvider, packageMapProvider, instrumentationService);
ServerContextManagerCallbacks contextManagerCallbacks =
new ServerContextManagerCallbacks(this, resourceProvider);
contextManager.callbacks = contextManagerCallbacks;
defaultContextOptions.incremental = true;
defaultContextOptions.incrementalApi =
options.enableIncrementalResolutionApi;
defaultContextOptions.incrementalValidation =
options.enableIncrementalResolutionValidation;
defaultContextOptions.generateImplicitErrors = false;
_noErrorNotification = options.noErrorNotification;
AnalysisEngine.instance.logger = new AnalysisLogger();
_onAnalysisStartedController = new StreamController.broadcast();
_onFileAnalyzedController = new StreamController.broadcast();
_onPriorityChangeController =
new StreamController<PriorityChangeEvent>.broadcast();
running = true;
onAnalysisStarted.first.then((_) {
onAnalysisComplete.then((_) {
performanceAfterStartup = new ServerPerformance();
_performance = performanceAfterStartup;
});
});
Notification notification =
new ServerConnectedParams(VERSION).toNotification();
channel.sendNotification(notification);
channel.listen(handleRequest, onDone: done, onError: error);
handlers = serverPlugin.createDomains(this);
}
/**
* The [Future] that completes when analysis is complete.
*/
Future get onAnalysisComplete {
if (isAnalysisComplete()) {
return new Future.value();
}
if (_onAnalysisCompleteCompleter == null) {
_onAnalysisCompleteCompleter = new Completer();
}
return _onAnalysisCompleteCompleter.future;
}
/**
* The stream that is notified when analysis of a context is started.
*/
Stream<AnalysisContext> get onAnalysisStarted {
return _onAnalysisStartedController.stream;
}
/**
* The stream that is notified when contexts are added or removed.
*/
Stream<ContextsChangedEvent> get onContextsChanged =>
_onContextsChangedController.stream;
/**
* The stream that is notified when a single file has been analyzed.
*/
Stream get onFileAnalyzed => _onFileAnalyzedController.stream;
/**
* The stream that is notified when priority sources change.
*/
Stream<PriorityChangeEvent> get onPriorityChange =>
_onPriorityChangeController.stream;
/**
* The [Future] that completes when the next operation is performed.
*/
Future get test_onOperationPerformed {
if (_test_onOperationPerformedCompleter == null) {
_test_onOperationPerformedCompleter = new Completer();
}
return _test_onOperationPerformedCompleter.future;
}
/**
* Adds the given [ServerOperation] to the queue, but does not schedule
* operations execution.
*/
void addOperation(ServerOperation operation) {
operationQueue.add(operation);
}
/**
* The socket from which requests are being read has been closed.
*/
void done() {
index.stop();
running = false;
}
/**
* There was an error related to the socket from which requests are being
* read.
*/
void error(argument) {
running = false;
}
/**
* If the given notice applies to a file contained within an analysis root,
* notify interested parties that the file has been (at least partially)
* analyzed.
*/
void fileAnalyzed(ChangeNotice notice) {
if (contextManager.isInAnalysisRoot(notice.source.fullName)) {
_onFileAnalyzedController.add(notice);
}
}
/**
* Return the preferred [AnalysisContext] for analyzing the given [path].
* This will be the context that explicitly contains the path, if any such
* context exists, otherwise it will be the first analysis context that
* implicitly analyzes it. Return `null` if no context is analyzing the
* path.
*/
AnalysisContext getAnalysisContext(String path) {
return getContextSourcePair(path).context;
}
/**
* Return any [AnalysisContext] that is analyzing the given [source], either
* explicitly or implicitly. Return `null` if there is no such context.
*/
AnalysisContext getAnalysisContextForSource(Source source) {
for (AnalysisContext context in folderMap.values) {
SourceKind kind = context.getKindOf(source);
if (kind != SourceKind.UNKNOWN) {
return context;
}
}
return null;
}
/**
* Return the [AnalysisContext]s that are being used to analyze the analysis
* roots.
*/
Iterable<AnalysisContext> getAnalysisContexts() {
return folderMap.values;
}
CompilationUnitElement getCompilationUnitElement(String file) {
ContextSourcePair pair = getContextSourcePair(file);
if (pair == null) {
return null;
}
// prepare AnalysisContext and Source
AnalysisContext context = pair.context;
Source unitSource = pair.source;
if (context == null || unitSource == null) {
return null;
}
// get element in the first library
List<Source> librarySources = context.getLibrariesContaining(unitSource);
if (!librarySources.isNotEmpty) {
return null;
}
Source librarySource = librarySources.first;
return context.getCompilationUnitElement(unitSource, librarySource);
}
/**
* Return the [AnalysisContext] that contains the given [path].
* Return `null` if no context contains the [path].
*/
AnalysisContext getContainingContext(String path) {
Folder containingFolder = null;
AnalysisContext containingContext = null;
folderMap.forEach((Folder folder, AnalysisContext context) {
if (folder.isOrContains(path)) {
if (containingFolder == null ||
containingFolder.path.length < folder.path.length) {
containingFolder = folder;
containingContext = context;
}
}
});
return containingContext;
}
/**
* Return the primary [ContextSourcePair] representing the given [path].
*
* The [AnalysisContext] of this pair will be the context that explicitly
* contains the path, if any such context exists, otherwise it will be the
* first context that implicitly analyzes it.
*
* If the [path] is not analyzed by any context, a [ContextSourcePair] with
* a `null` context and a `file` [Source] is returned.
*
* If the [path] doesn't represent a file, a [ContextSourcePair] with a `null`
* context and `null` [Source] is returned.
*
* Does not return `null`.
*/
ContextSourcePair getContextSourcePair(String path) {
// try SDK
{
Uri uri = resourceProvider.pathContext.toUri(path);
Source sdkSource = defaultSdk.fromFileUri(uri);
if (sdkSource != null) {
AnalysisContext sdkContext = defaultSdk.context;
return new ContextSourcePair(sdkContext, sdkSource);
}
}
// try to find the deep-most containing context
Resource resource = resourceProvider.getResource(path);
if (resource is! File) {
return new ContextSourcePair(null, null);
}
File file = resource;
{
AnalysisContext containingContext = getContainingContext(path);
if (containingContext != null) {
Source source =
ContextManagerImpl.createSourceInContext(containingContext, file);
return new ContextSourcePair(containingContext, source);
}
}
// try to find a context that analysed the file
for (AnalysisContext context in folderMap.values) {
Source source = ContextManagerImpl.createSourceInContext(context, file);
SourceKind kind = context.getKindOf(source);
if (kind != SourceKind.UNKNOWN) {
return new ContextSourcePair(context, source);
}
}
// try to find a context for which the file is a priority source
for (InternalAnalysisContext context in folderMap.values) {
List<Source> sources = context.getSourcesWithFullName(path);
if (sources.isNotEmpty) {
Source source = sources.first;
return new ContextSourcePair(context, source);
}
}
// file-based source
Source fileSource = file.createSource();
return new ContextSourcePair(null, fileSource);
}
/**
* Returns [Element]s at the given [offset] of the given [file].
*
* May be empty if cannot be resolved, but not `null`.
*/
List<Element> getElementsAtOffset(String file, int offset) {
List<AstNode> nodes = getNodesAtOffset(file, offset);
return getElementsOfNodes(nodes);
}
/**
* Returns [Element]s of the given [nodes].
*
* May be empty if not resolved, but not `null`.
*/
List<Element> getElementsOfNodes(List<AstNode> nodes) {
List<Element> elements = <Element>[];
for (AstNode node in nodes) {
if (node is SimpleIdentifier && node.parent is LibraryIdentifier) {
node = node.parent;
}
if (node is LibraryIdentifier) {
node = node.parent;
}
Element element = ElementLocator.locate(node);
if (node is SimpleIdentifier && element is PrefixElement) {
element = getImportElement(node);
}
if (element != null) {
elements.add(element);
}
}
return elements;
}
// TODO(brianwilkerson) Add the following method after 'prioritySources' has
// been added to InternalAnalysisContext.
// /**
// * Return a list containing the full names of all of the sources that are
// * priority sources.
// */
// List<String> getPriorityFiles() {
// List<String> priorityFiles = new List<String>();
// folderMap.values.forEach((ContextDirectory directory) {
// InternalAnalysisContext context = directory.context;
// context.prioritySources.forEach((Source source) {
// priorityFiles.add(source.fullName);
// });
// });
// return priorityFiles;
// }
/**
* Return an analysis error info containing the array of all of the errors and
* the line info associated with [file].
*
* Returns `null` if [file] does not belong to any [AnalysisContext], or the
* file does not exist.
*
* The array of errors will be empty if there are no errors in [file]. The
* errors contained in the array can be incomplete.
*
* This method does not wait for all errors to be computed, and returns just
* the current state.
*/
AnalysisErrorInfo getErrors(String file) {
ContextSourcePair contextSource = getContextSourcePair(file);
AnalysisContext context = contextSource.context;
Source source = contextSource.source;
if (context == null) {
return null;
}
if (!context.exists(source)) {
return null;
}
return context.getErrors(source);
}
/**
* Returns resolved [AstNode]s at the given [offset] of the given [file].
*
* May be empty, but not `null`.
*/
List<AstNode> getNodesAtOffset(String file, int offset) {
List<CompilationUnit> units = getResolvedCompilationUnits(file);
List<AstNode> nodes = <AstNode>[];
for (CompilationUnit unit in units) {
AstNode node = new NodeLocator(offset).searchWithin(unit);
if (node != null) {
nodes.add(node);
}
}
return nodes;
}
/**
* Returns resolved [CompilationUnit]s of the Dart file with the given [path].
*
* May be empty, but not `null`.
*/
List<CompilationUnit> getResolvedCompilationUnits(String path) {
List<CompilationUnit> units = <CompilationUnit>[];
ContextSourcePair contextSource = getContextSourcePair(path);
// prepare AnalysisContext
AnalysisContext context = contextSource.context;
if (context == null) {
return units;
}
// add a unit for each unit/library combination
runWithWorkingCacheSize(context, () {
Source unitSource = contextSource.source;
List<Source> librarySources = context.getLibrariesContaining(unitSource);
for (Source librarySource in librarySources) {
CompilationUnit unit =
context.resolveCompilationUnit2(unitSource, librarySource);
if (unit != null) {
units.add(unit);
}
}
});
// done
return units;
}
/**
* Handle a [request] that was read from the communication channel.
*/
void handleRequest(Request request) {
_performance.logRequest(request);
runZoned(() {
ServerPerformanceStatistics.serverRequests.makeCurrentWhile(() {
int count = handlers.length;
for (int i = 0; i < count; i++) {
try {
Response response = handlers[i].handleRequest(request);
if (response == Response.DELAYED_RESPONSE) {
return;
}
if (response != null) {
channel.sendResponse(response);
return;
}
} on RequestFailure catch (exception) {
channel.sendResponse(exception.response);
return;
} catch (exception, stackTrace) {
RequestError error = new RequestError(
RequestErrorCode.SERVER_ERROR, exception.toString());
if (stackTrace != null) {
error.stackTrace = stackTrace.toString();
}
Response response = new Response(request.id, error: error);
channel.sendResponse(response);
return;
}
}
channel.sendResponse(new Response.unknownRequest(request));
});
}, onError: (exception, stackTrace) {
sendServerErrorNotification(exception, stackTrace, fatal: true);
});
}
/**
* Returns `true` if there is a subscription for the given [service] and
* [file].
*/
bool hasAnalysisSubscription(AnalysisService service, String file) {
Set<String> files = analysisServices[service];
return files != null && files.contains(file);
}
/**
* Return `true` if analysis is complete.
*/
bool isAnalysisComplete() {
return operationQueue.isEmpty;
}
/**
* Returns a [Future] completing when [file] has been completely analyzed, in
* particular, all its errors have been computed. The future is completed
* with an [AnalysisDoneReason] indicating what caused the file's analysis to
* be considered complete.
*
* If the given file doesn't belong to any context, null is returned.
*
* TODO(scheglov) this method should be improved.
*
* 1. The analysis context should be told to analyze this particular file ASAP.
*
* 2. We should complete the future as soon as the file is analyzed (not wait
* until the context is completely finished)
*/
Future<AnalysisDoneReason> onFileAnalysisComplete(String file) {
// prepare AnalysisContext
AnalysisContext context = getAnalysisContext(file);
if (context == null) {
return null;
}
// schedule context analysis
schedulePerformAnalysisOperation(context);
// associate with the context completer
Completer<AnalysisDoneReason> completer =
contextAnalysisDoneCompleters[context];
if (completer == null) {
completer = new Completer<AnalysisDoneReason>();
contextAnalysisDoneCompleters[context] = completer;
}
return completer.future;
}
/**
* Perform the next available [ServerOperation].
*/
void performOperation() {
assert(performOperationPending);
PerformanceTag.UNKNOWN.makeCurrent();
performOperationPending = false;
if (!running) {
// An error has occurred, or the connection to the client has been
// closed, since this method was scheduled on the event queue. So
// don't do anything. Instead clear the operation queue.
operationQueue.clear();
return;
}
// prepare next operation
ServerOperation operation = operationQueue.take();
if (operation == null) {
// This can happen if the operation queue is cleared while the operation
// loop is in progress. No problem; we just need to exit the operation
// loop and wait for the next operation to be added.
ServerPerformanceStatistics.idle.makeCurrent();
return;
}
sendStatusNotification(operation);
// perform the operation
try {
operation.perform(this);
} catch (exception, stackTrace) {
AnalysisEngine.instance.logger.logError("${exception}\n${stackTrace}");
if (rethrowExceptions) {
throw new AnalysisException('Unexpected exception during analysis',
new CaughtException(exception, stackTrace));
}
sendServerErrorNotification(exception, stackTrace, fatal: true);
shutdown();
} finally {
if (_test_onOperationPerformedCompleter != null) {
_test_onOperationPerformedCompleter.complete(operation);
_test_onOperationPerformedCompleter = null;
}
if (!operationQueue.isEmpty) {
ServerPerformanceStatistics.intertask.makeCurrent();
_schedulePerformOperation();
} else {
if (generalAnalysisServices
.contains(GeneralAnalysisService.ANALYZED_FILES)) {
sendAnalysisNotificationAnalyzedFiles(this);
}
sendStatusNotification(null);
_scheduleAnalysisImplementedNotification();
if (_onAnalysisCompleteCompleter != null) {
_onAnalysisCompleteCompleter.complete();
_onAnalysisCompleteCompleter = null;
}
ServerPerformanceStatistics.idle.makeCurrent();
}
}
}
/**
* Trigger reanalysis of all files in the given list of analysis [roots], or
* everything if the analysis roots is `null`.
*/
void reanalyze(List<Resource> roots) {
// Clear any operations that are pending.
if (roots == null) {
operationQueue.clear();
} else {
for (AnalysisContext context in _getContexts(roots)) {
operationQueue.contextRemoved(context);
}
}
// Instruct the contextDirectoryManager to rebuild all contexts from
// scratch.
contextManager.refresh(roots);
}
/**
* Schedules execution of the given [ServerOperation].
*/
void scheduleOperation(ServerOperation operation) {
addOperation(operation);
_schedulePerformOperation();
}
/**
* Schedules analysis of the given context.
*/
void schedulePerformAnalysisOperation(AnalysisContext context) {
_onAnalysisStartedController.add(context);
scheduleOperation(new PerformAnalysisOperation(context, false));
}
/**
* This method is called when analysis of the given [AnalysisContext] is
* done.
*/
void sendContextAnalysisDoneNotifications(
AnalysisContext context, AnalysisDoneReason reason) {
Completer<AnalysisDoneReason> completer =
contextAnalysisDoneCompleters.remove(context);
if (completer != null) {
completer.complete(reason);
}
}
/**
* Send the given [notification] to the client.
*/
void sendNotification(Notification notification) {
channel.sendNotification(notification);
}
/**
* Send the given [response] to the client.
*/
void sendResponse(Response response) {
channel.sendResponse(response);
}
/**
* Sends a `server.error` notification.
*/
void sendServerErrorNotification(exception, stackTrace, {bool fatal: false}) {
// prepare exception.toString()
String exceptionString;
if (exception != null) {
exceptionString = exception.toString();
} else {
exceptionString = 'null exception';
}
// prepare stackTrace.toString()
String stackTraceString;
if (stackTrace != null) {
stackTraceString = stackTrace.toString();
} else {
try {
throw 'ignored';
} catch (ignored, stackTrace) {
stackTraceString = stackTrace.toString();
}
if (stackTraceString == null) {
// This code should be unreachable.
stackTraceString = 'null stackTrace';
}
}
// send the notification
channel.sendNotification(
new ServerErrorParams(fatal, exceptionString, stackTraceString)
.toNotification());
}
/**
* Send status notification to the client. The `operation` is the operation
* being performed or `null` if analysis is complete.
*/
void sendStatusNotification(ServerOperation operation) {
// Only send status when subscribed.
if (!serverServices.contains(ServerService.STATUS)) {
return;
}
// Only send status when it changes
bool isAnalyzing = operation != null;
if (statusAnalyzing == isAnalyzing) {
return;
}
statusAnalyzing = isAnalyzing;
AnalysisStatus analysis = new AnalysisStatus(isAnalyzing);
channel.sendNotification(
new ServerStatusParams(analysis: analysis).toNotification());
}
/**
* Implementation for `analysis.setAnalysisRoots`.
*
* TODO(scheglov) implement complete projects/contexts semantics.
*
* The current implementation is intentionally simplified and expected
* that only folders are given each given folder corresponds to the exactly
* one context.
*
* So, we can start working in parallel on adding services and improving
* projects/contexts support.
*/
void setAnalysisRoots(String requestId, List<String> includedPaths,
List<String> excludedPaths, Map<String, String> packageRoots) {
try {
contextManager.setRoots(includedPaths, excludedPaths, packageRoots);
} on UnimplementedError catch (e) {
throw new RequestFailure(
new Response.unsupportedFeature(requestId, e.message));
}
}
/**
* Implementation for `analysis.setSubscriptions`.
*/
void setAnalysisSubscriptions(
Map<AnalysisService, Set<String>> subscriptions) {
// send notifications for already analyzed sources
subscriptions.forEach((service, Set<String> newFiles) {
Set<String> oldFiles = analysisServices[service];
Set<String> todoFiles =
oldFiles != null ? newFiles.difference(oldFiles) : newFiles;
for (String file in todoFiles) {
ContextSourcePair contextSource = getContextSourcePair(file);
// prepare context
AnalysisContext context = contextSource.context;
if (context == null) {
continue;
}
Source source = contextSource.source;
// Ensure that if the AST is flushed / not ready, it will be
// computed eventually.
if (AnalysisEngine.isDartFileName(file)) {
(context as InternalAnalysisContext).ensureResolvedDartUnits(source);
}
// Send notifications that don't directly take an AST.
switch (service) {
case AnalysisService.NAVIGATION:
sendAnalysisNotificationNavigation(this, context, source);
continue;
case AnalysisService.OCCURRENCES:
sendAnalysisNotificationOccurrences(this, context, source);
continue;
}
// Dart unit notifications.
if (AnalysisEngine.isDartFileName(file)) {
// TODO(scheglov) This way to get resolved information is very Dart
// specific. OTOH as it is planned now Angular results are not
// flushable.
CompilationUnit dartUnit =
_getResolvedCompilationUnitToResendNotification(context, source);
if (dartUnit != null) {
switch (service) {
case AnalysisService.HIGHLIGHTS:
sendAnalysisNotificationHighlights(this, file, dartUnit);
break;
case AnalysisService.OUTLINE:
AnalysisContext context = dartUnit.element.context;
LineInfo lineInfo = context.getLineInfo(source);
sendAnalysisNotificationOutline(this, file, lineInfo, dartUnit);
break;
case AnalysisService.OVERRIDES:
sendAnalysisNotificationOverrides(this, file, dartUnit);
break;
}
}
}
}
});
// remember new subscriptions
this.analysisServices = subscriptions;
// special case for implemented elements
if (analysisServices.containsKey(AnalysisService.IMPLEMENTED) &&
isAnalysisComplete()) {
_scheduleAnalysisImplementedNotification();
}
}
/**
* Implementation for `analysis.setGeneralSubscriptions`.
*/
void setGeneralAnalysisSubscriptions(
List<GeneralAnalysisService> subscriptions) {
Set<GeneralAnalysisService> newServices = subscriptions.toSet();
if (newServices.contains(GeneralAnalysisService.ANALYZED_FILES) &&
!generalAnalysisServices
.contains(GeneralAnalysisService.ANALYZED_FILES) &&
isAnalysisComplete()) {
sendAnalysisNotificationAnalyzedFiles(this);
} else if (!newServices.contains(GeneralAnalysisService.ANALYZED_FILES) &&
generalAnalysisServices
.contains(GeneralAnalysisService.ANALYZED_FILES)) {
prevAnalyzedFiles = null;
}
generalAnalysisServices = newServices;
}
/**
* Set the priority files to the given [files].
*/
void setPriorityFiles(String requestId, List<String> files) {
// Note: when a file is a priority file, that information needs to be
// propagated to all contexts that analyze the file, so that all contexts
// will be able to do incremental resolution of the file. See
// dartbug.com/22209.
Map<AnalysisContext, List<Source>> sourceMap =
new HashMap<AnalysisContext, List<Source>>();
List<String> unanalyzed = new List<String>();
Source firstSource = null;
files.forEach((String file) {
ContextSourcePair contextSource = getContextSourcePair(file);
AnalysisContext preferredContext = contextSource.context;
Source source = contextSource.source;
// Try to make the file analyzable.
// If it is not in any context yet, add it to the first one which
// could use it, e.g. imports its package, even if not the library.
if (preferredContext == null) {
Resource resource = resourceProvider.getResource(file);
if (resource is File && resource.exists) {
for (AnalysisContext context in folderMap.values) {
Uri uri = context.sourceFactory.restoreUri(source);
if (uri.scheme != 'file') {
preferredContext = context;
source =
ContextManagerImpl.createSourceInContext(context, resource);
break;
}
}
}
}
// Fill the source map.
bool contextFound = false;
if (preferredContext != null) {
sourceMap.putIfAbsent(preferredContext, () => <Source>[]).add(source);
contextFound = true;
}
for (AnalysisContext context in folderMap.values) {
if (context.getKindOf(source) != SourceKind.UNKNOWN) {
sourceMap.putIfAbsent(context, () => <Source>[]).add(source);
contextFound = true;
}
}
if (firstSource == null) {
firstSource = source;
}
if (!contextFound) {
unanalyzed.add(file);
}
});
if (unanalyzed.isNotEmpty) {
StringBuffer buffer = new StringBuffer();
buffer.writeAll(unanalyzed, ', ');
throw new RequestFailure(
new Response.unanalyzedPriorityFiles(requestId, buffer.toString()));
}
sourceMap.forEach((context, List<Source> sourceList) {
context.analysisPriorityOrder = sourceList;
// Schedule the context for analysis so that it has the opportunity to
// cache the AST's for the priority sources as soon as possible.
schedulePerformAnalysisOperation(context);
});
operationQueue.reschedule();
_onPriorityChangeController.add(new PriorityChangeEvent(firstSource));
}
/**
* Returns `true` if errors should be reported for [file] with the given
* absolute path.
*/
bool shouldSendErrorsNotificationFor(String file) {
return !_noErrorNotification && contextManager.isInAnalysisRoot(file);
}
void shutdown() {
running = false;
if (index != null) {
index.clear();
index.stop();
}
// Defer closing the channel and shutting down the instrumentation server so
// that the shutdown response can be sent and logged.
new Future(() {
instrumentationService.shutdown();
channel.close();
});
}
void test_flushAstStructures(String file) {
if (AnalysisEngine.isDartFileName(file)) {
ContextSourcePair contextSource = getContextSourcePair(file);
InternalAnalysisContext context = contextSource.context;
Source source = contextSource.source;
context.test_flushAstStructures(source);
}
}
/**
* Performs all scheduled analysis operations.
*/
void test_performAllAnalysisOperations() {
while (true) {
ServerOperation operation = operationQueue.takeIf((operation) {
return operation is PerformAnalysisOperation;
});
if (operation == null) {
break;
}
operation.perform(this);
}
}
/**
* Implementation for `analysis.updateContent`.
*/
void updateContent(String id, Map<String, dynamic> changes) {
changes.forEach((file, change) {
ContextSourcePair contextSource = getContextSourcePair(file);
Source source = contextSource.source;
operationQueue.sourceAboutToChange(source);
// Prepare the new contents.
String oldContents = overlayState.getContents(source);
String newContents;
if (change is AddContentOverlay) {
newContents = change.content;
} else if (change is ChangeContentOverlay) {
if (oldContents == null) {
// The client may only send a ChangeContentOverlay if there is
// already an existing overlay for the source.
throw new RequestFailure(new Response(id,
error: new RequestError(RequestErrorCode.INVALID_OVERLAY_CHANGE,
'Invalid overlay change')));
}
try {
newContents = SourceEdit.applySequence(oldContents, change.edits);
} on RangeError {
throw new RequestFailure(new Response(id,
error: new RequestError(RequestErrorCode.INVALID_OVERLAY_CHANGE,
'Invalid overlay change')));
}
} else if (change is RemoveContentOverlay) {
newContents = null;
} else {
// Protocol parsing should have ensured that we never get here.
throw new AnalysisException('Illegal change type');
}
overlayState.setContents(source, newContents);
// If the source does not exist, then it was an overlay-only one.
// Remove it from contexts.
if (newContents == null && !source.exists()) {
for (InternalAnalysisContext context in folderMap.values) {
List<Source> sources = context.getSourcesWithFullName(file);
ChangeSet changeSet = new ChangeSet();
sources.forEach(changeSet.removedSource);
context.applyChanges(changeSet);
schedulePerformAnalysisOperation(context);
}
return;
}
// Update all contexts.
bool anyContextUpdated = false;
for (InternalAnalysisContext context in folderMap.values) {
List<Source> sources = context.getSourcesWithFullName(file);
sources.forEach((Source source) {
anyContextUpdated = true;
if (context.handleContentsChanged(
source, oldContents, newContents, true)) {
schedulePerformAnalysisOperation(context);
} else {
// When the client sends any change for a source, we should resend
// subscribed notifications, even if there were no changes in the
// source contents.
// TODO(scheglov) consider checking if there are subscriptions.
if (AnalysisEngine.isDartFileName(file)) {
List<CompilationUnit> dartUnits =
context.ensureResolvedDartUnits(source);
if (dartUnits != null) {
AnalysisErrorInfo errorInfo = context.getErrors(source);
for (var dartUnit in dartUnits) {
scheduleNotificationOperations(this, file, errorInfo.lineInfo,
context, null, dartUnit, errorInfo.errors);
scheduleIndexOperation(this, file, context, dartUnit);
}
} else {
schedulePerformAnalysisOperation(context);
}
}
}
});
}
// The source is not analyzed by any context, add to the containing one.
if (!anyContextUpdated) {
AnalysisContext context = contextSource.context;
if (context != null && source != null) {
ChangeSet changeSet = new ChangeSet();
changeSet.addedSource(source);
context.applyChanges(changeSet);
schedulePerformAnalysisOperation(context);
}
}
});
}
/**
* Use the given updaters to update the values of the options in every
* existing analysis context.
*/
void updateOptions(List<OptionUpdater> optionUpdaters) {
//
// Update existing contexts.
//
folderMap.forEach((Folder folder, AnalysisContext context) {
AnalysisOptionsImpl options =
new AnalysisOptionsImpl.from(context.analysisOptions);
optionUpdaters.forEach((OptionUpdater optionUpdater) {
optionUpdater(options);
});
context.analysisOptions = options;
// TODO(brianwilkerson) As far as I can tell, this doesn't cause analysis
// to be scheduled for this context.
});
//
// Update the defaults used to create new contexts.
//
optionUpdaters.forEach((OptionUpdater optionUpdater) {
optionUpdater(defaultContextOptions);
});
}
/**
* Return a set of all contexts whose associated folder is contained within,
* or equal to, one of the resources in the given list of [resources].
*/
Set<AnalysisContext> _getContexts(List<Resource> resources) {
Set<AnalysisContext> contexts = new HashSet<AnalysisContext>();
resources.forEach((Resource resource) {
if (resource is Folder) {
contexts.addAll(contextManager.contextsInAnalysisRoot(resource));
}
});
return contexts;
}
/**
* Returns the [CompilationUnit] of the Dart file with the given [source] that
* should be used to resend notifications for already resolved unit.
* Returns `null` if the file is not a part of any context, library has not
* been yet resolved, or any problem happened.
*/
CompilationUnit _getResolvedCompilationUnitToResendNotification(
AnalysisContext context, Source source) {
List<Source> librarySources = context.getLibrariesContaining(source);
if (librarySources.isEmpty) {
return null;
}
// if library has not been resolved yet, the unit will be resolved later
Source librarySource = librarySources[0];
if (context.getLibraryElement(librarySource) == null) {
return null;
}
// if library has been already resolved, resolve unit
return runWithWorkingCacheSize(context, () {
return context.resolveCompilationUnit2(source, librarySource);
});
}
_scheduleAnalysisImplementedNotification() async {
Set<String> files = analysisServices[AnalysisService.IMPLEMENTED];
if (files != null) {
scheduleImplementedNotification(this, files);
}
}
/**
* Schedules [performOperation] exection.
*/
void _schedulePerformOperation() {
if (performOperationPending) {
return;
}
/*
* TODO (danrubel) Rip out this workaround once the underlying problem
* is fixed. Currently, the VM and dart:io do not deliver content
* on stdin in a timely manner if the event loop is busy.
* To work around this problem, we delay for 1 millisecond
* every 25 milliseconds.
*
* To disable this workaround and see the underlying problem,
* set performOperationDelayFreqency to zero
*/
int now = new DateTime.now().millisecondsSinceEpoch;
if (now > _nextPerformOperationDelayTime &&
performOperationDelayFreqency > 0) {
_nextPerformOperationDelayTime = now + performOperationDelayFreqency;
new Future.delayed(new Duration(milliseconds: 1), performOperation);
} else {
new Future(performOperation);
}
performOperationPending = true;
}
}
class AnalysisServerOptions {
bool enableIncrementalResolutionApi = false;
bool enableIncrementalResolutionValidation = false;
bool noErrorNotification = false;
bool noIndex = false;
bool useAnalysisHighlight2 = false;
String fileReadMode = 'as-is';
}
/**
* Information about a file - an [AnalysisContext] that analyses the file,
* and the [Source] representing the file in this context.
*/
class ContextSourcePair {
/**
* A context that analysis the file.
* May be `null` if the file is not analyzed by any context.
*/
final AnalysisContext context;
/**
* The source that corresponds to the file.
* May be `null` if the file is not a regular file.
* If the file cannot be found in the [context], then it has a `file` uri.
*/
final Source source;
ContextSourcePair(this.context, this.source);
}
/**
* A [PriorityChangeEvent] indicates the set the priority files has changed.
*/
class PriorityChangeEvent {
final Source firstSource;
PriorityChangeEvent(this.firstSource);
}
class ServerContextManagerCallbacks extends ContextManagerCallbacks {
final AnalysisServer analysisServer;
/**
* The [ResourceProvider] by which paths are converted into [Resource]s.
*/
final ResourceProvider resourceProvider;
ServerContextManagerCallbacks(this.analysisServer, this.resourceProvider);
@override
AnalysisContext addContext(Folder folder, FolderDisposition disposition) {
InternalAnalysisContext context =
AnalysisEngine.instance.createAnalysisContext();
context.contentCache = analysisServer.overlayState;
analysisServer.folderMap[folder] = context;
context.sourceFactory = _createSourceFactory(disposition);
context.analysisOptions =
new AnalysisOptionsImpl.from(analysisServer.defaultContextOptions);
analysisServer._onContextsChangedController
.add(new ContextsChangedEvent(added: [context]));
analysisServer.schedulePerformAnalysisOperation(context);
return context;
}
@override
void applyChangesToContext(Folder contextFolder, ChangeSet changeSet) {
AnalysisContext context = analysisServer.folderMap[contextFolder];
if (context != null) {
context.applyChanges(changeSet);
analysisServer.schedulePerformAnalysisOperation(context);
List<String> flushedFiles = new List<String>();
for (Source source in changeSet.removedSources) {
flushedFiles.add(source.fullName);
}
sendAnalysisNotificationFlushResults(analysisServer, flushedFiles);
}
}
@override
void beginComputePackageMap() {
_computingPackageMap(true);
}
@override
void endComputePackageMap() {
_computingPackageMap(false);
}
@override
void removeContext(Folder folder, List<String> flushedFiles) {
AnalysisContext context = analysisServer.folderMap.remove(folder);
sendAnalysisNotificationFlushResults(analysisServer, flushedFiles);
if (analysisServer.index != null) {
analysisServer.index.removeContext(context);
}
analysisServer.operationQueue.contextRemoved(context);
analysisServer._onContextsChangedController
.add(new ContextsChangedEvent(removed: [context]));
analysisServer.sendContextAnalysisDoneNotifications(
context, AnalysisDoneReason.CONTEXT_REMOVED);
context.dispose();
}
@override
bool shouldFileBeAnalyzed(File file) {
List<ShouldAnalyzeFile> functions =
analysisServer.serverPlugin.analyzeFileFunctions;
for (ShouldAnalyzeFile shouldAnalyzeFile in functions) {
if (shouldAnalyzeFile(file)) {
// Emacs creates dummy links to track the fact that a file is open for
// editing and has unsaved changes (e.g. having unsaved changes to
// 'foo.dart' causes a link '.#foo.dart' to be created, which points to
// the non-existent file 'username@hostname.pid'. To avoid these dummy
// links causing the analyzer to thrash, just ignore links to
// non-existent files.
return file.exists;
}
}
return false;
}
@override
void updateContextPackageUriResolver(
Folder contextFolder, FolderDisposition disposition) {
AnalysisContext context = analysisServer.folderMap[contextFolder];
context.sourceFactory = _createSourceFactory(disposition);
analysisServer._onContextsChangedController
.add(new ContextsChangedEvent(changed: [context]));
analysisServer.schedulePerformAnalysisOperation(context);
}
void _computingPackageMap(bool computing) {
if (analysisServer.serverServices.contains(ServerService.STATUS)) {
PubStatus pubStatus = new PubStatus(computing);
ServerStatusParams params = new ServerStatusParams(pub: pubStatus);
analysisServer.sendNotification(params.toNotification());
}
}
/**
* Set up a [SourceFactory] that resolves packages as appropriate for the
* given [disposition].
*/
SourceFactory _createSourceFactory(FolderDisposition disposition) {
UriResolver dartResolver = new DartUriResolver(analysisServer.defaultSdk);
UriResolver resourceResolver = new ResourceUriResolver(resourceProvider);
List<UriResolver> resolvers = [];
resolvers.add(dartResolver);
resolvers.addAll(disposition.createPackageUriResolvers(resourceProvider));
resolvers.add(resourceResolver);
return new SourceFactory(resolvers, disposition.packages);
}
}
/**
* A class used by [AnalysisServer] to record performance information
* such as request latency.
*/
class ServerPerformance {
/**
* The creation time and the time when performance information
* started to be recorded here.
*/
int startTime = new DateTime.now().millisecondsSinceEpoch;
/**
* The number of requests.
*/
int requestCount = 0;
/**
* The total latency (milliseconds) for all recorded requests.
*/
int requestLatency = 0;
/**
* The maximum latency (milliseconds) for all recorded requests.
*/
int maxLatency = 0;
/**
* The number of requests with latency > 150 milliseconds.
*/
int slowRequestCount = 0;
/**
* Log performation information about the given request.
*/
void logRequest(Request request) {
++requestCount;
if (request.clientRequestTime != null) {
int latency =
new DateTime.now().millisecondsSinceEpoch - request.clientRequestTime;
requestLatency += latency;
maxLatency = max(maxLatency, latency);
if (latency > 150) {
++slowRequestCount;
}
}
}
}
/**
* Container with global [AnalysisServer] performance statistics.
*/
class ServerPerformanceStatistics {
/**
* The [PerformanceTag] for time spent in [ExecutionDomainHandler].
*/
static PerformanceTag executionNotifications =
new PerformanceTag('executionNotifications');
/**
* The [PerformanceTag] for time spent performing a _DartIndexOperation.
*/
static PerformanceTag indexOperation = new PerformanceTag('indexOperation');
/**
* The [PerformanceTag] for time spent between calls to
* AnalysisServer.performOperation when the server is not idle.
*/
static PerformanceTag intertask = new PerformanceTag('intertask');
/**
* The [PerformanceTag] for time spent between calls to
* AnalysisServer.performOperation when the server is idle.
*/
static PerformanceTag idle = new PerformanceTag('idle');
/**
* The [PerformanceTag] for time spent in
* PerformAnalysisOperation._sendNotices.
*/
static PerformanceTag notices = new PerformanceTag('notices');
/**
* The [PerformanceTag] for time spent running pub.
*/
static PerformanceTag pub = new PerformanceTag('pub');
/**
* The [PerformanceTag] for time spent in server comminication channels.
*/
static PerformanceTag serverChannel = new PerformanceTag('serverChannel');
/**
* The [PerformanceTag] for time spent in server request handlers.
*/
static PerformanceTag serverRequests = new PerformanceTag('serverRequests');
/**
* The [PerformanceTag] for time spent in split store microtasks.
*/
static PerformanceTag splitStore = new PerformanceTag('splitStore');
}