| // Copyright (c) 2016, 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.src.single_context_manager; |
| |
| import 'dart:async'; |
| import 'dart:core'; |
| import 'dart:math' as math; |
| |
| import 'package:analysis_server/src/context_manager.dart'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:analyzer/plugin/resolver_provider.dart'; |
| import 'package:analyzer/src/dart/analysis/driver.dart'; |
| import 'package:analyzer/src/generated/engine.dart'; |
| import 'package:analyzer/src/generated/sdk.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer/src/util/glob.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:watcher/watcher.dart'; |
| |
| /** |
| * Implementation of [ContextManager] that supports only one [AnalysisContext]. |
| * So, sources from all analysis roots are added to this single context. All |
| * features that could otherwise cause creating additional contexts, such as |
| * presence of `pubspec.yaml` or `.packages` files, or analysis options files |
| * are ignored. |
| */ |
| class SingleContextManager implements ContextManager { |
| /** |
| * The [ResourceProvider] using which paths are converted into [Resource]s. |
| */ |
| final ResourceProvider resourceProvider; |
| |
| /** |
| * The context used to work with file system paths. |
| */ |
| path.Context pathContext; |
| |
| /** |
| * The manager used to access the SDK that should be associated with a |
| * particular context. |
| */ |
| final DartSdkManager sdkManager; |
| |
| /** |
| * A function that will return a [UriResolver] that can be used to resolve |
| * `package:` URIs. |
| */ |
| final ResolverProvider packageResolverProvider; |
| |
| /** |
| * A list of the globs used to determine which files should be analyzed. |
| */ |
| final List<Glob> analyzedFilesGlobs; |
| |
| /** |
| * The default options used to create new analysis contexts. |
| */ |
| final AnalysisOptionsImpl defaultContextOptions; |
| |
| /** |
| * The list of included paths (folders and files) most recently passed to |
| * [setRoots]. |
| */ |
| List<String> includedPaths = <String>[]; |
| |
| /** |
| * The list of excluded paths (folders and files) most recently passed to |
| * [setRoots]. |
| */ |
| List<String> excludedPaths = <String>[]; |
| |
| /** |
| * The map of package roots most recently passed to [setRoots]. |
| */ |
| Map<String, String> packageRoots = <String, String>{}; |
| |
| /** |
| * Same as [packageRoots], except that source folders have been normalized |
| * and non-folders have been removed. |
| */ |
| Map<String, String> normalizedPackageRoots = <String, String>{}; |
| |
| @override |
| ContextManagerCallbacks callbacks; |
| |
| /** |
| * The analysis driver which analyses everything. |
| */ |
| AnalysisDriver analysisDriver; |
| |
| /** |
| * The context in which everything is being analyzed. |
| */ |
| AnalysisContext context; |
| |
| /** |
| * The folder associated with the context. |
| */ |
| Folder contextFolder; |
| |
| /** |
| * The current watch subscriptions. |
| */ |
| Map<String, StreamSubscription<WatchEvent>> watchSubscriptions = |
| new Map<String, StreamSubscription<WatchEvent>>(); |
| |
| /** |
| * The [packageResolverProvider] must not be `null`. |
| */ |
| SingleContextManager( |
| this.resourceProvider, |
| this.sdkManager, |
| this.packageResolverProvider, |
| this.analyzedFilesGlobs, |
| this.defaultContextOptions) { |
| pathContext = resourceProvider.pathContext; |
| } |
| |
| @override |
| Iterable<AnalysisContext> get analysisContexts => |
| context == null ? <AnalysisContext>[] : <AnalysisContext>[context]; |
| |
| @override |
| Map<Folder, AnalysisDriver> get driverMap => {contextFolder: analysisDriver}; |
| |
| @override |
| Map<Folder, AnalysisContext> get folderMap => {contextFolder: context}; |
| |
| @override |
| List<AnalysisContext> contextsInAnalysisRoot(Folder analysisRoot) { |
| if (context == null || !includedPaths.contains(analysisRoot.path)) { |
| return <AnalysisContext>[]; |
| } |
| return <AnalysisContext>[context]; |
| } |
| |
| @override |
| AnalysisContext getContextFor(String path) { |
| if (context == null) { |
| return null; |
| } else if (_isContainedIn(includedPaths, path)) { |
| return context; |
| } |
| return null; |
| } |
| |
| @override |
| Folder getContextFolderFor(String path) { |
| if (isInAnalysisRoot(path)) { |
| return contextFolder; |
| } |
| return null; |
| } |
| |
| @override |
| AnalysisDriver getDriverFor(String path) { |
| throw new UnimplementedError( |
| 'Unexpected invocation of getDriverFor in SingleContextManager'); |
| } |
| |
| @override |
| List<AnalysisDriver> getDriversInAnalysisRoot(Folder analysisRoot) { |
| throw new UnimplementedError( |
| 'Unexpected invocation of getDriversInAnalysisRoot in SingleContextManager'); |
| } |
| |
| @override |
| bool isIgnored(String path) { |
| return !_isContainedIn(includedPaths, path) || _isExcludedPath(path); |
| } |
| |
| @override |
| bool isInAnalysisRoot(String path) { |
| return _isContainedIn(includedPaths, path) && |
| !_isContainedIn(excludedPaths, path); |
| } |
| |
| @override |
| void refresh(List<Resource> roots) { |
| if (context != null) { |
| callbacks.removeContext(contextFolder, null); |
| context.dispose(); |
| context = null; |
| contextFolder = null; |
| _cancelCurrentWatchSubscriptions(); |
| setRoots(includedPaths, excludedPaths, packageRoots); |
| } |
| } |
| |
| @override |
| void setRoots(List<String> includedPaths, List<String> excludedPaths, |
| Map<String, String> packageRoots) { |
| includedPaths = _nonOverlappingPaths(includedPaths); |
| excludedPaths = _nonOverlappingPaths(excludedPaths); |
| this.packageRoots = packageRoots; |
| _updateNormalizedPackageRoots(); |
| // Update context path. |
| { |
| String contextPath = _commonPrefix(includedPaths); |
| Folder contextFolder = resourceProvider.getFolder(contextPath); |
| if (contextFolder != this.contextFolder) { |
| if (context != null) { |
| callbacks.moveContext(this.contextFolder, contextFolder); |
| } |
| this.contextFolder = contextFolder; |
| } |
| } |
| // Start new watchers and cancel old ones. |
| { |
| Map<String, StreamSubscription<WatchEvent>> newSubscriptions = |
| new Map<String, StreamSubscription<WatchEvent>>(); |
| for (String includedPath in includedPaths) { |
| Resource resource = resourceProvider.getResource(includedPath); |
| if (resource is Folder) { |
| // Extract the existing subscription or create a new one. |
| StreamSubscription<WatchEvent> subscription = |
| watchSubscriptions.remove(includedPath); |
| if (subscription == null) { |
| subscription = resource.changes.listen(_handleWatchEvent); |
| } |
| // Remember the subscription. |
| newSubscriptions[includedPath] = subscription; |
| } |
| _cancelCurrentWatchSubscriptions(); |
| this.watchSubscriptions = newSubscriptions; |
| } |
| } |
| // Create or update the analysis context. |
| if (context == null) { |
| context = callbacks.addContext(contextFolder, defaultContextOptions); |
| ChangeSet changeSet = |
| _buildChangeSet(added: _includedFiles(includedPaths, excludedPaths)); |
| callbacks.applyChangesToContext(contextFolder, changeSet); |
| } else { |
| // TODO(brianwilkerson) Optimize this. |
| List<File> oldFiles = |
| _includedFiles(this.includedPaths, this.excludedPaths); |
| List<File> newFiles = _includedFiles(includedPaths, excludedPaths); |
| ChangeSet changeSet = _buildChangeSet( |
| added: _diff(newFiles, oldFiles), removed: _diff(oldFiles, newFiles)); |
| callbacks.applyChangesToContext(contextFolder, changeSet); |
| } |
| this.includedPaths = includedPaths; |
| this.excludedPaths = excludedPaths; |
| } |
| |
| /** |
| * Recursively add the given [resource] (if it's a file) or its children (if |
| * it's a folder) to the [addedFiles]. |
| */ |
| void _addFilesInResource( |
| List<File> addedFiles, Resource resource, List<String> excludedPaths) { |
| if (_isImplicitlyExcludedResource(resource)) { |
| return; |
| } |
| String path = resource.path; |
| if (_isEqualOrWithinAny(excludedPaths, path)) { |
| return; |
| } |
| if (resource is File) { |
| if (_matchesAnyAnalyzedFilesGlob(path) && resource.exists) { |
| addedFiles.add(resource); |
| } |
| } else if (resource is Folder) { |
| for (Resource child in _getChildrenSafe(resource)) { |
| _addFilesInResource(addedFiles, child, excludedPaths); |
| } |
| } |
| } |
| |
| ChangeSet _buildChangeSet({List<File> added, List<File> removed}) { |
| ChangeSet changeSet = new ChangeSet(); |
| if (added != null) { |
| for (File file in added) { |
| Source source = createSourceInContext(context, file); |
| changeSet.addedSource(source); |
| } |
| } |
| if (removed != null) { |
| for (File file in removed) { |
| Source source = createSourceInContext(context, file); |
| changeSet.removedSource(source); |
| } |
| } |
| return changeSet; |
| } |
| |
| void _cancelCurrentWatchSubscriptions() { |
| for (StreamSubscription<WatchEvent> subscription |
| in watchSubscriptions.values) { |
| subscription.cancel(); |
| } |
| watchSubscriptions.clear(); |
| } |
| |
| String _commonPrefix(List<String> paths) { |
| if (paths.isEmpty) { |
| return ''; |
| } |
| List<String> left = pathContext.split(paths[0]); |
| int count = left.length; |
| for (int i = 1; i < paths.length; i++) { |
| List<String> right = pathContext.split(paths[i]); |
| count = _commonComponents(left, count, right); |
| } |
| return pathContext.joinAll(left.sublist(0, count)); |
| } |
| |
| List<Resource> _existingResources(List<String> pathList) { |
| List<Resource> resources = <Resource>[]; |
| for (String path in pathList) { |
| Resource resource = resourceProvider.getResource(path); |
| if (resource is Folder) { |
| resources.add(resource); |
| } else if (!resource.exists) { |
| // Non-existent resources are ignored. TODO(paulberry): we should set |
| // up a watcher to ensure that if the resource appears later, we will |
| // begin analyzing it. |
| } else if (resource is File) { |
| resources.add(resource); |
| } else { |
| throw new UnimplementedError('$path is not a folder. ' |
| 'Only support for file and folder analysis is implemented.'); |
| } |
| } |
| return resources; |
| } |
| |
| void _handleWatchEvent(WatchEvent event) { |
| String path = event.path; |
| // Ignore if excluded. |
| if (_isExcludedPath(path)) { |
| return; |
| } |
| // Ignore if not in a root. |
| if (!_isContainedIn(includedPaths, path)) { |
| return; |
| } |
| // Handle the change. |
| switch (event.type) { |
| case ChangeType.ADD: |
| Resource resource = resourceProvider.getResource(path); |
| if (resource is File) { |
| if (_matchesAnyAnalyzedFilesGlob(path)) { |
| callbacks.applyChangesToContext( |
| contextFolder, _buildChangeSet(added: <File>[resource])); |
| } |
| } |
| break; |
| case ChangeType.REMOVE: |
| List<Source> sources = context.getSourcesWithFullName(path); |
| if (!sources.isEmpty) { |
| ChangeSet changeSet = new ChangeSet(); |
| sources.forEach(changeSet.removedSource); |
| callbacks.applyChangesToContext(contextFolder, changeSet); |
| } |
| break; |
| case ChangeType.MODIFY: |
| List<Source> sources = context.getSourcesWithFullName(path); |
| if (!sources.isEmpty) { |
| ChangeSet changeSet = new ChangeSet(); |
| sources.forEach(changeSet.changedSource); |
| callbacks.applyChangesToContext(contextFolder, changeSet); |
| } |
| break; |
| } |
| } |
| |
| List<File> _includedFiles( |
| List<String> includedPaths, List<String> excludedPaths) { |
| List<Resource> includedResources = _existingResources(includedPaths); |
| List<File> includedFiles = <File>[]; |
| for (Resource resource in includedResources) { |
| _addFilesInResource(includedFiles, resource, excludedPaths); |
| } |
| return includedFiles; |
| } |
| |
| bool _isContainedIn(List<String> pathList, String path) { |
| for (String pathInList in pathList) { |
| if (_isEqualOrWithin(pathInList, path)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool _isEqualOrWithin(String parent, String child) { |
| return child == parent || pathContext.isWithin(parent, child); |
| } |
| |
| bool _isEqualOrWithinAny(List<String> parents, String child) { |
| for (String parent in parents) { |
| if (_isEqualOrWithin(parent, child)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Return `true` if the given [path] should be excluded, using explicit |
| * or implicit rules. |
| */ |
| bool _isExcludedPath(String path) { |
| List<String> parts = resourceProvider.pathContext.split(path); |
| // Implicit rules. |
| for (String part in parts) { |
| if (part.startsWith('.')) { |
| return true; |
| } |
| } |
| // Explicitly excluded paths. |
| if (_isEqualOrWithinAny(excludedPaths, path)) { |
| return true; |
| } |
| // OK |
| return false; |
| } |
| |
| /** |
| * Return `true` if the given [resource] and children should be excluded |
| * because of some implicit exclusion rules, e.g. `.name`. |
| */ |
| bool _isImplicitlyExcludedResource(Resource resource) { |
| String shortName = resource.shortName; |
| if (shortName.startsWith('.')) { |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Return `true` if the given [path] matches one of the [analyzedFilesGlobs]. |
| */ |
| bool _matchesAnyAnalyzedFilesGlob(String path) { |
| for (Glob glob in analyzedFilesGlobs) { |
| if (glob.matches(path)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Return a list consisting of the elements from [pathList] that describe the |
| * minimal set of directories that include everything in the original list of |
| * paths and nothing more. In particular: |
| * |
| * * if a path is in the input list multiple times it will appear at most |
| * once in the output list, and |
| * * if a directory D and a subdirectory of it are both in the input list |
| * then only the directory D will be in the output list. |
| * |
| * The original list is not modified. |
| */ |
| List<String> _nonOverlappingPaths(List<String> pathList) { |
| List<String> sortedPaths = new List<String>.from(pathList); |
| sortedPaths.sort((a, b) => a.length - b.length); |
| int pathCount = sortedPaths.length; |
| for (int i = pathCount - 1; i > 0; i--) { |
| String path = sortedPaths[i]; |
| for (int j = 0; j < i; j++) { |
| if (_isEqualOrWithin(path, sortedPaths[j])) { |
| sortedPaths.removeAt(i); |
| break; |
| } |
| } |
| } |
| return sortedPaths; |
| } |
| |
| /** |
| * Normalize all package root sources by mapping them to folders on the |
| * filesystem. Ignore any package root sources that aren't folders. |
| */ |
| void _updateNormalizedPackageRoots() { |
| normalizedPackageRoots = <String, String>{}; |
| packageRoots.forEach((String sourcePath, String targetPath) { |
| Resource resource = resourceProvider.getResource(sourcePath); |
| if (resource is Folder) { |
| normalizedPackageRoots[resource.path] = targetPath; |
| } |
| }); |
| } |
| |
| /** |
| * Create and return a source representing the given [file] within the given |
| * [context]. |
| */ |
| static Source createSourceInContext(AnalysisContext context, File file) { |
| // TODO(brianwilkerson) Optimize this, by allowing support for source |
| // factories to restore URI's from a file path rather than a source. |
| Source source = file.createSource(); |
| if (context == null) { |
| return source; |
| } |
| Uri uri = context.sourceFactory.restoreUri(source); |
| return file.createSource(uri); |
| } |
| |
| static int _commonComponents( |
| List<String> left, int count, List<String> right) { |
| int max = math.min(count, right.length); |
| for (int i = 0; i < max; i++) { |
| if (left[i] != right[i]) { |
| return i; |
| } |
| } |
| return max; |
| } |
| |
| /** |
| * Return a list of all the files in the [left] that are not in the [right]. |
| */ |
| static List<File> _diff(List<File> left, List<File> right) { |
| List<File> diff = new List.from(left); |
| for (File file in right) { |
| diff.remove(file); |
| } |
| return diff; |
| } |
| |
| static List<Resource> _getChildrenSafe(Folder folder) { |
| try { |
| return folder.getChildren(); |
| } on FileSystemException { |
| // The folder either doesn't exist or cannot be read. |
| // Either way, there are no children. |
| return const <Resource>[]; |
| } |
| } |
| } |