blob: f39ba53b16f4a0ae276d8a453b160db3e5723fe4 [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 'package:analysis_server/src/analysis_logger.dart';
import 'package:analysis_server/src/channel.dart';
import 'package:analysis_server/src/constants.dart';
import 'package:analysis_server/src/context_directory_manager.dart';
import 'package:analysis_server/src/domain_analysis.dart';
import 'package:analysis_server/src/operation/operation_analysis.dart';
import 'package:analysis_server/src/operation/operation.dart';
import 'package:analysis_server/src/operation/operation_queue.dart';
import 'package:analysis_server/src/protocol.dart';
import 'package:analysis_server/src/resource.dart';
import 'package:analyzer/src/generated/ast.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/error.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/generated/sdk_io.dart';
import 'package:analyzer/src/generated/source_io.dart';
import 'package:analyzer/src/generated/java_engine.dart';
/**
* An instance of [DirectoryBasedDartSdk] that is shared between
* [AnalysisServer] instances to improve performance.
*/
final DirectoryBasedDartSdk SHARED_SDK = DirectoryBasedDartSdk.defaultSdk;
class AnalysisServerContextDirectoryManager extends ContextDirectoryManager {
final AnalysisServer analysisServer;
AnalysisServerContextDirectoryManager(this.analysisServer, ResourceProvider resourceProvider)
: super(resourceProvider);
@override
void addContext(Folder folder, File pubspecFile) {
ContextDirectory contextDirectory = new ContextDirectory(
analysisServer.defaultSdk, folder, pubspecFile);
analysisServer.folderMap[folder] = contextDirectory;
analysisServer.schedulePerformAnalysisOperation(contextDirectory.context);
}
@override
void applyChangesToContext(Folder contextFolder, ChangeSet changeSet) {
analysisServer.folderMap[contextFolder].context.applyChanges(changeSet);
}
@override
void removeContext(Folder folder) {
analysisServer.folderMap.remove(folder);
}
}
/**
* Instances of the class [AnalysisServer] implement a server that listens on a
* [CommunicationChannel] for analysis requests and process them.
*/
class AnalysisServer {
/**
* The channel from which requests are received and to which responses should
* be sent.
*/
final ServerCommunicationChannel channel;
/**
* [ContextDirectoryManager] which handles the mapping from analysis roots
* to context directories.
*/
AnalysisServerContextDirectoryManager contextDirectoryManager;
/**
* 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 list of the request handlers used to handle the requests sent to this
* server.
*/
List<RequestHandler> handlers;
/**
* The current default [DartSdk].
*/
DartSdk defaultSdk = SHARED_SDK;
/**
* A table mapping [Folder]s to the [ContextDirectory]s associated with them.
*/
final Map<Folder, ContextDirectory> folderMap =
new HashMap<Folder, ContextDirectory>();
/**
* A queue of the operations to perform in this server.
*
* Invariant: when this queue is non-empty, there is exactly one pending call
* to [performOperation] on the event queue. When this list is empty, there are
* no calls to [performOperation] on the event queue.
*/
ServerOperationQueue operationQueue;
/**
* A set of the [ServerService]s to send notifications for.
*/
Set<ServerService> serverServices = new HashSet<ServerService>();
/**
* 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>>();
/**
* True if any exceptions thrown by analysis should be propagated up the call
* stack.
*/
bool rethrowExceptions;
/**
* 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, ResourceProvider resourceProvider,
{this.rethrowExceptions: true}) {
operationQueue = new ServerOperationQueue(this);
contextDirectoryManager = new AnalysisServerContextDirectoryManager(this, resourceProvider);
AnalysisEngine.instance.logger = new AnalysisLogger();
running = true;
Notification notification = new Notification(NOTIFICATION_CONNECTED);
channel.sendNotification(notification);
channel.listen(handleRequest, onDone: done, onError: error);
}
/**
* Schedules analysis of the given context.
*/
void schedulePerformAnalysisOperation(AnalysisContext context) {
scheduleOperation(new PerformAnalysisOperation(context, false));
}
/**
* Schedules execution of the given [ServerOperation].
*/
void scheduleOperation(ServerOperation operation) {
bool wasEmpty = operationQueue.isEmpty;
addOperation(operation);
if (wasEmpty) {
_schedulePerformOperation();
}
}
/**
* 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() {
running = false;
}
/**
* There was an error related to the socket from which requests are being
* read.
*/
void error(argument) {
running = false;
}
/**
* Handle a [request] that was read from the communication channel.
*/
void handleRequest(Request request) {
int count = handlers.length;
for (int i = 0; i < count; i++) {
try {
Response response = handlers[i].handleRequest(request);
if (response != null) {
channel.sendResponse(response);
return;
}
} on RequestFailure catch (exception) {
channel.sendResponse(exception.response);
return;
}
}
channel.sendResponse(new Response.unknownRequest(request));
}
/**
* Returns `true` if there is a subscription for the given [server] and [file].
*/
bool hasAnalysisSubscription(AnalysisService service, String file) {
Set<String> files = analysisServices[service];
return files != null && files.contains(file);
}
/**
* Returns `true` if the given [AnalysisContext] is a priority one.
*/
bool isPriorityContext(AnalysisContext context) {
// TODO(scheglov) implement support for priority sources/contexts
return false;
}
/**
* Perform the next available [ServerOperation].
*/
void performOperation() {
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();
// 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));
}
} finally {
if (!operationQueue.isEmpty) {
_schedulePerformOperation();
} else {
sendStatusNotification(null);
}
}
}
/**
* Send status notification to the client. The `contextId` indicates
* the current context being analyzed or `null` if analysis is complete.
*/
void sendStatusNotification(String contextId) {
Notification notification = new Notification(NOTIFICATION_STATUS);
Map<String, Object> analysis = new Map();
if (contextId != null) {
analysis['analyzing'] = true;
// TODO(danrubel): replace contextId with real analysisTarget
analysis['analysisTarget'] = contextId;
} else {
analysis['analyzing'] = false;
}
notification.params['analysis'] = analysis;
channel.sendNotification(notification);
}
/**
* 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) {
try {
contextDirectoryManager.setRoots(includedPaths, excludedPaths);
} on UnimplementedError catch (e) {
throw new RequestFailure(
new Response.unsupportedFeature(
requestId, e.message));
}
}
/**
* Implementation for `analysis.updateContent`.
*/
void updateContent(Map<String, ContentChange> changes) {
changes.forEach((file, change) {
AnalysisContext analysisContext = _getAnalysisContext(file);
if (analysisContext != null) {
Source source = _getSource(file);
if (change.offset == null) {
analysisContext.setContents(source, change.content);
} else {
analysisContext.setChangedContents(source, change.content,
change.offset, change.oldLength, change.newLength);
}
schedulePerformAnalysisOperation(analysisContext);
}
});
}
/**
* 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) {
if (service == AnalysisService.ERRORS) {
Source source = _getSource(file);
AnalysisContext analysisContext = _getAnalysisContext(file);
List<AnalysisError> errors = analysisContext.getErrors(source).errors;
sendAnalysisNotificationErrors(this, file, errors);
}
// TODO(scheglov)
// 1. implement resolveCompilationUnit()
// 2. Share "if (dartUnit != null)"
if (service == AnalysisService.HIGHLIGHTS) {
CompilationUnit dartUnit = test_getResolvedCompilationUnit(file);
if (dartUnit != null) {
sendAnalysisNotificationHighlights(this, file, dartUnit);
}
}
if (service == AnalysisService.NAVIGATION) {
CompilationUnit dartUnit = test_getResolvedCompilationUnit(file);
if (dartUnit != null) {
sendAnalysisNotificationNavigation(this, file, dartUnit);
}
}
if (service == AnalysisService.OUTLINE) {
CompilationUnit dartUnit = test_getResolvedCompilationUnit(file);
if (dartUnit != null) {
sendAnalysisNotificationOutline(this, file, dartUnit);
}
}
}
});
// remember new subscriptions
this.analysisServices = subscriptions;
}
/**
* Return the [AnalysisContext] that is used to analyze the given [path].
* Return `null` if there is no such context.
*/
AnalysisContext _getAnalysisContext(String path) {
for (Folder folder in folderMap.keys) {
if (path.startsWith(folder.path)) {
return folderMap[folder].context;
}
}
return null;
}
/**
* Return the [Source] of the Dart file with the given [path].
*/
Source _getSource(String path) {
File file = contextDirectoryManager.resourceProvider.getResource(path);
return file.createSource(UriKind.FILE_URI);
}
/**
* Return the [CompilationUnit] of the Dart file with the given [path].
* Return `null` if the file is not a part of any context.
*/
CompilationUnit test_getResolvedCompilationUnit(String path) {
// prepare AnalysisContext
AnalysisContext context = _getAnalysisContext(path);
if (context == null) {
return null;
}
// prepare sources
Source unitSource = _getSource(path);
List<Source> librarySources = context.getLibrariesContaining(unitSource);
if (librarySources.isEmpty) {
return null;
}
// get a resolved unit
return context.getResolvedCompilationUnit2(unitSource, librarySources[0]);
}
/**
* Return `true` if all operations have been performed in this [AnalysisServer].
*/
bool test_areOperationsFinished() {
return operationQueue.isEmpty;
}
/**
* Send the given [notification] to the client.
*/
void sendNotification(Notification notification) {
channel.sendNotification(notification);
}
/**
* Schedules [performOperation] exection.
*/
void _schedulePerformOperation() {
new Future(performOperation);
}
}
/**
* An enumeration of the services provided by the analysis domain.
*/
class AnalysisService extends Enum2<AnalysisService> {
static const AnalysisService ERRORS = const AnalysisService('ERRORS', 0);
static const AnalysisService HIGHLIGHTS = const AnalysisService('HIGHLIGHTS', 1);
static const AnalysisService NAVIGATION = const AnalysisService('NAVIGATION', 2);
static const AnalysisService OUTLINE = const AnalysisService('OUTLINE', 3);
static const List<AnalysisService> VALUES =
const [ERRORS, HIGHLIGHTS, NAVIGATION, OUTLINE];
const AnalysisService(String name, int ordinal) : super(name, ordinal);
}
/**
* Instances of [ContextDirectory] represents a [Folder] associated with an
* analysis context. The folder may or may not contain a Pub `pubspec.yaml`.
*
* TODO(scheglov) implement complete projects/contexts semantics.
*
* This class is intentionally simplified to serve as a base to start working
* on services while work on complete semantics is being done in parallel.
*/
class ContextDirectory {
/**
* The root [Folder] of this [ContextDirectory].
*/
final Folder _folder;
/**
* The `pubspec.yaml` file in [_folder], or null if there isn't one.
*/
File _pubspecFile;
/**
* The [AnalysisContext] of this [_folder].
*/
AnalysisContext _context;
ContextDirectory(DartSdk sdk, this._folder, this._pubspecFile) {
// create AnalysisContext
_context = AnalysisEngine.instance.createAnalysisContext();
// TODO(scheglov) replace FileUriResolver with an Resource based resolver
// TODO(scheglov) create packages resolver
_context.sourceFactory = new SourceFactory([
new DartUriResolver(sdk),
new FileUriResolver(),
// new PackageUriResolver(),
]);
}
/**
* Return the [AnalysisContext] of this folder.
*/
AnalysisContext get context => _context;
}
/**
* An enumeration of the services provided by the server domain.
*/
class ServerService extends Enum2<ServerService> {
static const ServerService STATUS = const ServerService('STATUS', 0);
static const List<ServerService> VALUES = const [STATUS];
const ServerService(String name, int ordinal) : super(name, ordinal);
}