blob: bb6b1e9e47254d03f0582f2d574e9c691b3cc93f [file]
// Copyright (c) 2018, 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 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart'
show PhysicalResourceProvider;
import 'package:analyzer/src/analysis_options/analysis_options.dart';
import 'package:analyzer/src/analysis_options/analysis_options_parser.dart';
import 'package:analyzer/src/context/packages.dart';
import 'package:analyzer/src/dart/analysis/context_root.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/lint/pub.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/utilities/extensions/file_system.dart';
import 'package:analyzer/src/workspace/basic.dart';
import 'package:analyzer/src/workspace/blaze.dart';
import 'package:analyzer/src/workspace/gn.dart';
import 'package:analyzer/src/workspace/pub.dart';
import 'package:analyzer/src/workspace/workspace.dart';
import 'package:collection/collection.dart';
import 'package:glob/glob.dart';
import 'package:path/path.dart';
/// Returns a list of the context roots that should be used to analyze the
/// files that are included by the list of [includedPaths].
///
/// If an [optionsFile] is specified, then it is assumed to be the path to the
/// `analysis_options.yaml` file that should be used in place of the ones that
/// would be found by looking in the directories containing the context roots.
///
/// If a [packageConfigFile] is specified, then it is assumed to be the path
/// to the package config file that should be used in place of the one that
/// would be found by looking in the directories containing the context roots.
List<ContextRootImpl> locateContextRoots({
required List<String> includedPaths,
String? optionsFile,
String? packageConfigFile,
required ResourceProvider resourceProvider,
}) {
File? defaultOptionsFile;
if (optionsFile != null) {
defaultOptionsFile = resourceProvider.getFile(optionsFile);
if (!defaultOptionsFile.exists) {
defaultOptionsFile = null;
}
}
File? defaultPackageConfigFile;
if (packageConfigFile != null) {
defaultPackageConfigFile = resourceProvider.getFile(packageConfigFile);
if (!defaultPackageConfigFile.exists) {
defaultPackageConfigFile = null;
}
}
var (includedFolders, includedFiles) = _resourcesFromPaths(
includedPaths,
resourceProvider,
);
return _ContextLocator(
resourceProvider: resourceProvider,
defaultOptionsFile: defaultOptionsFile,
defaultPackageConfigFile: defaultPackageConfigFile,
)._locateRoots(
includedFolders: includedFolders,
includedFiles: includedFiles,
);
}
/// Returns a list of [Folder]s and a list of [File]s containing all of the
/// resources in the given list of [paths] that exist and are not contained
/// within one of the folders.
(List<Folder>, List<File>) _resourcesFromPaths(
List<String> paths,
ResourceProvider resourceProvider,
) {
List<Folder> folders = <Folder>[];
List<File> files = <File>[];
for (String path in _uniqueSortedPaths(paths)) {
Resource resource = resourceProvider.getResource(path);
if (resource is Folder) {
folders.add(resource);
} else if (resource is File) {
files.add(resource);
} else {
// Internal error: unhandled kind of resource.
}
}
return (folders, files);
}
/// Returns a list of paths that contains all of the unique elements from the
/// given list of [paths], sorted such that shorter paths are first.
List<String> _uniqueSortedPaths(List<String> paths) {
Set<String> uniquePaths = Set<String>.from(paths);
List<String> sortedPaths = uniquePaths.toList();
sortedPaths.sort((a, b) => a.length - b.length);
return sortedPaths;
}
/// Determines the list of analysis contexts that can be used to analyze the
/// files and folders that should be analyzed given a list of included files and
/// folders and a list of excluded files and folders.
class _ContextLocator {
/// The resource provider used to access the file system.
final ResourceProvider _resourceProvider;
final File? _defaultOptionsFile;
final File? _defaultPackageConfigFile;
/// A parse session for one context-location operation.
final AnalysisOptionsParseSession _analysisOptionsParseSession = .new();
/// Cache of enabled legacy plugin names by source factory and options file.
///
/// Plugin names depend on the source factory because package includes can
/// resolve differently for the same options file in different workspaces.
final Map<(SourceFactory, File), Set<String>> _legacyPlugins = {};
/// Parsed package configurations reused by this operation.
final Map<File, Packages> _packagesByFile = {};
/// Package-config workspace snapshots reused by this operation.
late final PackageConfigWorkspaceFindSession
_packageConfigWorkspaceFindSession = .new(_resourceProvider);
/// The list of context roots ultimately returned by [_locateRoots].
final _roots = <ContextRootImpl>[];
/// Initialize a newly created context locator. If a [resourceProvider] is
/// supplied, it will be used to access the file system. Otherwise the default
/// resource provider will be used.
_ContextLocator({
required ResourceProvider? resourceProvider,
required File? defaultOptionsFile,
required File? defaultPackageConfigFile,
}) : _resourceProvider =
resourceProvider ?? PhysicalResourceProvider.INSTANCE,
_defaultOptionsFile = defaultOptionsFile,
_defaultPackageConfigFile = defaultPackageConfigFile;
/// Adds individually included files to contexts. Unlike an options file,
/// workspace and package configuration affect how every library reachable
/// from a file is resolved. Files that share those driver-wide inputs can
/// therefore use one context and select their path-local options through the
/// context's options map.
void _addIncludedFiles(List<File> includedFiles) {
var rootsByFolder = <Folder, ContextRootImpl>{};
for (var file in includedFiles) {
var folder = file.parent;
var root = rootsByFolder[folder] ??= _rootForFolder(folder);
if (!root.isAnalyzed(file.path)) {
root.included.add(file);
}
}
}
/// Adds the path-local configuration represented by [optionsFile] to
/// [root]. Options outside the root apply at the root; options inside it
/// apply only below their own directory.
void _addOptionsMapping(
ContextRootImpl root,
File? optionsFile, {
Folder? mappingFolder,
}) {
if (optionsFile == null) return;
var optionsFolder = optionsFile.parent;
mappingFolder ??= root.root.isOrContains(optionsFolder.path)
? optionsFolder
: root.root;
var mappingFolders = root.optionsFileMap.putIfAbsent(optionsFile, () => {});
if (mappingFolders.add(mappingFolder)) {
root.excludedGlobs.addAll(
_getExcludedGlobs(optionsFile, root.workspace.partialSourceFactory),
);
}
}
/// Returns the location of a context root for a file in the [parent].
///
/// If [_defaultOptionsFile] is non-`null`, it will be used, not a file found
/// relative to the [parent].
///
/// If [_defaultPackageConfigFile] is non-`null`, it will be used, not a file
/// found relative to the [parent].
///
/// The root folder of the context is the parent of either the options, or the
/// grand-parent of the `.dart_tool/package_config.json` file, whichever is
/// lower.
_RootLocation _contextRootLocation(
Folder parent, {
required Folder Function() defaultRootFolder,
}) {
File? optionsFile;
Folder? optionsFolderToChooseRoot;
if (_defaultOptionsFile != null) {
optionsFile = _defaultOptionsFile;
} else {
optionsFile = parent.findAnalysisOptionsYamlFile();
optionsFolderToChooseRoot = optionsFile?.parent;
}
File? packageConfigFile;
Folder? packagesFolderToChooseRoot;
if (_defaultPackageConfigFile != null) {
packageConfigFile = _defaultPackageConfigFile;
// If the packages file is in .dart_tool directory, use the grandparent
// folder, else use the parent folder.
packagesFolderToChooseRoot =
_findFolderWithPackageConfigFile(packageConfigFile.parent) ??
packageConfigFile.parent;
}
var buildGnFile = _findBuildGnFile(parent);
// If a default package config file is given, create workspace for it.
var workspace = _createWorkspace(
folder: parent,
packageConfigFile: packageConfigFile,
buildGnFile: buildGnFile,
);
var resolutionRootFolder = _lowest([
buildGnFile?.parent,
if (workspace is! BasicWorkspace)
_resourceProvider.getFolder(workspace.root),
]);
var contextRootFolder = _lowest([
optionsFolderToChooseRoot,
resolutionRootFolder,
]);
if (workspace is PackageConfigWorkspace) {
packageConfigFile ??= workspace.packageConfigFile;
// If the default packages folder is a parent of the workspace root,
// choose that as the context root.
if (contextRootFolder != null && packagesFolderToChooseRoot != null) {
if (packagesFolderToChooseRoot.contains(contextRootFolder.path)) {
contextRootFolder = packagesFolderToChooseRoot;
}
}
// A default package config applies to every included file, so if its
// folder is above the workspace root, use that folder as the resolution
// root.
if (resolutionRootFolder != null &&
packagesFolderToChooseRoot != null &&
packagesFolderToChooseRoot.contains(resolutionRootFolder.path)) {
resolutionRootFolder = packagesFolderToChooseRoot;
}
}
if (contextRootFolder == null) {
contextRootFolder = defaultRootFolder();
if (workspace is BasicWorkspace) {
workspace = _createWorkspace(
folder: contextRootFolder,
packageConfigFile: packageConfigFile,
buildGnFile: buildGnFile,
);
}
}
// If no resolution root was found, this is a basic workspace with no
// marker-defined boundary, so retain the path-local root.
if (resolutionRootFolder == null) {
assert(workspace is BasicWorkspace);
resolutionRootFolder = contextRootFolder;
}
return _RootLocation(
contextRootFolder: contextRootFolder,
resolutionRootFolder: resolutionRootFolder,
workspace: workspace,
optionsFile: optionsFile,
packageConfigFile: packageConfigFile,
);
}
ContextRootImpl _createContextRoot({
required Folder rootFolder,
required File? optionsFile,
required _RootLocation location,
bool useWorkspaceDefaultOptions = true,
}) {
if (optionsFile == null &&
useWorkspaceDefaultOptions &&
location.workspace is WorkspaceWithDefaultAnalysisOptions) {
optionsFile = _findDefaultOptionsFile(
location.workspace.partialSourceFactory,
);
}
var root = ContextRootImpl(
_resourceProvider,
rootFolder,
location.workspace,
optionsFile: optionsFile,
packagesFile: location.packageConfigFile,
);
_addOptionsMapping(root, optionsFile, mappingFolder: rootFolder);
_roots.add(root);
return root;
}
/// If the given [folder] should be the root of a new analysis context, then
/// creates a new context root for it and add it to the list of context
/// [_roots]. The [containingRoot] is the context root from an enclosing
/// directory and is used to inherit configuration information that isn't
/// overridden.
///
/// If either the [_defaultOptionsFile] or [_defaultPackageConfigFile] is
/// non-`null`, then the given file is used even if there is a local version
/// of the file.
///
/// For each directory within the given [folder] that isn't excluded by the
/// `containingRoot.excludedGlobs`, recursively searches for nested context
/// roots.
///
/// Returns true if the folder was contained in the root and did not create a
/// new root, false if it did create a new root.
bool _createContextRoots(
Set<String> visited,
Folder folder,
ContextRootImpl containingRoot,
Set<String> containingRootEnabledLegacyPlugins, {
File? optionsFileFromParentInSameRoot,
}) {
var packageConfigFileToUse =
_defaultPackageConfigFile ??
_getPackageConfigFile(folder) ??
containingRoot.packagesFile;
var buildGnFile = folder.getExistingFile(file_paths.buildGn);
var optionsFileToUse = _defaultOptionsFile;
if (optionsFileToUse == null) {
optionsFileToUse = folder.existingAnalysisOptionsYamlFile;
// If this folder doesn't have one use the one from a parent folder if any,
// that will be the one we find anyway.
optionsFileToUse ??= optionsFileFromParentInSameRoot;
if (optionsFileToUse == null) {
var parentFolder = folder.parent;
while (parentFolder != containingRoot.root) {
optionsFileToUse = parentFolder.existingAnalysisOptionsYamlFile;
if (optionsFileToUse != null) {
break;
}
parentFolder = parentFolder.parent;
}
}
}
var localEnabledPlugins = _getEnabledLegacyPlugins(
containingRoot.workspace,
optionsFileToUse,
);
// Legacy plugins differ only if there is an analysis_options and it
// contains a different set of plugins from the containing context.
var pluginsDiffer =
optionsFileToUse != null &&
!const SetEquality<String>().equals(
containingRootEnabledLegacyPlugins,
localEnabledPlugins,
);
bool usedThisRoot = true;
// Create a context root for the given [folder] if a packages or build file
// is locally specified, or the set of enabled legacy plugins changed.
if (pluginsDiffer ||
packageConfigFileToUse != containingRoot.packagesFile ||
buildGnFile != null) {
var workspace = _createWorkspace(
folder: folder,
packageConfigFile: packageConfigFileToUse,
buildGnFile: buildGnFile,
);
var root = ContextRootImpl(
_resourceProvider,
folder,
workspace,
optionsFile: optionsFileToUse ?? containingRoot.optionsFile,
packagesFile: packageConfigFileToUse,
);
root.included.add(folder);
containingRoot.excluded.add(folder);
_roots.add(root);
containingRoot = root;
containingRootEnabledLegacyPlugins = localEnabledPlugins;
var excludedGlobs = _getExcludedGlobs(
root.optionsFile,
workspace.partialSourceFactory,
);
root.excludedGlobs.addAll(excludedGlobs);
usedThisRoot = false;
}
if (optionsFileToUse != null &&
optionsFileToUse != optionsFileFromParentInSameRoot) {
containingRoot.optionsFileMap
.putIfAbsent(optionsFileToUse, () => {})
.add(folder);
// Add excluded globs only if we found a new options file.
var excludes = _getExcludedGlobs(
optionsFileToUse,
containingRoot.workspace.partialSourceFactory,
);
containingRoot.excludedGlobs.addAll(excludes);
}
_createContextRootsIn(
visited,
folder,
containingRoot,
containingRootEnabledLegacyPlugins,
optionsFileToUseForFolder: usedThisRoot ? optionsFileToUse : null,
);
return usedThisRoot;
}
/// For each directory within the given [folder] that isn't excluded by the
/// `containingRoot.excludedGlobs`, recursively searches for nested context
/// roots and add them to the list of [_roots].
///
/// If either the [_defaultOptionsFile] or [_defaultPackageConfigFile] is
/// non-`null`, then the given file will be used even if there is a local
/// version of the file.
void _createContextRootsIn(
Set<String> visited,
Folder folder,
ContextRootImpl containingRoot,
Set<String> containingRootEnabledLegacyPlugins, {
File? optionsFileToUseForFolder,
}) {
bool isExcluded(Folder folder) {
if (folder.shortName.startsWith('.')) {
return true;
}
for (var pattern in containingRoot.excludedGlobs) {
if (pattern.matches(folder.path)) {
return true;
}
}
return false;
}
List<Resource> children;
try {
// Stop infinite recursion via links.
var canonicalFolderPath = folder.resolveSymbolicLinksSync().path;
if (!visited.add(canonicalFolderPath)) {
return;
}
// Check each of the subdirectories to see whether a context root needs to
// be added for it.
children = folder.getChildren();
} on FileSystemException {
// The directory either doesn't exist or cannot be read. Either way, there
// are no subdirectories that need to be added.
return;
}
for (Resource child in children) {
if (child is Folder) {
if (!isExcluded(child)) {
_createContextRoots(
visited,
child,
containingRoot,
containingRootEnabledLegacyPlugins,
optionsFileFromParentInSameRoot: optionsFileToUseForFolder,
);
}
}
}
}
Workspace _createWorkspace({
required Folder folder,
required File? packageConfigFile,
required File? buildGnFile,
}) {
if (buildGnFile != null) {
var workspace = GnWorkspace.find(buildGnFile);
if (workspace != null) {
return workspace;
}
}
Packages? providedPackages = packageConfigFile != null
? _packagesByFile.putIfAbsent(
packageConfigFile,
() => parsePackageConfigJsonFile(
_resourceProvider,
packageConfigFile,
),
)
: null;
var rootPath = folder.path;
Workspace? workspace = _mostSpecificWorkspace(
BlazeWorkspace.find(
_resourceProvider,
rootPath,
lookForBuildFileSubstitutes: false,
),
_packageConfigWorkspaceFindSession.find(
rootPath,
providedPackages: providedPackages,
),
);
return workspace ??
BasicWorkspace.find(
_resourceProvider,
providedPackages ?? Packages.empty,
rootPath,
);
}
File? _effectiveOptionsFile(_RootLocation location) {
if (location.optionsFile case var optionsFile?) {
return optionsFile;
}
if (location.workspace is WorkspaceWithDefaultAnalysisOptions) {
return _findDefaultOptionsFile(location.workspace.partialSourceFactory);
}
return null;
}
File? _findBuildGnFile(Folder folder) {
for (var current in folder.withAncestors) {
var file = current.getExistingFile(file_paths.buildGn);
if (file != null) {
return file;
}
}
return null;
}
File? _findDefaultOptionsFile(SourceFactory sourceFactory) {
var uriStr = WorkspaceWithDefaultAnalysisOptions.uri;
var path = sourceFactory.forUri(uriStr)?.fullName;
if (path != null) {
var file = _resourceProvider.getFile(path);
if (file.exists) {
return file;
}
}
return null;
}
/// Returns the folder containing the `.dart_tool/` folder which contains the
/// package config file to be used to analyze files in the given [folder], or
/// `null` if there is no package config file in the given folder or any
/// parent folder.
Folder? _findFolderWithPackageConfigFile(Folder folder) {
for (var current in folder.withAncestors) {
var file = _getPackageConfigFile(current);
if (file != null) {
return current;
}
}
return null;
}
/// Gets an unmodifiable set of enabled legacy plugins for [optionsFile],
/// taking into account any includes.
Set<String> _getEnabledLegacyPlugins(Workspace workspace, File? optionsFile) {
if (optionsFile == null) {
return const {};
}
try {
var sourceFactory = workspace.partialSourceFactory;
var key = (sourceFactory, optionsFile);
return _legacyPlugins[key] ??= Set.unmodifiable(
_analysisOptionsParseSession
.parse(
sourceFactory: sourceFactory,
contextRoot: optionsFile.parent,
file: optionsFile,
)
.analysisOptions
.enabledLegacyPluginNames,
);
} catch (_) {
// No legacy plugins will be enabled if the file doesn't parse or cannot
// be read for any reason.
return const {};
}
}
/// Return a list containing the glob patterns used to exclude files from
/// analysis by the given [optionsFile]. The list will be empty if there is no
/// options file or if there are no exclusion patterns in the options file.
///
/// All patterns are relative to the folder of [optionsFile], including
/// patterns from included files, not to their declaring files: shared
/// options describe files of the including package.
List<LocatedGlob> _getExcludedGlobs(
File? optionsFile,
SourceFactory sourceFactory,
) {
if (optionsFile == null) return const [];
List<AnalysisOptionsExcludePattern> excludePatterns;
try {
excludePatterns = _analysisOptionsParseSession
.parse(
sourceFactory: sourceFactory,
contextRoot: optionsFile.parent,
file: optionsFile,
)
.analysisOptions
.excludePatterns2;
} catch (exception) {
// If we can't read and parse the analysis options file, then there
// aren't any excluded files that need to be read.
return const [];
}
var pathContext = _resourceProvider.pathContext;
List<LocatedGlob> patterns = [];
void addGlob(List<String> components) {
var pattern = posix.joinAll(components);
patterns.add(
LocatedGlob(optionsFile.parent, Glob(pattern, context: pathContext)),
);
}
for (var excluded in excludePatterns) {
var excludedComponents = posix.split(excluded.pattern);
addGlob(excludedComponents);
if (excludedComponents.length > 1 && excludedComponents.last == '**') {
addGlob(excludedComponents..removeLast());
}
}
return patterns;
}
/// Returns the package config file in the given [folder], or `null` if the
/// folder does not contain a package config file.
File? _getPackageConfigFile(Folder folder) {
var file = folder
.getFolder(file_paths.dotDartTool)
.getFile(file_paths.packageConfigJson);
if (file.exists) {
return file;
}
return null;
}
bool _hasSameResolution(ContextRootImpl root, _RootLocation location) {
return root.packagesFile == location.packageConfigFile &&
_sameWorkspace(root.workspace, location.workspace);
}
/// Load the `workspace` paths from the pubspec file in the given [root].
///
/// From https://dart.dev/tools/pub/workspaces a root folder pubspec file will
/// look like this:
///
/// ```
/// name: _
/// publish_to: none
/// environment:
/// sdk: ^3.6.0
/// workspace:
/// - packages/helper
/// - packages/client_package
/// - packages/server_package
/// ```
///
/// This loads the paths from the `workspace` entry and return them as
/// Folders if they exist as folders in the filesystem.
Set<Folder> _loadWorkspaceDetailsFromPubspec(String root) {
var result = <Folder>{};
var rootFolder = _resourceProvider.getFolder(root);
var rootPubspecFile = rootFolder.getFile(file_paths.pubspecYaml);
if (rootPubspecFile.exists) {
var rootPubspec = Pubspec.parse(
rootPubspecFile.readAsStringSync(),
resourceProvider: _resourceProvider,
);
var workspace = rootPubspec.workspace;
if (workspace != null) {
for (var entry in workspace) {
if (entry.text case var relativePath?) {
var child = rootFolder.getChild(relativePath);
if (child.exists && child is Folder) {
result.add(child);
}
}
}
}
}
return result;
}
/// For each resource in [includedFolders] and [includedFiles], walks the
/// directory structure, creates context roots, and returns them.
List<ContextRootImpl> _locateRoots({
required List<Folder> includedFolders,
required List<File> includedFiles,
}) {
var (workspaceResolutionRootMap, nonWorkspaceResolutionFolders) =
_sortIncludedFoldersIntoWorkspaceResolutions(includedFolders);
for (var MapEntry(key: workspaceRoot, value: workspaceFolders)
in workspaceResolutionRootMap.entries) {
var workspaceRootFolder = _resourceProvider.getFolder(workspaceRoot);
var location = _contextRootLocation(
workspaceRootFolder,
defaultRootFolder: () => workspaceRootFolder,
);
ContextRootImpl root = _createContextRoot(
rootFolder: workspaceRootFolder,
optionsFile: location.optionsFile,
location: location,
);
var rootEnabledLegacyPlugins = _getEnabledLegacyPlugins(
location.workspace,
location.optionsFile,
);
Set<String> visited = {};
bool usedRoot = false;
for (var folder in workspaceFolders) {
if (!root.isAnalyzed(folder.path)) {
root.included.add(folder);
}
usedRoot |= _createContextRoots(
visited,
folder,
root,
rootEnabledLegacyPlugins,
);
}
if (!usedRoot) {
// If all included folders under this workspace resolution ended up
// creating new contexts remove the (not used) root.
_roots.remove(root);
}
}
for (Folder folder in nonWorkspaceResolutionFolders) {
var location = _contextRootLocation(
folder,
defaultRootFolder: () => folder,
);
ContextRootImpl? root;
// Check whether there are existing roots that overlap with this one.
for (var existingRoot in _roots) {
if (existingRoot.root.isOrContains(folder.path)) {
if (_matchRootWithLocation(existingRoot, location)) {
// This root is covered exactly by the existing root (with the same
// options/packages file) so we can simple use it.
root = existingRoot;
break;
} else {
// This root is within another (but doesn't share options/packages)
// so we still need a new root. However, we should exclude this
// from the existing root so these files aren't analyzed by both.
//
// It's possible this folder is already excluded (for example
// because it's also a project and had a context root created as
// part of the parent analysis root).
if (!existingRoot.excluded.contains(folder)) {
existingRoot.excluded.add(folder);
}
}
}
}
root ??= _createContextRoot(
rootFolder: folder,
optionsFile: location.optionsFile,
location: location,
);
if (!root.isAnalyzed(folder.path)) {
root.included.add(folder);
}
var rootEnabledLegacyPlugins = _getEnabledLegacyPlugins(
location.workspace,
location.optionsFile,
);
_createContextRootsIn({}, folder, root, rootEnabledLegacyPlugins);
}
_addIncludedFiles(includedFiles);
return _roots;
}
/// Picks a workspace with the most specific root.
///
/// If any of [first] and [second] is null, returns the other one. If the root
/// of [first] is non-null and is within the root of [second], returns
/// [second]. If the roots aren't within each other, return [first].
Workspace? _mostSpecificWorkspace(Workspace? first, Workspace? second) {
if (first == null) return second;
if (second == null) return first;
var pathContext = _resourceProvider.pathContext;
return pathContext.isWithin(first.root, second.root) ? second : first;
}
/// Returns an existing or newly created context root for [folder].
///
/// Searches [_roots] for a root that contains [folder], shares the same
/// workspace resolution semantics, and has identical enabled legacy plugins.
/// If none exists, creates a new root (partitioned at the resolution root, or
/// at the folder if legacy plugins are present) and adds it to [_roots].
///
/// Also registers the path-local options mapping for [folder] on the root.
ContextRootImpl _rootForFolder(Folder folder) {
var location = _contextRootLocation(
folder,
defaultRootFolder: () => _fileSystemRoot(folder),
);
var optionsFile = _effectiveOptionsFile(location);
var enabledLegacyPlugins = _getEnabledLegacyPlugins(
location.workspace,
optionsFile,
);
ContextRootImpl? existingRoot;
for (var root in _roots) {
if (root.root.isOrContains(folder.path) &&
_hasSameResolution(root, location) &&
const SetEquality<String>().equals(
_getEnabledLegacyPlugins(root.workspace, root.optionsFile),
enabledLegacyPlugins,
)) {
existingRoot = root;
break;
}
}
// When reusing a context, use its root to determine whether these options
// apply to the whole context or only to a nested folder.
Folder rootFolder;
if (existingRoot != null) {
rootFolder = existingRoot.root;
} else if (enabledLegacyPlugins.isEmpty) {
rootFolder = location.resolutionRootFolder;
} else {
rootFolder = location.contextRootFolder;
}
var optionsApplyToWholeContext =
_defaultOptionsFile != null ||
(location.optionsFile == null &&
location.workspace is WorkspaceWithDefaultAnalysisOptions) ||
(optionsFile?.parent.isOrContains(rootFolder.path) ?? false);
var root = existingRoot;
root ??= _createContextRoot(
rootFolder: rootFolder,
optionsFile: optionsApplyToWholeContext ? optionsFile : null,
location: location,
useWorkspaceDefaultOptions: false,
);
_addOptionsMapping(
root,
optionsFile,
mappingFolder: optionsApplyToWholeContext ? root.root : null,
);
return root;
}
/// Returns whether [first] and [second] define the same workspace-wide
/// resolution semantics.
bool _sameWorkspace(Workspace first, Workspace second) {
if (first.runtimeType != second.runtimeType) return false;
// BasicWorkspace has no marker-defined configuration. Containment and the
// package-config comparison decide whether an existing context can be used.
if (first is BasicWorkspace) return true;
return first.root == second.root;
}
/// Sorts [includedFolders] into either pub workspace resolution or not.
///
/// For each [Folder] in [includedFolders], sorts into either
/// `nonWorkspaceResolutionFolders` or `workspaceResolutionRootMap` depending
/// on `pubspec.yaml` specifications.
///
/// Folders with `pubspec.yaml` files with a `resolution: workspace` setting
/// that matches a root-folders `pubspec.yaml` file's `workspace` list is
/// sorted into the `workspaceResolutionRootMap` map. Other folders end up in
/// `nonWorkspaceResolutionFolders`.
(Map<String, List<Folder>>, List<Folder>)
_sortIncludedFoldersIntoWorkspaceResolutions(List<Folder> includedFolders) {
var workspaceResolutionRootMap = <String, List<Folder>>{};
var nonWorkspaceResolutionFolders = <Folder>[];
var rootWorkspaceSpecification = <String, Set<Folder>>{};
for (Folder folder in includedFolders) {
var location = _contextRootLocation(
folder,
defaultRootFolder: () => folder,
);
var addedToWorkspace = false;
if (folder.path == location.workspace.root) {
// If opening the root don't try to do anything special.
var known = rootWorkspaceSpecification[location.workspace.root] ??= {};
known.clear();
nonWorkspaceResolutionFolders.addAll(
workspaceResolutionRootMap[location.workspace.root] ?? [],
);
} else {
var pubspecFile = folder.getFile(file_paths.pubspecYaml);
if (pubspecFile.exists) {
var pubspec = Pubspec.parse(
pubspecFile.readAsStringSync(),
resourceProvider: _resourceProvider,
);
var resolution = pubspec.resolution;
if (resolution != null && resolution.value.text == 'workspace') {
var known = rootWorkspaceSpecification[location.workspace.root] ??=
_loadWorkspaceDetailsFromPubspec(location.workspace.root);
if (known.contains(folder)) {
(workspaceResolutionRootMap[location.workspace.root] ??= []).add(
folder,
);
addedToWorkspace = true;
}
}
}
}
if (!addedToWorkspace) {
nonWorkspaceResolutionFolders.add(folder);
}
}
return (workspaceResolutionRootMap, nonWorkspaceResolutionFolders);
}
static Folder _fileSystemRoot(Resource resource) {
for (var current = resource.parent; ; current = current.parent) {
if (current.isRoot) {
return current;
}
}
}
/// Every element in [folders] must be a folder on the path from a file to
/// the root of the file system. As such, they are either the same folder,
/// or one is strictly above the other.
static Folder? _lowest(List<Folder?> folders) {
return folders.fold<Folder?>(null, (result, folder) {
if (result == null) {
return folder;
} else if (folder != null && result.contains(folder.path)) {
return folder;
} else {
return result;
}
});
}
/// Return `true` if the configuration of [existingRoot] is the same as
/// the requested configuration for the [location].
static bool _matchRootWithLocation(
ContextRootImpl existingRoot,
_RootLocation location,
) {
if (existingRoot.optionsFile != location.optionsFile) {
return false;
}
if (existingRoot.packagesFile != location.packageConfigFile) {
return false;
}
// BasicWorkspace has no special meaning, so can be ignored.
// Other workspaces have semantic meaning, so must match.
var workspace = location.workspace;
if (workspace is! BasicWorkspace &&
existingRoot.workspace.root != workspace.root) {
return false;
}
return true;
}
}
class _RootLocation {
/// The context root selected from both resolution configuration and the
/// effective analysis options file.
///
/// Unlike [resolutionRootFolder], this can be lowered to the options file's
/// directory. Directory includes use this root; explicitly included files
/// can share [resolutionRootFolder] when their options are path-local.
final Folder contextRootFolder;
/// The root selected by configuration that affects URI and package
/// resolution, without lowering it to the directory of an options file.
final Folder resolutionRootFolder;
final Workspace workspace;
final File? optionsFile;
final File? packageConfigFile;
_RootLocation({
required this.contextRootFolder,
required this.resolutionRootFolder,
required this.workspace,
required this.optionsFile,
required this.packageConfigFile,
});
}