| // 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:core'; |
| |
| import 'package:analysis_server/src/plugin/notification_manager.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/context/packages.dart'; |
| import 'package:analyzer/src/dart/analysis/driver.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/sdk.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer/src/generated/source_io.dart'; |
| import 'package:analyzer/src/manifest/manifest_validator.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/task/options.dart'; |
| import 'package:analyzer/src/util/glob.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:path/path.dart' as pathos; |
| import 'package:watcher/watcher.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| /// An indication of which files have been added, changed, removed, or deleted. |
| /// |
| /// No file should be added to the change set more than once, either with the |
| /// same or a different kind of change. It does not make sense, for example, |
| /// for a file to be both added and removed. |
| class ChangeSet { |
| /// A list containing paths of added files. |
| final List<String> addedFiles = []; |
| |
| /// A list containing paths of changed files. |
| final List<String> changedFiles = []; |
| |
| /// A list containing paths of removed files. |
| final List<String> removedFiles = []; |
| |
| /// Return `true` if this change set does not contain any changes. |
| bool get isEmpty => |
| addedFiles.isEmpty && changedFiles.isEmpty && removedFiles.isEmpty; |
| |
| /// Record that the file with the specified [path] has been added. |
| void addedSource(String path) { |
| addedFiles.add(path); |
| } |
| |
| /// Record that the file with the specified [path] has been changed. |
| void changedSource(String path) { |
| changedFiles.add(path); |
| } |
| |
| /// Record that the file with the specified [path] has been removed. |
| void removedSource(String path) { |
| removedFiles.add(path); |
| } |
| } |
| |
| /// 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 [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 = <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 = HashMap<String, Source>(); |
| |
| ContextInfo(ContextManagerImpl contextManager, this.parent, Folder folder, |
| File packagespecFile, this.disposition) |
| : folder = folder, |
| pathFilter = 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, |
| disposition = null; |
| |
| /// Iterate through all [children] and their children, recursively. |
| Iterable<ContextInfo> get descendants sync* { |
| for (var 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 (var 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 (var child in children) { |
| if (child._managesOrHasChildThatManages(path)) { |
| return true; |
| } |
| } |
| return false; |
| } else { |
| if (!folder.isOrContains(path)) { |
| return false; |
| } |
| for (var 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. |
| 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); |
| |
| /// Determine whether the given [path], when interpreted relative to innermost |
| /// context root, contains a folder whose name starts with '.'. |
| bool isContainedInDotFolder(String path); |
| |
| /// 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); |
| } |
| |
| /// 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. |
| AbstractNotificationManager 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 analysis options or URI resolution in the [driver] are |
| /// changed. |
| void analysisOptionsUpdated(AnalysisDriver driver); |
| |
| /// 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 Android manifest files. |
| static const String MANIFEST_NAME = 'AndroidManifest.xml'; |
| |
| /// 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 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]. |
| @override |
| List<String> excludedPaths = <String>[]; |
| |
| /// The list of included paths (folders and files) most recently passed to |
| /// [setRoots]. |
| @override |
| List<String> includedPaths = <String>[]; |
| |
| /// 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 = ContextInfo._root(); |
| |
| @override |
| final Map<Folder, AnalysisDriver> driverMap = |
| 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.sdkManager, |
| 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; |
| |
| @override |
| Folder getContextFolderFor(String path) { |
| return _getInnermostContextInfoFor(path)?.folder; |
| } |
| |
| /// For testing: get the [ContextInfo] object for the given [folder], if any. |
| ContextInfo getContextInfoFor(Folder folder) { |
| var 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) { |
| var drivers = <AnalysisDriver>[]; |
| void addContextAndDescendants(ContextInfo info) { |
| drivers.add(info.analysisDriver); |
| info.children.forEach(addContextAndDescendants); |
| } |
| |
| var innermostContainingInfo = |
| _getInnermostContextInfoFor(analysisRoot.path); |
| if (innermostContainingInfo != null) { |
| if (analysisRoot == innermostContainingInfo.folder) { |
| addContextAndDescendants(innermostContainingInfo); |
| } else { |
| for (var info in innermostContainingInfo.children) { |
| if (analysisRoot.isOrContains(info.folder.path)) { |
| addContextAndDescendants(info); |
| } |
| } |
| } |
| } |
| return drivers; |
| } |
| |
| /// Determine whether the given [path], when interpreted relative to innermost |
| /// context root, contains a folder whose name starts with '.'. |
| @override |
| bool isContainedInDotFolder(String path) { |
| var info = _getInnermostContextInfoFor(path); |
| return info != null && _isContainedInDotFolder(info.folder.path, path); |
| } |
| |
| @override |
| bool isIgnored(String path) { |
| var 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 (var info in rootInfo.children) { |
| if (info.folder.contains(path)) { |
| return true; |
| } |
| } |
| // no |
| return false; |
| } |
| |
| @override |
| int numberOfContextsInAnalysisRoot(Folder analysisRoot) { |
| var count = 0; |
| void addContextAndDescendants(ContextInfo info) { |
| count++; |
| info.children.forEach(addContextAndDescendants); |
| } |
| |
| var innermostContainingInfo = |
| _getInnermostContextInfoFor(analysisRoot.path); |
| if (innermostContainingInfo != null) { |
| if (analysisRoot == innermostContainingInfo.folder) { |
| addContextAndDescendants(innermostContainingInfo); |
| } else { |
| for (var 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. |
| var embeddedOptions = _getEmbeddedOptions(info); |
| if (embeddedOptions != null) { |
| options = 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 |
| var 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); |
| } |
| |
| /// 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) { |
| var contextInfos = rootInfo.descendants.toList(); |
| // included |
| var 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. |
| var uniqueIncludedPaths = LinkedHashSet<String>.from(includedPaths); |
| var sortedIncludedPaths = uniqueIncludedPaths.toList(); |
| sortedIncludedPaths.sort((a, b) => a.length - b.length); |
| // Convert paths to folders. |
| for (var path in sortedIncludedPaths) { |
| var 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 UnimplementedError('$path is not a folder. ' |
| 'Only support for folder analysis is implemented currently.'); |
| } |
| } |
| } |
| this.includedPaths = includedPaths; |
| // excluded |
| var oldExcludedPaths = this.excludedPaths; |
| this.excludedPaths = excludedPaths; |
| // destroy old contexts |
| for (var contextInfo in contextInfos) { |
| var isIncluded = includedFolders.any((folder) { |
| return folder.isOrContains(contextInfo.folder.path); |
| }); |
| if (!isIncluded) { |
| _destroyContext(contextInfo); |
| } |
| } |
| // create new contexts |
| for (var includedFolder in includedFolders) { |
| var includedPath = includedFolder.path; |
| var isManaged = rootInfo._managesOrHasChildThatManages(includedPath); |
| if (!isManaged) { |
| var parent = _getParentForNewContext(includedPath); |
| changeSubscriptions[includedFolder] = includedFolder.changes |
| .listen(_handleWatchEvent, onError: _handleWatchInterruption); |
| _createContexts(parent, includedFolder, excludedPaths, false); |
| } |
| } |
| // remove newly excluded sources |
| for (var info in rootInfo.descendants) { |
| // prepare excluded sources |
| Map<String, Source> excludedSources = HashMap<String, Source>(); |
| info.sources.forEach((String path, Source source) { |
| if (_isExcludedBy(excludedPaths, path) && |
| !_isExcludedBy(oldExcludedPaths, path)) { |
| excludedSources[path] = source; |
| } |
| }); |
| // apply exclusion |
| var changeSet = ChangeSet(); |
| excludedSources.forEach((String path, Source source) { |
| info.sources.remove(path); |
| changeSet.removedSource(path); |
| }); |
| callbacks.applyChangesToContext(info.folder, changeSet); |
| } |
| // add previously excluded sources |
| for (var info in rootInfo.descendants) { |
| var changeSet = 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 (var child in children) { |
| var 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 |
| var wasExcluded = _isExcludedBy(oldExcludedPaths, path) && |
| !_isExcludedBy(excludedPaths, path); |
| if (!wasExcluded) { |
| continue; |
| } |
| // do add the file |
| var source = createSourceInContext(info.analysisDriver, child); |
| changeSet.addedSource(child.path); |
| 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; |
| 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 (var child in children) { |
| var 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)) { |
| var source = createSourceInContext(info.analysisDriver, child); |
| changeSet.addedSource(child.path); |
| 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 { |
| var content = _readFile(path); |
| var lineInfo = _computeLineInfo(content); |
| var errors = analyzeAnalysisOptions( |
| resourceProvider.getFile(path).createSource(), |
| content, |
| driver.sourceFactory); |
| var converter = 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 |
| /// AndroidManifest file at the given [path]. |
| void _analyzeManifestFile(AnalysisDriver driver, String path) { |
| List<protocol.AnalysisError> convertedErrors; |
| try { |
| var content = _readFile(path); |
| var validator = |
| ManifestValidator(resourceProvider.getFile(path).createSource()); |
| var lineInfo = _computeLineInfo(content); |
| var errors = validator.validate( |
| content, driver.analysisOptions.chromeOsManifestChecks); |
| var converter = 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 { |
| var content = _readFile(path); |
| var node = loadYamlNode(content); |
| if (node is YamlMap) { |
| var validator = PubspecValidator( |
| resourceProvider, resourceProvider.getFile(path).createSource()); |
| var lineInfo = _computeLineInfo(content); |
| var errors = validator.validate(node.nodes); |
| var converter = 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) { |
| if (AnalysisEngine.isAnalysisOptionsFileName(path, pathContext)) { |
| var 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; |
| } |
| // TODO(brianwilkerson) Set exclusion patterns. |
| _analyzeAnalysisOptionsFile(driver, path); |
| _updateAnalysisOptions(info); |
| } |
| } |
| |
| void _checkForManifestUpdate(String path, ContextInfo info) { |
| if (_isManifest(path)) { |
| var 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; |
| } |
| _analyzeManifestFile(driver, path); |
| } |
| } |
| |
| void _checkForPackagespecUpdate(String path, ContextInfo info) { |
| // 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) { |
| var 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; |
| } |
| |
| _updateAnalysisOptions(info); |
| } |
| } |
| |
| void _checkForPubspecUpdate(String path, ContextInfo info) { |
| if (_isPubspec(path)) { |
| var 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); |
| _updateAnalysisOptions(info); |
| } |
| } |
| |
| /// 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) { |
| var flushedFiles = info.analysisDriver.addedFiles.toSet(); |
| for (var contextInfo in rootInfo.descendants) { |
| var 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 Function(String path) addDependency, File packagespecFile) { |
| // Try .packages first. |
| if (pathContext.basename(packagespecFile.path) == PACKAGE_SPEC_NAME) { |
| var packages = parsePackagesFile( |
| resourceProvider, |
| packagespecFile, |
| ); |
| return PackagesFileDisposition(packages); |
| } |
| |
| return NoPackageFolderDisposition(); |
| } |
| |
| /// Compute line information for the given [content]. |
| LineInfo _computeLineInfo(String content) { |
| var lineStarts = StringUtilities.computeLineStarts(content); |
| return 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) { |
| var packageMap = <String, List<Folder>>{}; |
| if (packages != null) { |
| for (var package in packages.packages) { |
| packageMap[package.name] = [package.libFolder]; |
| } |
| } |
| |
| var resolvers = <UriResolver>[ |
| ResourceUriResolver(resourceProvider), |
| PackageMapUriResolver(resourceProvider, packageMap), |
| ]; |
| var sourceFactory = SourceFactory(resolvers); |
| return 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) { |
| var dependencies = <String>[]; |
| var disposition = |
| _computeFolderDisposition(folder, dependencies.add, packagesFile); |
| var info = ContextInfo(this, parent, folder, packagesFile, disposition); |
| |
| File optionsFile; |
| YamlMap optionMap; |
| try { |
| var 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 = AnalysisOptionsImpl.from(defaultContextOptions); |
| applyToAnalysisOptions(options, optionMap); |
| |
| info.setDependencies(dependencies); |
| var includedPath = folder.path; |
| var containedExcludedPaths = excludedPaths |
| .where((String excludedPath) => |
| pathContext.isWithin(includedPath, excludedPath)) |
| .toList(); |
| processOptionsForDriver(info, options, optionMap); |
| var contextRoot = 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); |
| } |
| var pubspecFile = folder.getChildAssumingFile(PUBSPEC_NAME); |
| if (pubspecFile.exists) { |
| _analyzePubspecFile(info.analysisDriver, pubspecFile.path); |
| } |
| |
| void checkManifestFilesIn(Folder folder) { |
| // Don't traverse into dot directories. |
| if (folder.shortName.startsWith('.')) { |
| return; |
| } |
| |
| for (var child in folder.getChildren()) { |
| if (child is File) { |
| if (child.shortName == MANIFEST_NAME && |
| !excludedPaths.contains(child.path)) { |
| _analyzeManifestFile(info.analysisDriver, child.path); |
| } |
| } else if (child is Folder) { |
| if (!excludedPaths.contains(child.path)) { |
| checkManifestFilesIn(child); |
| } |
| } |
| } |
| } |
| |
| checkManifestFilesIn(folder); |
| 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. |
| var packageSpec = _findPackageSpecFile(folder); |
| var 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 (var 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. |
| var changeSet = 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) { |
| var 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)); |
| var wasRemoved = info.parent.children.remove(info); |
| assert(wasRemoved); |
| } |
| |
| /// Extract a new [packagespecFile]-based context from [oldInfo]. |
| void _extractContext(ContextInfo oldInfo, File packagespecFile) { |
| var newFolder = packagespecFile.parent; |
| var newInfo = |
| _createContext(oldInfo, newFolder, excludedPaths, packagespecFile); |
| // prepare sources to extract |
| Map<String, Source> extractedSources = HashMap<String, Source>(); |
| oldInfo.sources.forEach((path, source) { |
| if (newFolder.contains(path)) { |
| extractedSources[path] = source; |
| } |
| }); |
| // update new context |
| { |
| var changeSet = ChangeSet(); |
| extractedSources.forEach((path, source) { |
| newInfo.sources[path] = source; |
| changeSet.addedSource(path); |
| }); |
| callbacks.applyChangesToContext(newFolder, changeSet); |
| } |
| // update old context |
| { |
| var changeSet = ChangeSet(); |
| extractedSources.forEach((path, source) { |
| oldInfo.sources.remove(path); |
| changeSet.removedSource(path); |
| }); |
| 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; |
| var locator = info.disposition.getEmbedderLocator(resourceProvider); |
| var 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) { |
| var info = rootInfo.findChildInfoFor(path); |
| if (info == null) { |
| return null; |
| } |
| while (true) { |
| var 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) { |
| var 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). |
| var path = event.path; |
| var type = event.type; |
| var 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: |
| var resource = resourceProvider.getResource(path); |
| |
| var 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) { |
| var 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; |
| } |
| } |
| } |
| } |
| |
| var resource = resourceProvider.getResource(path); |
| if (resource is File && |
| _shouldFileBeAnalyzed(resource, mustExist: false)) { |
| callbacks.applyFileRemoved(info.analysisDriver, path); |
| } |
| break; |
| case ChangeType.MODIFY: |
| var resource = resourceProvider.getResource(path); |
| if (resource is File) { |
| if (_shouldFileBeAnalyzed(resource)) { |
| for (var driver in driverMap.values) { |
| driver.changeFile(path); |
| } |
| break; |
| } |
| } |
| } |
| _checkForPackagespecUpdate(path, info); |
| _checkForAnalysisOptionsUpdate(path, info); |
| _checkForPubspecUpdate(path, info); |
| _checkForManifestUpdate(path, info); |
| } |
| |
| /// On windows, the directory watcher may overflow, and we must recover. |
| void _handleWatchInterruption(dynamic error, StackTrace stackTrace) { |
| // We've handled the error, so we only have to log it. |
| AnalysisEngine.instance.instrumentationService |
| .logError('Watcher error; refreshing contexts.\n$error\n$stackTrace'); |
| // TODO(mfairhurst): Optimize this, or perhaps be less complete. |
| refresh(null); |
| } |
| |
| /// 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) { |
| var pathDir = pathContext.dirname(path); |
| var rootPrefix = root + pathContext.separator; |
| if (pathDir.startsWith(rootPrefix)) { |
| var suffixPath = pathDir.substring(rootPrefix.length); |
| for (var 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) { |
| var rootPrefix = root + pathContext.separator; |
| if (path.startsWith(rootPrefix)) { |
| var suffix = path.substring(rootPrefix.length); |
| return suffix == DOC_DIR_NAME || |
| suffix.startsWith(DOC_DIR_NAME + pathContext.separator); |
| } |
| return false; |
| } |
| |
| bool _isManifest(String path) => pathContext.basename(path) == MANIFEST_NAME; |
| |
| 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 |
| var parentInfo = info.parent; |
| if (parentInfo != null) { |
| parentInfo.children.remove(info); |
| var changeSet = ChangeSet(); |
| info.sources.forEach((path, source) { |
| parentInfo.sources[path] = source; |
| changeSet.addedSource(path); |
| }); |
| 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 resourceProvider.getFile(path).readAsStringSync(); |
| } |
| |
| /// 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. |
| var dependencies = <String>[]; |
| info.setDependencies(dependencies); |
| _updateContextPackageUriResolver(info.folder); |
| } |
| |
| /// Return `true` if the given [file] should be analyzed. |
| bool _shouldFileBeAnalyzed(File file, {bool mustExist = true}) { |
| for (var 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 !mustExist || file.exists; |
| } |
| } |
| return false; |
| } |
| |
| void _updateAnalysisOptions(ContextInfo info) { |
| var driver = info.analysisDriver; |
| var contextRoot = info.folder.path; |
| var builder = |
| callbacks.createContextBuilder(info.folder, defaultContextOptions); |
| var options = builder.getAnalysisOptions(contextRoot, |
| contextRoot: driver.contextRoot); |
| var factory = builder.createSourceFactory(contextRoot, options); |
| driver.configure(analysisOptions: options, sourceFactory: factory); |
| callbacks.analysisOptionsUpdated(driver); |
| } |
| |
| void _updateContextPackageUriResolver(Folder contextFolder) { |
| var info = getContextInfoFor(contextFolder); |
| var driver = info.analysisDriver; |
| var 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. |
| var source = file.createSource(); |
| if (driver == null) { |
| return source; |
| } |
| var uri = driver.sourceFactory.restoreUri(source); |
| return file.createSource(uri); |
| } |
| } |
| |
| /// 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); |
| } |
| |
| /// 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) => |
| EmbedderYamlLocator(null); |
| } |
| |
| /// 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; |
| |
| 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) { |
| for (var package in packages.packages) { |
| packageMap[package.name] = [package.libFolder]; |
| } |
| } |
| } |
| return packageMap; |
| } |
| |
| @override |
| Iterable<UriResolver> createPackageUriResolvers( |
| ResourceProvider resourceProvider) { |
| if (packages != null) { |
| var packageMap = buildPackageMap(resourceProvider); |
| return <UriResolver>[ |
| PackageMapUriResolver(resourceProvider, packageMap), |
| ]; |
| } else { |
| return const <UriResolver>[]; |
| } |
| } |
| |
| @override |
| EmbedderYamlLocator getEmbedderLocator(ResourceProvider resourceProvider) { |
| _embedderLocator ??= EmbedderYamlLocator(buildPackageMap(resourceProvider)); |
| return _embedderLocator; |
| } |
| } |