Chromium Code Reviews| Index: pkg/analysis_server/lib/src/single_context_manager.dart |
| diff --git a/pkg/analysis_server/lib/src/single_context_manager.dart b/pkg/analysis_server/lib/src/single_context_manager.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..6e5d51e19a4c68ab0b6a9dfc9963407fcfe2b266 |
| --- /dev/null |
| +++ b/pkg/analysis_server/lib/src/single_context_manager.dart |
| @@ -0,0 +1,366 @@ |
| +// 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.experimental; |
|
Brian Wilkerson
2016/04/25 15:31:06
"experimental" --> "single_context_manager"
|
| + |
| +import 'dart:core' hide Resource; |
| +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/source/path_filter.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; |
| + |
| +/** |
| + * 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 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:` URI's within a given folder, or `null` if we should fall back |
| + * to the standard URI resolver. |
| + */ |
| + final ResolverProvider packageResolverProvider; |
| + |
| + /** |
| + * A list of the globs used to determine which files should be analyzed. |
| + */ |
| + final List<Glob> analyzedFilesGlobs; |
| + |
| + /** |
| + * 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 context in which everything is being analyzed. |
| + */ |
| + AnalysisContext context; |
| + |
| + /** |
| + * The folder associated with the context. |
| + */ |
| + Folder contextFolder; |
| + |
| + /** |
| + * The [PathFilter] used to filter sources from being analyzed. |
| + */ |
| + PathFilter pathFilter; |
| + |
| + /** |
| + * The [packageResolverProvider] must not be `null`. |
| + */ |
| + SingleContextManager(this.resourceProvider, this.sdkManager, |
| + this.packageResolverProvider, this.analyzedFilesGlobs); |
| + |
| + @override |
| + Iterable<AnalysisContext> get analysisContexts => |
| + context == null ? <AnalysisContext>[] : <AnalysisContext>[context]; |
| + |
| + @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 |
| + bool isIgnored(String path) => pathFilter.ignored(path); |
| + |
| + @override |
| + bool isInAnalysisRoot(String path) { |
| + return _isContainedIn(includedPaths, path) && |
| + !_isContainedIn(excludedPaths, path); |
| + } |
| + |
| + @override |
| + void refresh(List<Resource> roots) { |
| + if (context != null) { |
| + // TODO(brianwilkerson) Not sure whether this is right. |
| + callbacks.removeContext(contextFolder, null); |
| + context.dispose(); |
| + context = null; |
| + contextFolder = null; |
| + pathFilter = null; |
| + 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(); |
| + if (context == null) { |
| + String contextPath = _commonPrefix(includedPaths); |
| + contextFolder = resourceProvider.getFolder(contextPath); |
| + // TODO(scheglov) watch |
|
Brian Wilkerson
2016/04/25 15:31:06
More detail would be good; I'm not sure what "watc
|
| + pathFilter = new PathFilter( |
| + contextFolder.path, null, resourceProvider.pathContext); |
| + AnalysisOptions options = new AnalysisOptionsImpl(); |
| + FolderDisposition disposition = new CustomPackageResolverDisposition( |
| + packageResolverProvider(contextFolder)); |
| + context = callbacks.addContext(contextFolder, options, disposition); |
| + ChangeSet changeSet = |
| + _buildChangeSet(added: _includedFiles(includedPaths, excludedPaths)); |
| + callbacks.applyChangesToContext(contextFolder, changeSet); |
| + } else { |
| + // TODO(scheglov) in general 'contextFolder' may be different now |
| + // 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; |
| + } |
| + // TODO(scheglov) apply pathFilter |
| + if (_isEqualOrWithinAny(excludedPaths, resource.path)) { |
| + return; |
| + } |
| + if (resource is File && resource.exists) { |
| + addedFiles.add(resource); |
| + } else if (resource is Folder) { |
| + for (Resource child in _getChildrenSafe(resource)) { |
| + _addFilesInResource(addedFiles, child, excludedPaths); |
| + } |
| + } |
| + } |
| + |
| + 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; |
| + } |
| + |
| + 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; |
| + } |
| + |
| + /** |
| + * 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; |
| + } |
| + |
| + /** |
| + * 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; |
| + } |
| + }); |
| + } |
| + |
| + static ChangeSet _buildChangeSet({List<File> added, List<File> removed}) { |
| + ChangeSet changeSet = new ChangeSet(); |
| + if (added != null) { |
| + for (File file in added) { |
| + changeSet.addedSource(file.createSource()); |
| + } |
| + } |
| + if (removed != null) { |
| + for (File file in removed) { |
| + changeSet.removedSource(file.createSource()); |
| + } |
| + } |
| + return changeSet; |
| + } |
| + |
| + 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; |
| + } |
| + |
| + static String _commonPrefix(List<String> paths) { |
| + if (paths.isEmpty) { |
| + return ''; |
| + } |
| + List<String> left = path.split(paths[0]); |
| + int count = left.length; |
| + for (int i = 0; i < paths.length; i++) { |
| + List<String> right = path.split(paths[0]); |
| + count = _commonComponents(left, count, right); |
| + } |
| + return path.joinAll(left); |
| + } |
| + |
| + /** |
| + * 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>[]; |
| + } |
| + } |
| + |
| + static bool _isContainedIn(List<String> pathList, String path) { |
| + for (String pathInList in pathList) { |
| + if (_isEqualOrWithin(path, pathInList)) { |
| + return true; |
| + } |
| + } |
| + return false; |
| + } |
| + |
| + static bool _isEqualOrWithin(String parent, String child) { |
| + return child == parent || path.isWithin(parent, child); |
| + } |
| + |
| + static bool _isEqualOrWithinAny(List<String> parents, String child) { |
| + for (String parent in parents) { |
| + if (_isEqualOrWithin(parent, child)) { |
| + 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. |
| + */ |
| + static 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; |
| + } |
| +} |