5bcf91d973
Change-Id: Ida5c2ad29f943d676a6f16a51d66892b037ad099 Reviewed-on: https://dart-review.googlesource.com/77240 Commit-Queue: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
1796 lines
60 KiB
Dart
1796 lines
60 KiB
Dart
// 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.
|
|
|
|
import 'dart:async';
|
|
import 'dart:collection';
|
|
import 'dart:convert';
|
|
import 'dart:core';
|
|
|
|
import 'package:analysis_server/src/plugin/notification_manager.dart';
|
|
import 'package:analyzer/error/error.dart';
|
|
import 'package:analyzer/file_system/file_system.dart';
|
|
import 'package:analyzer/instrumentation/instrumentation.dart';
|
|
import 'package:analyzer/src/analysis_options/analysis_options_provider.dart';
|
|
import 'package:analyzer/src/context/builder.dart';
|
|
import 'package:analyzer/src/context/context_root.dart';
|
|
import 'package:analyzer/src/dart/analysis/driver.dart';
|
|
import 'package:analyzer/src/dart/analysis/file_state.dart';
|
|
import 'package:analyzer/src/dart/sdk/sdk.dart';
|
|
import 'package:analyzer/src/file_system/file_system.dart';
|
|
import 'package:analyzer/src/generated/engine.dart';
|
|
import 'package:analyzer/src/generated/java_engine.dart';
|
|
import 'package:analyzer/src/generated/java_io.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/plugin/resolver_provider.dart';
|
|
import 'package:analyzer/src/pubspec/pubspec_validator.dart';
|
|
import 'package:analyzer/src/source/package_map_resolver.dart';
|
|
import 'package:analyzer/src/source/path_filter.dart';
|
|
import 'package:analyzer/src/source/sdk_ext.dart';
|
|
import 'package:analyzer/src/task/options.dart';
|
|
import 'package:analyzer/src/util/glob.dart';
|
|
import 'package:analyzer/src/util/uri.dart';
|
|
import 'package:analyzer/src/util/yaml.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
|
|
import 'package:analyzer_plugin/utilities/analyzer_converter.dart';
|
|
import 'package:package_config/packages.dart';
|
|
import 'package:package_config/packages_file.dart' as pkgfile show parse;
|
|
import 'package:package_config/src/packages_impl.dart' show MapPackages;
|
|
import 'package:path/path.dart' as pathos;
|
|
import 'package:watcher/watcher.dart';
|
|
import 'package:yaml/yaml.dart';
|
|
|
|
/**
|
|
* Information tracked by the [ContextManager] for each context.
|
|
*/
|
|
class ContextInfo {
|
|
/**
|
|
* The [Folder] for which this information object is created.
|
|
*/
|
|
final Folder folder;
|
|
|
|
/// The [PathFilter] used to filter sources from being analyzed.
|
|
final PathFilter pathFilter;
|
|
|
|
/**
|
|
* The enclosed pubspec-based contexts.
|
|
*/
|
|
final List<ContextInfo> children = <ContextInfo>[];
|
|
|
|
/**
|
|
* The package root for this context, or null if there is no package root.
|
|
*/
|
|
String packageRoot;
|
|
|
|
/**
|
|
* The [ContextInfo] that encloses this one, or `null` if this is the virtual
|
|
* [ContextInfo] object that acts as the ancestor of all other [ContextInfo]
|
|
* objects.
|
|
*/
|
|
ContextInfo parent;
|
|
|
|
/**
|
|
* The package description file path for this context.
|
|
*/
|
|
String packageDescriptionPath;
|
|
|
|
/**
|
|
* The folder disposition for this context.
|
|
*/
|
|
final FolderDisposition disposition;
|
|
|
|
/**
|
|
* Paths to files which determine the folder disposition and package map.
|
|
*
|
|
* TODO(paulberry): if any of these files are outside of [folder], they won't
|
|
* be watched for changes. I believe the use case for watching these files
|
|
* is no longer relevant.
|
|
*/
|
|
Set<String> _dependencies = new Set<String>();
|
|
|
|
/**
|
|
* The analysis driver that was created for the [folder].
|
|
*/
|
|
AnalysisDriver analysisDriver;
|
|
|
|
/**
|
|
* Map from full path to the [Source] object, for each source that has been
|
|
* added to the context.
|
|
*/
|
|
Map<String, Source> sources = new HashMap<String, Source>();
|
|
|
|
ContextInfo(ContextManagerImpl contextManager, this.parent, Folder folder,
|
|
File packagespecFile, this.packageRoot, this.disposition)
|
|
: folder = folder,
|
|
pathFilter = new PathFilter(
|
|
folder.path, null, contextManager.resourceProvider.pathContext) {
|
|
packageDescriptionPath = packagespecFile.path;
|
|
parent.children.add(this);
|
|
}
|
|
|
|
/**
|
|
* Create the virtual [ContextInfo] which acts as an ancestor to all other
|
|
* [ContextInfo]s.
|
|
*/
|
|
ContextInfo._root()
|
|
: folder = null,
|
|
pathFilter = null,
|
|
packageRoot = null,
|
|
disposition = null;
|
|
|
|
/**
|
|
* Iterate through all [children] and their children, recursively.
|
|
*/
|
|
Iterable<ContextInfo> get descendants sync* {
|
|
for (ContextInfo child in children) {
|
|
yield child;
|
|
yield* child.descendants;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if this is a "top level" context, meaning that the folder
|
|
* associated with it is not contained within any other folders that have an
|
|
* associated context.
|
|
*/
|
|
bool get isTopLevel => parent.parent == null;
|
|
|
|
/**
|
|
* Returns `true` if [path] is excluded, as it is in one of the children.
|
|
*/
|
|
bool excludes(String path) {
|
|
return children.any((child) {
|
|
return child.folder.contains(path);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if [resource] is excluded, as it is in one of the children.
|
|
*/
|
|
bool excludesResource(Resource resource) => excludes(resource.path);
|
|
|
|
/**
|
|
* Return the first [ContextInfo] in [children] whose associated folder is or
|
|
* contains [path]. If there is no such [ContextInfo], return `null`.
|
|
*/
|
|
ContextInfo findChildInfoFor(String path) {
|
|
for (ContextInfo info in children) {
|
|
if (info.folder.isOrContains(path)) {
|
|
return info;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Determine if the given [path] is one of the dependencies most recently
|
|
* passed to [setDependencies].
|
|
*/
|
|
bool hasDependency(String path) => _dependencies.contains(path);
|
|
|
|
/// Returns `true` if [path] should be ignored.
|
|
bool ignored(String path) => pathFilter.ignored(path);
|
|
|
|
/**
|
|
* Returns `true` if [path] is the package description file for this context
|
|
* (pubspec.yaml or .packages).
|
|
*/
|
|
bool isPathToPackageDescription(String path) =>
|
|
path == packageDescriptionPath;
|
|
|
|
/**
|
|
* Update the set of dependencies for this context.
|
|
*/
|
|
void setDependencies(Iterable<String> newDependencies) {
|
|
_dependencies = newDependencies.toSet();
|
|
}
|
|
|
|
/**
|
|
* Return `true` if the given [path] is managed by this context or by
|
|
* any of its children.
|
|
*/
|
|
bool _managesOrHasChildThatManages(String path) {
|
|
if (parent == null) {
|
|
for (ContextInfo child in children) {
|
|
if (child._managesOrHasChildThatManages(path)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
} else {
|
|
if (!folder.isOrContains(path)) {
|
|
return false;
|
|
}
|
|
for (ContextInfo child in children) {
|
|
if (child._managesOrHasChildThatManages(path)) {
|
|
return true;
|
|
}
|
|
}
|
|
return !pathFilter.ignored(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Class that maintains a mapping from included/excluded paths to a set of
|
|
* folders that should correspond to analysis contexts.
|
|
*/
|
|
abstract class ContextManager {
|
|
// TODO(brianwilkerson) Support:
|
|
// setting the default analysis options
|
|
// setting the default content cache
|
|
// setting the default SDK
|
|
// telling server when a context has been added or removed (see onContextsChanged)
|
|
// telling server when a context needs to be re-analyzed
|
|
// notifying the client when results should be flushed
|
|
// using analyzeFileFunctions to determine which files to analyze
|
|
//
|
|
// TODO(brianwilkerson) Move this class to a public library.
|
|
|
|
/**
|
|
* Get the callback interface used to create, destroy, and update contexts.
|
|
*/
|
|
ContextManagerCallbacks get callbacks;
|
|
|
|
/**
|
|
* Set the callback interface used to create, destroy, and update contexts.
|
|
*/
|
|
void set callbacks(ContextManagerCallbacks value);
|
|
|
|
/**
|
|
* A table mapping [Folder]s to the [AnalysisDriver]s associated with them.
|
|
*/
|
|
Map<Folder, AnalysisDriver> get driverMap;
|
|
|
|
/**
|
|
* Return the list of excluded paths (folders and files) most recently passed
|
|
* to [setRoots].
|
|
*/
|
|
List<String> get excludedPaths;
|
|
|
|
/**
|
|
* Return the list of included paths (folders and files) most recently passed
|
|
* to [setRoots].
|
|
*/
|
|
List<String> get includedPaths;
|
|
|
|
/**
|
|
* Like [getDriverFor], but returns the [Folder] which allows plugins to
|
|
* create & manage their own tree of drivers just like using [getDriverFor].
|
|
*
|
|
* This folder should be the root of analysis context, not just the containing
|
|
* folder of the path (like basename), as this is NOT just a file API.
|
|
*
|
|
* This exists at least temporarily, for plugin support until the new API is
|
|
* ready.
|
|
*/
|
|
Folder getContextFolderFor(String path);
|
|
|
|
/**
|
|
* Return the [AnalysisDriver] for the "innermost" context whose associated
|
|
* folder is or contains the given path. ("innermost" refers to the nesting
|
|
* of contexts, so if there is a context for path /foo and a context for
|
|
* path /foo/bar, then the innermost context containing /foo/bar/baz.dart is
|
|
* the context for /foo/bar.)
|
|
*
|
|
* If no driver contains the given path, `null` is returned.
|
|
*/
|
|
AnalysisDriver getDriverFor(String path);
|
|
|
|
/**
|
|
* Return a list of all of the analysis drivers reachable from the given
|
|
* [analysisRoot] (the driver associated with [analysisRoot] and all of its
|
|
* descendants).
|
|
*/
|
|
List<AnalysisDriver> getDriversInAnalysisRoot(Folder analysisRoot);
|
|
|
|
/**
|
|
* Return `true` if the given [path] is ignored by a [ContextInfo] whose
|
|
* folder contains it.
|
|
*/
|
|
bool isIgnored(String path);
|
|
|
|
/**
|
|
* Return `true` if the given absolute [path] is in one of the current
|
|
* root folders and is not excluded.
|
|
*/
|
|
bool isInAnalysisRoot(String path);
|
|
|
|
/**
|
|
* Return the number of contexts reachable from the given [analysisRoot] (the
|
|
* context associated with [analysisRoot] and all of its descendants).
|
|
*/
|
|
int numberOfContextsInAnalysisRoot(Folder analysisRoot);
|
|
|
|
/**
|
|
* Rebuild the set of contexts from scratch based on the data last sent to
|
|
* [setRoots]. Only contexts contained in the given list of analysis [roots]
|
|
* will be rebuilt, unless the list is `null`, in which case every context
|
|
* will be rebuilt.
|
|
*/
|
|
void refresh(List<Resource> roots);
|
|
|
|
/**
|
|
* Change the set of paths which should be used as starting points to
|
|
* determine the context directories.
|
|
*/
|
|
void setRoots(List<String> includedPaths, List<String> excludedPaths,
|
|
Map<String, String> packageRoots);
|
|
}
|
|
|
|
/**
|
|
* Callback interface used by [ContextManager] to (a) request that contexts be
|
|
* created, destroyed or updated, (b) inform the client when "pub list"
|
|
* operations are in progress, and (c) determine which files should be
|
|
* analyzed.
|
|
*
|
|
* TODO(paulberry): eliminate this interface, and instead have [ContextManager]
|
|
* operations return data structures describing how context state should be
|
|
* modified.
|
|
*/
|
|
abstract class ContextManagerCallbacks {
|
|
/**
|
|
* Return the notification manager associated with the server.
|
|
*/
|
|
NotificationManager get notificationManager;
|
|
|
|
/**
|
|
* Create and return a new analysis driver rooted at the given [folder], with
|
|
* the given analysis [options].
|
|
*/
|
|
AnalysisDriver addAnalysisDriver(
|
|
Folder folder, ContextRoot contextRoot, AnalysisOptions options);
|
|
|
|
/**
|
|
* An [event] was processed, so analysis state might be different now.
|
|
*/
|
|
void afterWatchEvent(WatchEvent event);
|
|
|
|
/**
|
|
* Called when the set of files associated with a context have changed (or
|
|
* some of those files have been modified). [changeSet] is the set of
|
|
* changes that need to be applied to the context.
|
|
*/
|
|
void applyChangesToContext(Folder contextFolder, ChangeSet changeSet);
|
|
|
|
/**
|
|
* The given [file] was removed from the folder analyzed in the [driver].
|
|
*/
|
|
void applyFileRemoved(AnalysisDriver driver, String file);
|
|
|
|
/**
|
|
* Sent the given watch [event] to any interested plugins.
|
|
*/
|
|
void broadcastWatchEvent(WatchEvent event);
|
|
|
|
/**
|
|
* Create and return a context builder that can be used to create a context
|
|
* for the files in the given [folder] when analyzed using the given [options].
|
|
*/
|
|
ContextBuilder createContextBuilder(Folder folder, AnalysisOptions options);
|
|
|
|
/**
|
|
* Remove the context associated with the given [folder]. [flushedFiles] is
|
|
* a list of the files which will be "orphaned" by removing this context
|
|
* (they will no longer be analyzed by any context).
|
|
*/
|
|
void removeContext(Folder folder, List<String> flushedFiles);
|
|
}
|
|
|
|
/**
|
|
* Class that maintains a mapping from included/excluded paths to a set of
|
|
* folders that should correspond to analysis contexts.
|
|
*/
|
|
class ContextManagerImpl implements ContextManager {
|
|
/**
|
|
* The name of the `doc` directory.
|
|
*/
|
|
static const String DOC_DIR_NAME = 'doc';
|
|
|
|
/**
|
|
* The name of the `lib` directory.
|
|
*/
|
|
static const String LIB_DIR_NAME = 'lib';
|
|
|
|
/**
|
|
* File name of pubspec files.
|
|
*/
|
|
static const String PUBSPEC_NAME = 'pubspec.yaml';
|
|
|
|
/**
|
|
* File name of package spec files.
|
|
*/
|
|
static const String PACKAGE_SPEC_NAME = '.packages';
|
|
|
|
/**
|
|
* The name of the key in an embedder file whose value is the list of
|
|
* libraries in the SDK.
|
|
* TODO(brianwilkerson) This is also defined in sdk.dart.
|
|
*/
|
|
static const String _EMBEDDED_LIB_MAP_KEY = 'embedded_libs';
|
|
|
|
/**
|
|
* The [ResourceProvider] using which paths are converted into [Resource]s.
|
|
*/
|
|
final ResourceProvider resourceProvider;
|
|
|
|
/**
|
|
* The file content overlay.
|
|
*/
|
|
final FileContentOverlay fileContentOverlay;
|
|
|
|
/**
|
|
* The manager used to access the SDK that should be associated with a
|
|
* particular context.
|
|
*/
|
|
final DartSdkManager sdkManager;
|
|
|
|
/**
|
|
* The context used to work with file system paths.
|
|
*/
|
|
pathos.Context pathContext;
|
|
|
|
/**
|
|
* The list of excluded paths (folders and files) most recently passed to
|
|
* [setRoots].
|
|
*/
|
|
List<String> excludedPaths = <String>[];
|
|
|
|
/**
|
|
* The list of included paths (folders and files) most recently passed to
|
|
* [setRoots].
|
|
*/
|
|
List<String> includedPaths = <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>{};
|
|
|
|
/**
|
|
* 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 default options used to create new analysis contexts.
|
|
*/
|
|
final AnalysisOptionsImpl defaultContextOptions;
|
|
|
|
/**
|
|
* The instrumentation service used to report instrumentation data.
|
|
*/
|
|
final InstrumentationService _instrumentationService;
|
|
|
|
@override
|
|
ContextManagerCallbacks callbacks;
|
|
|
|
/**
|
|
* Virtual [ContextInfo] which acts as the ancestor of all other
|
|
* [ContextInfo]s.
|
|
*/
|
|
final ContextInfo rootInfo = new ContextInfo._root();
|
|
|
|
@override
|
|
final Map<Folder, AnalysisDriver> driverMap =
|
|
new HashMap<Folder, AnalysisDriver>();
|
|
|
|
/**
|
|
* Stream subscription we are using to watch each analysis root directory for
|
|
* changes.
|
|
*/
|
|
final Map<Folder, StreamSubscription<WatchEvent>> changeSubscriptions =
|
|
<Folder, StreamSubscription<WatchEvent>>{};
|
|
|
|
ContextManagerImpl(
|
|
this.resourceProvider,
|
|
this.fileContentOverlay,
|
|
this.sdkManager,
|
|
this.packageResolverProvider,
|
|
this.analyzedFilesGlobs,
|
|
this._instrumentationService,
|
|
this.defaultContextOptions) {
|
|
pathContext = resourceProvider.pathContext;
|
|
}
|
|
|
|
/**
|
|
* Check if this map defines embedded libraries.
|
|
*/
|
|
bool definesEmbeddedLibs(Map map) => map[_EMBEDDED_LIB_MAP_KEY] != null;
|
|
|
|
Folder getContextFolderFor(String path) {
|
|
return _getInnermostContextInfoFor(path)?.folder;
|
|
}
|
|
|
|
/**
|
|
* For testing: get the [ContextInfo] object for the given [folder], if any.
|
|
*/
|
|
ContextInfo getContextInfoFor(Folder folder) {
|
|
ContextInfo info = _getInnermostContextInfoFor(folder.path);
|
|
if (info != null && folder == info.folder) {
|
|
return info;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
AnalysisDriver getDriverFor(String path) {
|
|
return _getInnermostContextInfoFor(path)?.analysisDriver;
|
|
}
|
|
|
|
@override
|
|
List<AnalysisDriver> getDriversInAnalysisRoot(Folder analysisRoot) {
|
|
List<AnalysisDriver> drivers = <AnalysisDriver>[];
|
|
void addContextAndDescendants(ContextInfo info) {
|
|
drivers.add(info.analysisDriver);
|
|
info.children.forEach(addContextAndDescendants);
|
|
}
|
|
|
|
ContextInfo innermostContainingInfo =
|
|
_getInnermostContextInfoFor(analysisRoot.path);
|
|
if (innermostContainingInfo != null) {
|
|
if (analysisRoot == innermostContainingInfo.folder) {
|
|
addContextAndDescendants(innermostContainingInfo);
|
|
} else {
|
|
for (ContextInfo info in innermostContainingInfo.children) {
|
|
if (analysisRoot.isOrContains(info.folder.path)) {
|
|
addContextAndDescendants(info);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return drivers;
|
|
}
|
|
|
|
@override
|
|
bool isIgnored(String path) {
|
|
ContextInfo info = rootInfo;
|
|
do {
|
|
info = info.findChildInfoFor(path);
|
|
if (info == null) {
|
|
return false;
|
|
}
|
|
if (info.ignored(path)) {
|
|
return true;
|
|
}
|
|
} while (true);
|
|
}
|
|
|
|
@override
|
|
bool isInAnalysisRoot(String path) {
|
|
// check if excluded
|
|
if (_isExcluded(path)) {
|
|
return false;
|
|
}
|
|
// check if in one of the roots
|
|
for (ContextInfo info in rootInfo.children) {
|
|
if (info.folder.contains(path)) {
|
|
return true;
|
|
}
|
|
}
|
|
// no
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
int numberOfContextsInAnalysisRoot(Folder analysisRoot) {
|
|
int count = 0;
|
|
void addContextAndDescendants(ContextInfo info) {
|
|
count++;
|
|
info.children.forEach(addContextAndDescendants);
|
|
}
|
|
|
|
ContextInfo innermostContainingInfo =
|
|
_getInnermostContextInfoFor(analysisRoot.path);
|
|
if (innermostContainingInfo != null) {
|
|
if (analysisRoot == innermostContainingInfo.folder) {
|
|
addContextAndDescendants(innermostContainingInfo);
|
|
} else {
|
|
for (ContextInfo info in innermostContainingInfo.children) {
|
|
if (analysisRoot.isOrContains(info.folder.path)) {
|
|
addContextAndDescendants(info);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Process [options] for the given context [info].
|
|
*/
|
|
void processOptionsForDriver(
|
|
ContextInfo info, AnalysisOptionsImpl analysisOptions, YamlMap options) {
|
|
if (options == null) {
|
|
return;
|
|
}
|
|
|
|
// Check for embedded options.
|
|
YamlMap embeddedOptions = _getEmbeddedOptions(info);
|
|
if (embeddedOptions != null) {
|
|
options = new Merger().merge(embeddedOptions, options);
|
|
}
|
|
|
|
applyToAnalysisOptions(analysisOptions, options);
|
|
|
|
if (analysisOptions.excludePatterns != null) {
|
|
// Set ignore patterns.
|
|
setIgnorePatternsForContext(info, analysisOptions.excludePatterns);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void refresh(List<Resource> roots) {
|
|
// Destroy old contexts
|
|
List<ContextInfo> contextInfos = rootInfo.descendants.toList();
|
|
if (roots == null) {
|
|
contextInfos.forEach(_destroyContext);
|
|
} else {
|
|
roots.forEach((Resource resource) {
|
|
contextInfos.forEach((ContextInfo contextInfo) {
|
|
if (resource is Folder &&
|
|
resource.isOrContains(contextInfo.folder.path)) {
|
|
_destroyContext(contextInfo);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Rebuild contexts based on the data last sent to setRoots().
|
|
setRoots(includedPaths, excludedPaths, packageRoots);
|
|
}
|
|
|
|
/**
|
|
* Sets the [ignorePatterns] for the context having info [info].
|
|
*/
|
|
void setIgnorePatternsForContext(
|
|
ContextInfo info, List<String> ignorePatterns) {
|
|
info.pathFilter.setIgnorePatterns(ignorePatterns);
|
|
}
|
|
|
|
@override
|
|
void setRoots(List<String> includedPaths, List<String> excludedPaths,
|
|
Map<String, String> packageRoots) {
|
|
this.packageRoots = packageRoots;
|
|
|
|
// Normalize all package root sources by mapping them to folders on the
|
|
// filesystem. Ignore any package root sources that aren't folders.
|
|
normalizedPackageRoots = <String, String>{};
|
|
packageRoots.forEach((String sourcePath, String targetPath) {
|
|
Resource resource = resourceProvider.getResource(sourcePath);
|
|
if (resource is Folder) {
|
|
normalizedPackageRoots[resource.path] = targetPath;
|
|
}
|
|
});
|
|
|
|
List<ContextInfo> contextInfos = rootInfo.descendants.toList();
|
|
// included
|
|
List<Folder> includedFolders = <Folder>[];
|
|
{
|
|
// Sort paths to ensure that outer roots are handled before inner roots,
|
|
// so we can correctly ignore inner roots, which are already managed
|
|
// by outer roots.
|
|
LinkedHashSet<String> uniqueIncludedPaths =
|
|
new LinkedHashSet<String>.from(includedPaths);
|
|
List<String> sortedIncludedPaths = uniqueIncludedPaths.toList();
|
|
sortedIncludedPaths.sort((a, b) => a.length - b.length);
|
|
// Convert paths to folders.
|
|
for (String path in sortedIncludedPaths) {
|
|
Resource resource = resourceProvider.getResource(path);
|
|
if (resource is Folder) {
|
|
includedFolders.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 {
|
|
// TODO(scheglov) implemented separate files analysis
|
|
throw new UnimplementedError('$path is not a folder. '
|
|
'Only support for folder analysis is implemented currently.');
|
|
}
|
|
}
|
|
}
|
|
this.includedPaths = includedPaths;
|
|
// excluded
|
|
List<String> oldExcludedPaths = this.excludedPaths;
|
|
this.excludedPaths = excludedPaths;
|
|
// destroy old contexts
|
|
for (ContextInfo contextInfo in contextInfos) {
|
|
bool isIncluded = includedFolders.any((folder) {
|
|
return folder.isOrContains(contextInfo.folder.path);
|
|
});
|
|
if (!isIncluded) {
|
|
_destroyContext(contextInfo);
|
|
}
|
|
}
|
|
// Update package roots for existing contexts
|
|
for (ContextInfo info in rootInfo.descendants) {
|
|
String newPackageRoot = normalizedPackageRoots[info.folder.path];
|
|
if (info.packageRoot != newPackageRoot) {
|
|
info.packageRoot = newPackageRoot;
|
|
_recomputeFolderDisposition(info);
|
|
}
|
|
}
|
|
// create new contexts
|
|
for (Folder includedFolder in includedFolders) {
|
|
String includedPath = includedFolder.path;
|
|
bool isManaged = rootInfo._managesOrHasChildThatManages(includedPath);
|
|
if (!isManaged) {
|
|
ContextInfo parent = _getParentForNewContext(includedPath);
|
|
changeSubscriptions[includedFolder] =
|
|
includedFolder.changes.listen(_handleWatchEvent);
|
|
_createContexts(parent, includedFolder, excludedPaths, false);
|
|
}
|
|
}
|
|
// remove newly excluded sources
|
|
for (ContextInfo info in rootInfo.descendants) {
|
|
// prepare excluded sources
|
|
Map<String, Source> excludedSources = new HashMap<String, Source>();
|
|
info.sources.forEach((String path, Source source) {
|
|
if (_isExcludedBy(excludedPaths, path) &&
|
|
!_isExcludedBy(oldExcludedPaths, path)) {
|
|
excludedSources[path] = source;
|
|
}
|
|
});
|
|
// apply exclusion
|
|
ChangeSet changeSet = new ChangeSet();
|
|
excludedSources.forEach((String path, Source source) {
|
|
info.sources.remove(path);
|
|
changeSet.removedSource(source);
|
|
});
|
|
callbacks.applyChangesToContext(info.folder, changeSet);
|
|
}
|
|
// add previously excluded sources
|
|
for (ContextInfo info in rootInfo.descendants) {
|
|
ChangeSet changeSet = new ChangeSet();
|
|
_addPreviouslyExcludedSources(
|
|
info, changeSet, info.folder, oldExcludedPaths);
|
|
callbacks.applyChangesToContext(info.folder, changeSet);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively adds all Dart and HTML files to the [changeSet].
|
|
*/
|
|
void _addPreviouslyExcludedSources(ContextInfo info, ChangeSet changeSet,
|
|
Folder folder, List<String> oldExcludedPaths) {
|
|
if (info.excludesResource(folder)) {
|
|
return;
|
|
}
|
|
List<Resource> children;
|
|
try {
|
|
children = folder.getChildren();
|
|
} on FileSystemException {
|
|
// The folder no longer exists, or cannot be read, to there's nothing to
|
|
// do.
|
|
return;
|
|
}
|
|
for (Resource child in children) {
|
|
String path = child.path;
|
|
// Path is being ignored.
|
|
if (info.ignored(path)) {
|
|
continue;
|
|
}
|
|
// add files, recurse into folders
|
|
if (child is File) {
|
|
// ignore if should not be analyzed at all
|
|
if (!_shouldFileBeAnalyzed(child)) {
|
|
continue;
|
|
}
|
|
// ignore if was not excluded
|
|
bool wasExcluded = _isExcludedBy(oldExcludedPaths, path) &&
|
|
!_isExcludedBy(excludedPaths, path);
|
|
if (!wasExcluded) {
|
|
continue;
|
|
}
|
|
// do add the file
|
|
Source source = createSourceInContext(info.analysisDriver, child);
|
|
changeSet.addedSource(source);
|
|
info.sources[path] = source;
|
|
} else if (child is Folder) {
|
|
_addPreviouslyExcludedSources(info, changeSet, child, oldExcludedPaths);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively adds all Dart and HTML files to the [changeSet].
|
|
*/
|
|
void _addSourceFiles(ChangeSet changeSet, Folder folder, ContextInfo info) {
|
|
if (info.excludesResource(folder) ||
|
|
folder.shortName.startsWith('.') ||
|
|
_isInTopLevelDocDir(info.folder.path, folder.path)) {
|
|
return;
|
|
}
|
|
List<Resource> children = null;
|
|
try {
|
|
children = folder.getChildren();
|
|
} on FileSystemException {
|
|
// The directory either doesn't exist or cannot be read. Either way, there
|
|
// are no children that need to be added.
|
|
return;
|
|
}
|
|
for (Resource child in children) {
|
|
String path = child.path;
|
|
// ignore excluded files or folders
|
|
if (_isExcluded(path) || info.excludes(path) || info.ignored(path)) {
|
|
continue;
|
|
}
|
|
// add files, recurse into folders
|
|
if (child is File) {
|
|
if (_shouldFileBeAnalyzed(child)) {
|
|
Source source = createSourceInContext(info.analysisDriver, child);
|
|
changeSet.addedSource(source);
|
|
info.sources[path] = source;
|
|
}
|
|
} else if (child is Folder) {
|
|
_addSourceFiles(changeSet, child, info);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Use the given analysis [driver] to analyze the content of the analysis
|
|
* options file at the given [path].
|
|
*/
|
|
void _analyzeAnalysisOptionsFile(AnalysisDriver driver, String path) {
|
|
List<protocol.AnalysisError> convertedErrors;
|
|
try {
|
|
String content = _readFile(path);
|
|
LineInfo lineInfo = _computeLineInfo(content);
|
|
List<AnalysisError> errors =
|
|
GenerateOptionsErrorsTask.analyzeAnalysisOptions(
|
|
resourceProvider.getFile(path).createSource(),
|
|
content,
|
|
driver.sourceFactory);
|
|
AnalyzerConverter converter = new AnalyzerConverter();
|
|
convertedErrors = converter.convertAnalysisErrors(errors,
|
|
lineInfo: lineInfo, options: driver.analysisOptions);
|
|
} catch (exception) {
|
|
// If the file cannot be analyzed, fall through to clear any previous
|
|
// errors.
|
|
}
|
|
callbacks.notificationManager.recordAnalysisErrors(
|
|
NotificationManager.serverId,
|
|
path,
|
|
convertedErrors ?? <protocol.AnalysisError>[]);
|
|
}
|
|
|
|
/**
|
|
* Use the given analysis [driver] to analyze the content of the pubspec file
|
|
* at the given [path].
|
|
*/
|
|
void _analyzePubspecFile(AnalysisDriver driver, String path) {
|
|
List<protocol.AnalysisError> convertedErrors;
|
|
try {
|
|
String content = _readFile(path);
|
|
YamlNode node = loadYamlNode(content);
|
|
if (node is YamlMap) {
|
|
PubspecValidator validator = new PubspecValidator(
|
|
resourceProvider, resourceProvider.getFile(path).createSource());
|
|
LineInfo lineInfo = _computeLineInfo(content);
|
|
List<AnalysisError> errors = validator.validate(node.nodes);
|
|
AnalyzerConverter converter = new AnalyzerConverter();
|
|
convertedErrors = converter.convertAnalysisErrors(errors,
|
|
lineInfo: lineInfo, options: driver.analysisOptions);
|
|
}
|
|
} catch (exception) {
|
|
// If the file cannot be analyzed, fall through to clear any previous
|
|
// errors.
|
|
}
|
|
callbacks.notificationManager.recordAnalysisErrors(
|
|
NotificationManager.serverId,
|
|
path,
|
|
convertedErrors ?? <protocol.AnalysisError>[]);
|
|
}
|
|
|
|
void _checkForAnalysisOptionsUpdate(
|
|
String path, ContextInfo info, ChangeType changeType) {
|
|
if (AnalysisEngine.isAnalysisOptionsFileName(path, pathContext)) {
|
|
AnalysisDriver driver = info.analysisDriver;
|
|
if (driver == null) {
|
|
// I suspect that this happens as a result of a race condition: server
|
|
// has determined that the file (at [path]) is in a context, but hasn't
|
|
// yet created a driver for that context.
|
|
return;
|
|
}
|
|
String contextRoot = info.folder.path;
|
|
ContextBuilder builder =
|
|
callbacks.createContextBuilder(info.folder, defaultContextOptions);
|
|
AnalysisOptions options = builder.getAnalysisOptions(contextRoot,
|
|
contextRoot: driver.contextRoot);
|
|
SourceFactory factory = builder.createSourceFactory(contextRoot, options);
|
|
driver.configure(analysisOptions: options, sourceFactory: factory);
|
|
// TODO(brianwilkerson) Set exclusion patterns.
|
|
_analyzeAnalysisOptionsFile(driver, path);
|
|
}
|
|
}
|
|
|
|
void _checkForPackagespecUpdate(
|
|
String path, ContextInfo info, Folder folder) {
|
|
// Check to see if this is the .packages file for this context and if so,
|
|
// update the context's source factory.
|
|
if (pathContext.basename(path) == PACKAGE_SPEC_NAME) {
|
|
String contextRoot = info.folder.path;
|
|
ContextBuilder builder =
|
|
callbacks.createContextBuilder(info.folder, defaultContextOptions);
|
|
AnalysisDriver driver = info.analysisDriver;
|
|
if (driver != null) {
|
|
AnalysisOptions options = builder.getAnalysisOptions(contextRoot,
|
|
contextRoot: driver.contextRoot);
|
|
SourceFactory factory =
|
|
builder.createSourceFactory(contextRoot, options);
|
|
driver.configure(analysisOptions: options, sourceFactory: factory);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _checkForPubspecUpdate(
|
|
String path, ContextInfo info, ChangeType changeType) {
|
|
if (_isPubspec(path)) {
|
|
AnalysisDriver driver = info.analysisDriver;
|
|
if (driver == null) {
|
|
// I suspect that this happens as a result of a race condition: server
|
|
// has determined that the file (at [path]) is in a context, but hasn't
|
|
// yet created a driver for that context.
|
|
return;
|
|
}
|
|
_analyzePubspecFile(driver, path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compute the set of files that are being flushed, this is defined as
|
|
* the set of sources in the removed context (context.sources), that are
|
|
* orphaned by this context being removed (no other context includes this
|
|
* file.)
|
|
*/
|
|
List<String> _computeFlushedFiles(ContextInfo info) {
|
|
Set<String> flushedFiles = info.analysisDriver.addedFiles.toSet();
|
|
for (ContextInfo contextInfo in rootInfo.descendants) {
|
|
AnalysisDriver other = contextInfo.analysisDriver;
|
|
if (other != info.analysisDriver) {
|
|
flushedFiles.removeAll(other.addedFiles);
|
|
}
|
|
}
|
|
return flushedFiles.toList(growable: false);
|
|
}
|
|
|
|
/**
|
|
* Compute the appropriate [FolderDisposition] for [folder]. Use
|
|
* [addDependency] to indicate which files needed to be consulted in order to
|
|
* figure out the [FolderDisposition]; these dependencies will be watched in
|
|
* order to determine when it is necessary to call this function again.
|
|
*
|
|
* TODO(paulberry): use [addDependency] for tracking all folder disposition
|
|
* dependencies (currently we only use it to track "pub list" dependencies).
|
|
*/
|
|
FolderDisposition _computeFolderDisposition(
|
|
Folder folder, void addDependency(String path), File packagespecFile) {
|
|
String packageRoot = normalizedPackageRoots[folder.path];
|
|
if (packageRoot != null) {
|
|
// TODO(paulberry): We shouldn't be using JavaFile here because it
|
|
// makes the code untestable (see dartbug.com/23909).
|
|
JavaFile packagesDirOrFile = new JavaFile(packageRoot);
|
|
Map<String, List<Folder>> packageMap = new Map<String, List<Folder>>();
|
|
if (packagesDirOrFile.isDirectory()) {
|
|
for (JavaFile file in packagesDirOrFile.listFiles()) {
|
|
// Ensure symlinks in packages directory are canonicalized
|
|
// to prevent 'type X cannot be assigned to type X' warnings
|
|
String path;
|
|
try {
|
|
path = file.getCanonicalPath();
|
|
} catch (e, s) {
|
|
// Ignore packages that do not exist
|
|
_instrumentationService.logException(e, s);
|
|
continue;
|
|
}
|
|
Resource res = resourceProvider.getResource(path);
|
|
if (res is Folder) {
|
|
packageMap[file.getName()] = <Folder>[res];
|
|
}
|
|
}
|
|
return new PackageMapDisposition(packageMap, packageRoot: packageRoot);
|
|
} else if (packagesDirOrFile.isFile()) {
|
|
File packageSpecFile = resourceProvider.getFile(packageRoot);
|
|
Packages packages = _readPackagespec(packageSpecFile);
|
|
if (packages != null) {
|
|
return new PackagesFileDisposition(packages);
|
|
}
|
|
}
|
|
// The package root does not exist (or is not a folder). Since
|
|
// [setRoots] ignores any package roots that don't exist (or aren't
|
|
// folders), the only way we should be able to get here is due to a race
|
|
// condition. In any case, the package root folder is gone, so we can't
|
|
// resolve packages.
|
|
return new NoPackageFolderDisposition(packageRoot: packageRoot);
|
|
} else {
|
|
// Try .packages first.
|
|
if (pathContext.basename(packagespecFile.path) == PACKAGE_SPEC_NAME) {
|
|
Packages packages = _readPackagespec(packagespecFile);
|
|
return new PackagesFileDisposition(packages);
|
|
}
|
|
|
|
if (packageResolverProvider != null) {
|
|
UriResolver resolver = packageResolverProvider(folder);
|
|
if (resolver != null) {
|
|
return new CustomPackageResolverDisposition(resolver);
|
|
}
|
|
}
|
|
|
|
return new NoPackageFolderDisposition();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compute line information for the given [content].
|
|
*/
|
|
LineInfo _computeLineInfo(String content) {
|
|
List<int> lineStarts = StringUtilities.computeLineStarts(content);
|
|
return new LineInfo(lineStarts);
|
|
}
|
|
|
|
/**
|
|
* Create an object that can be used to find and read the analysis options
|
|
* file for code being analyzed using the given [packages].
|
|
*/
|
|
AnalysisOptionsProvider _createAnalysisOptionsProvider(Packages packages) {
|
|
Map<String, List<Folder>> packageMap =
|
|
new ContextBuilder(resourceProvider, null, null)
|
|
.convertPackagesToMap(packages);
|
|
List<UriResolver> resolvers = <UriResolver>[
|
|
new ResourceUriResolver(resourceProvider),
|
|
new PackageMapUriResolver(resourceProvider, packageMap),
|
|
];
|
|
SourceFactory sourceFactory =
|
|
new SourceFactory(resolvers, packages, resourceProvider);
|
|
return new AnalysisOptionsProvider(sourceFactory);
|
|
}
|
|
|
|
/**
|
|
* Create a new empty context associated with [folder], having parent
|
|
* [parent] and using [packagesFile] to resolve package URI's.
|
|
*/
|
|
ContextInfo _createContext(ContextInfo parent, Folder folder,
|
|
List<String> excludedPaths, File packagesFile) {
|
|
List<String> dependencies = <String>[];
|
|
FolderDisposition disposition =
|
|
_computeFolderDisposition(folder, dependencies.add, packagesFile);
|
|
ContextInfo info = new ContextInfo(this, parent, folder, packagesFile,
|
|
normalizedPackageRoots[folder.path], disposition);
|
|
|
|
File optionsFile;
|
|
YamlMap optionMap;
|
|
try {
|
|
AnalysisOptionsProvider provider =
|
|
_createAnalysisOptionsProvider(disposition.packages);
|
|
optionsFile = provider.getOptionsFile(info.folder, crawlUp: true);
|
|
if (optionsFile != null) {
|
|
optionMap = provider.getOptionsFromFile(optionsFile);
|
|
}
|
|
} catch (_) {
|
|
// Parse errors are reported elsewhere.
|
|
}
|
|
AnalysisOptions options =
|
|
new AnalysisOptionsImpl.from(defaultContextOptions);
|
|
applyToAnalysisOptions(options, optionMap);
|
|
|
|
info.setDependencies(dependencies);
|
|
String includedPath = folder.path;
|
|
List<String> containedExcludedPaths = excludedPaths
|
|
.where((String excludedPath) =>
|
|
pathContext.isWithin(includedPath, excludedPath))
|
|
.toList();
|
|
processOptionsForDriver(info, options, optionMap);
|
|
ContextRoot contextRoot = new ContextRoot(
|
|
folder.path, containedExcludedPaths,
|
|
pathContext: pathContext);
|
|
if (optionsFile != null) {
|
|
contextRoot.optionsFilePath = optionsFile.path;
|
|
}
|
|
info.analysisDriver =
|
|
callbacks.addAnalysisDriver(folder, contextRoot, options);
|
|
if (optionsFile != null) {
|
|
_analyzeAnalysisOptionsFile(info.analysisDriver, optionsFile.path);
|
|
}
|
|
File pubspecFile = folder.getChildAssumingFile(PUBSPEC_NAME);
|
|
if (pubspecFile.exists) {
|
|
_analyzePubspecFile(info.analysisDriver, pubspecFile.path);
|
|
}
|
|
return info;
|
|
}
|
|
|
|
/**
|
|
* Potentially create a new context associated with the given [folder].
|
|
*
|
|
* If there are subfolders with 'pubspec.yaml' files, separate contexts are
|
|
* created for them and excluded from the context associated with the
|
|
* [folder].
|
|
*
|
|
* If [withPackageSpecOnly] is `true`, a context will be created only if there
|
|
* is a 'pubspec.yaml' or '.packages' file in the [folder].
|
|
*
|
|
* [parent] should be the parent of any contexts that are created.
|
|
*/
|
|
void _createContexts(ContextInfo parent, Folder folder,
|
|
List<String> excludedPaths, bool withPackageSpecOnly) {
|
|
if (_isExcluded(folder.path) || folder.shortName.startsWith('.')) {
|
|
return;
|
|
}
|
|
// Decide whether a context needs to be created for [folder] here, and if
|
|
// so, create it.
|
|
File packageSpec = _findPackageSpecFile(folder);
|
|
bool createContext = packageSpec.exists || !withPackageSpecOnly;
|
|
if (withPackageSpecOnly &&
|
|
packageSpec.exists &&
|
|
parent != null &&
|
|
parent.ignored(packageSpec.path)) {
|
|
// Don't create a context if the package spec is required and ignored.
|
|
createContext = false;
|
|
}
|
|
if (createContext) {
|
|
parent = _createContext(parent, folder, excludedPaths, packageSpec);
|
|
}
|
|
|
|
// Try to find subfolders with pubspecs or .packages files.
|
|
try {
|
|
for (Resource child in folder.getChildren()) {
|
|
if (child is Folder) {
|
|
if (!parent.ignored(child.path)) {
|
|
_createContexts(parent, child, excludedPaths, true);
|
|
}
|
|
}
|
|
}
|
|
} on FileSystemException {
|
|
// The directory either doesn't exist or cannot be read. Either way, there
|
|
// are no subfolders that need to be added.
|
|
}
|
|
|
|
if (createContext) {
|
|
// Now that the child contexts have been created, add the sources that
|
|
// don't belong to the children.
|
|
ChangeSet changeSet = new ChangeSet();
|
|
_addSourceFiles(changeSet, folder, parent);
|
|
callbacks.applyChangesToContext(folder, changeSet);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set up a [SourceFactory] that resolves packages as appropriate for the
|
|
* given [folder].
|
|
*/
|
|
SourceFactory _createSourceFactory(AnalysisOptions options, Folder folder) {
|
|
ContextBuilder builder = callbacks.createContextBuilder(folder, options);
|
|
return builder.createSourceFactory(folder.path, options);
|
|
}
|
|
|
|
/**
|
|
* Clean up and destroy the context associated with the given folder.
|
|
*/
|
|
void _destroyContext(ContextInfo info) {
|
|
changeSubscriptions.remove(info.folder)?.cancel();
|
|
callbacks.removeContext(info.folder, _computeFlushedFiles(info));
|
|
bool wasRemoved = info.parent.children.remove(info);
|
|
assert(wasRemoved);
|
|
}
|
|
|
|
/**
|
|
* Extract a new [packagespecFile]-based context from [oldInfo].
|
|
*/
|
|
void _extractContext(ContextInfo oldInfo, File packagespecFile) {
|
|
Folder newFolder = packagespecFile.parent;
|
|
ContextInfo newInfo =
|
|
_createContext(oldInfo, newFolder, excludedPaths, packagespecFile);
|
|
// prepare sources to extract
|
|
Map<String, Source> extractedSources = new HashMap<String, Source>();
|
|
oldInfo.sources.forEach((path, source) {
|
|
if (newFolder.contains(path)) {
|
|
extractedSources[path] = source;
|
|
}
|
|
});
|
|
// update new context
|
|
{
|
|
ChangeSet changeSet = new ChangeSet();
|
|
extractedSources.forEach((path, source) {
|
|
newInfo.sources[path] = source;
|
|
changeSet.addedSource(source);
|
|
});
|
|
callbacks.applyChangesToContext(newFolder, changeSet);
|
|
}
|
|
// update old context
|
|
{
|
|
ChangeSet changeSet = new ChangeSet();
|
|
extractedSources.forEach((path, source) {
|
|
oldInfo.sources.remove(path);
|
|
changeSet.removedSource(source);
|
|
});
|
|
callbacks.applyChangesToContext(oldInfo.folder, changeSet);
|
|
}
|
|
// TODO(paulberry): every context that was previously a child of oldInfo is
|
|
// is still a child of oldInfo. This is wrong--some of them ought to be
|
|
// adopted by newInfo now.
|
|
}
|
|
|
|
/**
|
|
* Find the file that should be used to determine whether a context needs to
|
|
* be created here--this is either the ".packages" file or the "pubspec.yaml"
|
|
* file.
|
|
*/
|
|
File _findPackageSpecFile(Folder folder) {
|
|
// Decide whether a context needs to be created for [folder] here, and if
|
|
// so, create it.
|
|
File packageSpec;
|
|
|
|
// Start by looking for .packages.
|
|
packageSpec = folder.getChild(PACKAGE_SPEC_NAME);
|
|
|
|
// Fall back to looking for a pubspec.
|
|
if (packageSpec == null || !packageSpec.exists) {
|
|
packageSpec = folder.getChild(PUBSPEC_NAME);
|
|
}
|
|
return packageSpec;
|
|
}
|
|
|
|
/// Get analysis options inherited from an `_embedder.yaml` (deprecated)
|
|
/// and/or a package specified configuration. If more than one
|
|
/// `_embedder.yaml` is associated with the given context, the embedder is
|
|
/// skipped.
|
|
///
|
|
/// Returns null if there are no embedded/configured options.
|
|
YamlMap _getEmbeddedOptions(ContextInfo info) {
|
|
Map embeddedOptions = null;
|
|
EmbedderYamlLocator locator =
|
|
info.disposition.getEmbedderLocator(resourceProvider);
|
|
Iterable<YamlMap> maps = locator.embedderYamls.values;
|
|
if (maps.length == 1) {
|
|
embeddedOptions = maps.first;
|
|
}
|
|
return embeddedOptions;
|
|
}
|
|
|
|
/**
|
|
* Return the [ContextInfo] for the "innermost" context whose associated
|
|
* folder is or contains the given path. ("innermost" refers to the nesting
|
|
* of contexts, so if there is a context for path /foo and a context for
|
|
* path /foo/bar, then the innermost context containing /foo/bar/baz.dart is
|
|
* the context for /foo/bar.)
|
|
*
|
|
* If no context contains the given path, `null` is returned.
|
|
*/
|
|
ContextInfo _getInnermostContextInfoFor(String path) {
|
|
ContextInfo info = rootInfo.findChildInfoFor(path);
|
|
if (info == null) {
|
|
return null;
|
|
}
|
|
while (true) {
|
|
ContextInfo childInfo = info.findChildInfoFor(path);
|
|
if (childInfo == null) {
|
|
return info;
|
|
}
|
|
info = childInfo;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the parent for a new [ContextInfo] with the given [path] folder.
|
|
*/
|
|
ContextInfo _getParentForNewContext(String path) {
|
|
ContextInfo parent = _getInnermostContextInfoFor(path);
|
|
if (parent != null) {
|
|
return parent;
|
|
}
|
|
return rootInfo;
|
|
}
|
|
|
|
void _handleWatchEvent(WatchEvent event) {
|
|
callbacks.broadcastWatchEvent(event);
|
|
_handleWatchEventImpl(event);
|
|
callbacks.afterWatchEvent(event);
|
|
}
|
|
|
|
void _handleWatchEventImpl(WatchEvent event) {
|
|
// Figure out which context this event applies to.
|
|
// TODO(brianwilkerson) If a file is explicitly included in one context
|
|
// but implicitly referenced in another context, we will only send a
|
|
// changeSet to the context that explicitly includes the file (because
|
|
// that's the only context that's watching the file).
|
|
String path = event.path;
|
|
ChangeType type = event.type;
|
|
ContextInfo info = _getInnermostContextInfoFor(path);
|
|
if (info == null) {
|
|
// This event doesn't apply to any context. This could happen due to a
|
|
// race condition (e.g. a context was removed while one of its events was
|
|
// in the event loop). The event is inapplicable now, so just ignore it.
|
|
return;
|
|
}
|
|
_instrumentationService.logWatchEvent(
|
|
info.folder.path, path, type.toString());
|
|
// First handle changes that affect folderDisposition (since these need to
|
|
// be processed regardless of whether they are part of an excluded/ignored
|
|
// path).
|
|
if (info.hasDependency(path)) {
|
|
_recomputeFolderDisposition(info);
|
|
}
|
|
// maybe excluded globally
|
|
if (_isExcluded(path) ||
|
|
_isContainedInDotFolder(info.folder.path, path) ||
|
|
_isInTopLevelDocDir(info.folder.path, path)) {
|
|
return;
|
|
}
|
|
// maybe excluded from the context, so other context will handle it
|
|
if (info.excludes(path)) {
|
|
return;
|
|
}
|
|
if (info.ignored(path)) {
|
|
return;
|
|
}
|
|
// handle the change
|
|
switch (type) {
|
|
case ChangeType.ADD:
|
|
Resource resource = resourceProvider.getResource(path);
|
|
|
|
String directoryPath = pathContext.dirname(path);
|
|
|
|
// Check to see if we need to create a new context.
|
|
if (info.isTopLevel) {
|
|
// Only create a new context if this is not the same directory
|
|
// described by our info object.
|
|
if (info.folder.path != directoryPath) {
|
|
if (_isPubspec(path)) {
|
|
// Check for a sibling .packages file.
|
|
if (!resourceProvider
|
|
.getFile(pathContext.join(directoryPath, PACKAGE_SPEC_NAME))
|
|
.exists) {
|
|
_extractContext(info, resource);
|
|
return;
|
|
}
|
|
}
|
|
if (_isPackagespec(path)) {
|
|
// Check for a sibling pubspec.yaml file.
|
|
if (!resourceProvider
|
|
.getFile(pathContext.join(directoryPath, PUBSPEC_NAME))
|
|
.exists) {
|
|
_extractContext(info, resource);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the file went away and was replaced by a folder before we
|
|
// had a chance to process the event, resource might be a Folder. In
|
|
// that case don't add it.
|
|
if (resource is File) {
|
|
if (_shouldFileBeAnalyzed(resource)) {
|
|
info.analysisDriver.addFile(path);
|
|
}
|
|
}
|
|
break;
|
|
case ChangeType.REMOVE:
|
|
// If package spec info is removed, check to see if we can merge contexts.
|
|
// Note that it's important to verify that there is NEITHER a .packages nor a
|
|
// lingering pubspec.yaml before merging.
|
|
if (!info.isTopLevel) {
|
|
String directoryPath = pathContext.dirname(path);
|
|
|
|
// Only merge if this is the same directory described by our info object.
|
|
if (info.folder.path == directoryPath) {
|
|
if (_isPubspec(path)) {
|
|
// Check for a sibling .packages file.
|
|
if (!resourceProvider
|
|
.getFile(pathContext.join(directoryPath, PACKAGE_SPEC_NAME))
|
|
.exists) {
|
|
_mergeContext(info);
|
|
return;
|
|
}
|
|
}
|
|
if (_isPackagespec(path)) {
|
|
// Check for a sibling pubspec.yaml file.
|
|
if (!resourceProvider
|
|
.getFile(pathContext.join(directoryPath, PUBSPEC_NAME))
|
|
.exists) {
|
|
_mergeContext(info);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
callbacks.applyFileRemoved(info.analysisDriver, path);
|
|
break;
|
|
case ChangeType.MODIFY:
|
|
Resource resource = resourceProvider.getResource(path);
|
|
if (resource is File) {
|
|
if (_shouldFileBeAnalyzed(resource)) {
|
|
for (AnalysisDriver driver in driverMap.values) {
|
|
driver.changeFile(path);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
_checkForPackagespecUpdate(path, info, info.folder);
|
|
_checkForAnalysisOptionsUpdate(path, info, type);
|
|
_checkForPubspecUpdate(path, info, type);
|
|
}
|
|
|
|
/**
|
|
* Determine whether the given [path], when interpreted relative to the
|
|
* context root [root], contains a folder whose name starts with '.'.
|
|
*/
|
|
bool _isContainedInDotFolder(String root, String path) {
|
|
String pathDir = pathContext.dirname(path);
|
|
String rootPrefix = root + pathContext.separator;
|
|
if (pathDir.startsWith(rootPrefix)) {
|
|
String suffixPath = pathDir.substring(rootPrefix.length);
|
|
for (String pathComponent in pathContext.split(suffixPath)) {
|
|
if (pathComponent.startsWith('.')) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if the given [path] is excluded by [excludedPaths].
|
|
*/
|
|
bool _isExcluded(String path) => _isExcludedBy(excludedPaths, path);
|
|
|
|
/**
|
|
* Returns `true` if the given [path] is excluded by [excludedPaths].
|
|
*/
|
|
bool _isExcludedBy(List<String> excludedPaths, String path) {
|
|
return excludedPaths.any((excludedPath) {
|
|
if (pathContext.isWithin(excludedPath, path)) {
|
|
return true;
|
|
}
|
|
return path == excludedPath;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Determine whether the given [path] is in the direct 'doc' folder of the
|
|
* context root [root].
|
|
*/
|
|
bool _isInTopLevelDocDir(String root, String path) {
|
|
String rootPrefix = root + pathContext.separator;
|
|
if (path.startsWith(rootPrefix)) {
|
|
String suffix = path.substring(rootPrefix.length);
|
|
return suffix == DOC_DIR_NAME ||
|
|
suffix.startsWith(DOC_DIR_NAME + pathContext.separator);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool _isPackagespec(String path) =>
|
|
pathContext.basename(path) == PACKAGE_SPEC_NAME;
|
|
|
|
bool _isPubspec(String path) => pathContext.basename(path) == PUBSPEC_NAME;
|
|
|
|
/**
|
|
* Merges [info] context into its parent.
|
|
*/
|
|
void _mergeContext(ContextInfo info) {
|
|
// destroy the context
|
|
_destroyContext(info);
|
|
// add files to the parent context
|
|
ContextInfo parentInfo = info.parent;
|
|
if (parentInfo != null) {
|
|
parentInfo.children.remove(info);
|
|
ChangeSet changeSet = new ChangeSet();
|
|
info.sources.forEach((path, source) {
|
|
parentInfo.sources[path] = source;
|
|
changeSet.addedSource(source);
|
|
});
|
|
callbacks.applyChangesToContext(parentInfo.folder, changeSet);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read the contents of the file at the given [path], or throw an exception if
|
|
* the contents cannot be read.
|
|
*/
|
|
String _readFile(String path) {
|
|
return fileContentOverlay[path] ??
|
|
resourceProvider.getFile(path).readAsStringSync();
|
|
}
|
|
|
|
Packages _readPackagespec(File specFile) {
|
|
try {
|
|
String contents = specFile.readAsStringSync();
|
|
Map<String, Uri> map =
|
|
pkgfile.parse(utf8.encode(contents), new Uri.file(specFile.path));
|
|
return new MapPackages(map);
|
|
} catch (_) {
|
|
//TODO(pquitslund): consider creating an error for the spec file.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recompute the [FolderDisposition] for the context described by [info],
|
|
* and update the client appropriately.
|
|
*/
|
|
void _recomputeFolderDisposition(ContextInfo info) {
|
|
// TODO(paulberry): when computePackageMap is changed into an
|
|
// asynchronous API call, we'll want to suspend analysis for this context
|
|
// while we're rerunning "pub list", since any analysis we complete while
|
|
// "pub list" is in progress is just going to get thrown away anyhow.
|
|
List<String> dependencies = <String>[];
|
|
info.setDependencies(dependencies);
|
|
_updateContextPackageUriResolver(info.folder);
|
|
}
|
|
|
|
/**
|
|
* Return `true` if the given [file] should be analyzed.
|
|
*/
|
|
bool _shouldFileBeAnalyzed(File file) {
|
|
for (Glob glob in analyzedFilesGlobs) {
|
|
if (glob.matches(file.path)) {
|
|
// 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;
|
|
}
|
|
|
|
void _updateContextPackageUriResolver(Folder contextFolder) {
|
|
ContextInfo info = getContextInfoFor(contextFolder);
|
|
AnalysisDriver driver = info.analysisDriver;
|
|
SourceFactory sourceFactory =
|
|
_createSourceFactory(driver.analysisOptions, contextFolder);
|
|
driver.configure(sourceFactory: sourceFactory);
|
|
}
|
|
|
|
/**
|
|
* Create and return a source representing the given [file] within the given
|
|
* [driver].
|
|
*/
|
|
static Source createSourceInContext(AnalysisDriver driver, 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 (driver == null) {
|
|
return source;
|
|
}
|
|
Uri uri = driver.sourceFactory.restoreUri(source);
|
|
return file.createSource(uri);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Concrete [FolderDisposition] object indicating that the context for a given
|
|
* folder should resolve package URIs using a custom URI resolver.
|
|
*/
|
|
class CustomPackageResolverDisposition extends FolderDisposition {
|
|
/**
|
|
* The [UriResolver] that should be used to resolve package URIs.
|
|
*/
|
|
UriResolver resolver;
|
|
|
|
CustomPackageResolverDisposition(this.resolver);
|
|
|
|
@override
|
|
String get packageRoot => null;
|
|
|
|
@override
|
|
Packages get packages => null;
|
|
|
|
@override
|
|
Iterable<UriResolver> createPackageUriResolvers(
|
|
ResourceProvider resourceProvider) =>
|
|
<UriResolver>[resolver];
|
|
|
|
@override
|
|
EmbedderYamlLocator getEmbedderLocator(ResourceProvider resourceProvider) =>
|
|
new EmbedderYamlLocator(null);
|
|
|
|
@override
|
|
SdkExtensionFinder getSdkExtensionFinder(ResourceProvider resourceProvider) =>
|
|
new SdkExtensionFinder(null);
|
|
}
|
|
|
|
/**
|
|
* An instance of the class [FolderDisposition] represents the information
|
|
* gathered by the [ContextManagerImpl] to determine how to create an analysis
|
|
* driver for a given folder.
|
|
*
|
|
* Note: [ContextManagerImpl] may use equality testing and hash codes to
|
|
* determine when two folders should share the same context, so derived classes
|
|
* may need to override operator== and hashCode() if object identity is
|
|
* insufficient.
|
|
*
|
|
* TODO(paulberry): consider adding a flag to indicate that it is not necessary
|
|
* to recurse into the given folder looking for additional contexts to create
|
|
* or files to analyze (this could help avoid unnecessarily weighing down the
|
|
* system with file watchers).
|
|
*/
|
|
abstract class FolderDisposition {
|
|
/**
|
|
* If this [FolderDisposition] was created based on a package root
|
|
* folder, the absolute path to that folder. Otherwise `null`.
|
|
*/
|
|
String get packageRoot;
|
|
|
|
/**
|
|
* If contexts governed by this [FolderDisposition] should resolve packages
|
|
* using the ".packages" file mechanism (DEP 5), retrieve the [Packages]
|
|
* object that resulted from parsing the ".packages" file.
|
|
*/
|
|
Packages get packages;
|
|
|
|
/**
|
|
* Create all the [UriResolver]s which should be used to resolve packages in
|
|
* contexts governed by this [FolderDisposition].
|
|
*
|
|
* [resourceProvider] is provided since it is needed to construct most
|
|
* [UriResolver]s.
|
|
*/
|
|
Iterable<UriResolver> createPackageUriResolvers(
|
|
ResourceProvider resourceProvider);
|
|
|
|
/**
|
|
* Return the locator used to locate the _embedder.yaml file used to configure
|
|
* the SDK. The [resourceProvider] is used to access the file system in cases
|
|
* where that is necessary.
|
|
*/
|
|
EmbedderYamlLocator getEmbedderLocator(ResourceProvider resourceProvider);
|
|
|
|
/**
|
|
* Return the extension finder used to locate the `_sdkext` file used to add
|
|
* extensions to the SDK. The [resourceProvider] is used to access the file
|
|
* system in cases where that is necessary.
|
|
*/
|
|
SdkExtensionFinder getSdkExtensionFinder(ResourceProvider resourceProvider);
|
|
}
|
|
|
|
/**
|
|
* Concrete [FolderDisposition] object indicating that the context for a given
|
|
* folder should not resolve "package:" URIs at all.
|
|
*/
|
|
class NoPackageFolderDisposition extends FolderDisposition {
|
|
@override
|
|
final String packageRoot;
|
|
|
|
NoPackageFolderDisposition({this.packageRoot});
|
|
|
|
@override
|
|
Packages get packages => null;
|
|
|
|
@override
|
|
Iterable<UriResolver> createPackageUriResolvers(
|
|
ResourceProvider resourceProvider) =>
|
|
const <UriResolver>[];
|
|
|
|
@override
|
|
EmbedderYamlLocator getEmbedderLocator(ResourceProvider resourceProvider) =>
|
|
new EmbedderYamlLocator(null);
|
|
|
|
@override
|
|
SdkExtensionFinder getSdkExtensionFinder(ResourceProvider resourceProvider) =>
|
|
new SdkExtensionFinder(null);
|
|
}
|
|
|
|
/**
|
|
* Concrete [FolderDisposition] object indicating that the context for a given
|
|
* folder should resolve packages using a package map.
|
|
*/
|
|
class PackageMapDisposition extends FolderDisposition {
|
|
final Map<String, List<Folder>> packageMap;
|
|
|
|
EmbedderYamlLocator _embedderLocator;
|
|
SdkExtensionFinder _sdkExtensionFinder;
|
|
|
|
@override
|
|
final String packageRoot;
|
|
|
|
PackageMapDisposition(this.packageMap, {this.packageRoot});
|
|
|
|
@override
|
|
Packages get packages => null;
|
|
|
|
@override
|
|
Iterable<UriResolver> createPackageUriResolvers(
|
|
ResourceProvider resourceProvider) =>
|
|
<UriResolver>[
|
|
new SdkExtUriResolver(packageMap),
|
|
new PackageMapUriResolver(resourceProvider, packageMap)
|
|
];
|
|
|
|
@override
|
|
EmbedderYamlLocator getEmbedderLocator(ResourceProvider resourceProvider) {
|
|
if (_embedderLocator == null) {
|
|
_embedderLocator = new EmbedderYamlLocator(packageMap);
|
|
}
|
|
return _embedderLocator;
|
|
}
|
|
|
|
@override
|
|
SdkExtensionFinder getSdkExtensionFinder(ResourceProvider resourceProvider) {
|
|
return _sdkExtensionFinder ??= new SdkExtensionFinder(packageMap);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Concrete [FolderDisposition] object indicating that the context for a given
|
|
* folder should resolve packages using a ".packages" file.
|
|
*/
|
|
class PackagesFileDisposition extends FolderDisposition {
|
|
@override
|
|
final Packages packages;
|
|
|
|
Map<String, List<Folder>> packageMap;
|
|
|
|
EmbedderYamlLocator _embedderLocator;
|
|
SdkExtensionFinder _sdkExtensionFinder;
|
|
|
|
PackagesFileDisposition(this.packages);
|
|
|
|
@override
|
|
String get packageRoot => null;
|
|
|
|
Map<String, List<Folder>> buildPackageMap(ResourceProvider resourceProvider) {
|
|
if (packageMap == null) {
|
|
packageMap = <String, List<Folder>>{};
|
|
if (packages != null) {
|
|
var pathContext = resourceProvider.pathContext;
|
|
packages.asMap().forEach((String name, Uri uri) {
|
|
if (uri.scheme == 'file' || uri.scheme == '' /* unspecified */) {
|
|
String path = fileUriToNormalizedPath(pathContext, uri);
|
|
packageMap[name] = <Folder>[resourceProvider.getFolder(path)];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return packageMap;
|
|
}
|
|
|
|
@override
|
|
Iterable<UriResolver> createPackageUriResolvers(
|
|
ResourceProvider resourceProvider) {
|
|
if (packages != null) {
|
|
// Construct package map for the SdkExtUriResolver.
|
|
Map<String, List<Folder>> packageMap = buildPackageMap(resourceProvider);
|
|
return <UriResolver>[new SdkExtUriResolver(packageMap)];
|
|
} else {
|
|
return const <UriResolver>[];
|
|
}
|
|
}
|
|
|
|
@override
|
|
EmbedderYamlLocator getEmbedderLocator(ResourceProvider resourceProvider) {
|
|
if (_embedderLocator == null) {
|
|
_embedderLocator =
|
|
new EmbedderYamlLocator(buildPackageMap(resourceProvider));
|
|
}
|
|
return _embedderLocator;
|
|
}
|
|
|
|
@override
|
|
SdkExtensionFinder getSdkExtensionFinder(ResourceProvider resourceProvider) {
|
|
return _sdkExtensionFinder ??=
|
|
new SdkExtensionFinder(buildPackageMap(resourceProvider));
|
|
}
|
|
}
|