blob: 5c3382a924f1c6a4ace3d0231dfcf46e4f722cb2 [file] [log] [blame]
// 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/dart/analysis/context_root.dart';
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_file.dart';
import 'package:analyzer/src/analysis_options/analysis_options_provider.dart';
import 'package:analyzer/src/context/packages.dart';
import 'package:analyzer/src/dart/analysis/analysis_options.dart';
import 'package:analyzer/src/dart/analysis/context_root.dart';
import 'package:analyzer/src/lint/pub.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/yaml.dart';
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';
import 'package:yaml/yaml.dart';
/// 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 ContextLocatorImpl {
/// The resource provider used to access the file system.
final ResourceProvider resourceProvider;
/// 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.
ContextLocatorImpl({ResourceProvider? resourceProvider})
: resourceProvider = resourceProvider ?? PhysicalResourceProvider.INSTANCE;
/// Return a list of the context roots that should be used to analyze the
/// files that are included by the list of [includedPaths] and not excluded by
/// the list of [excludedPaths].
///
/// 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 [packagesFile] is specified, then it is assumed to be the path to the
/// `.packages` file that should be used in place of the one that would be
/// found by looking in the directories containing the context roots.
List<ContextRoot> locateRoots({
required List<String> includedPaths,
List<String>? excludedPaths,
String? optionsFile,
String? packagesFile,
}) {
//
// Compute the list of folders and files that are to be included.
//
List<Folder> includedFolders = <Folder>[];
List<File> includedFiles = <File>[];
_resourcesFromPaths(includedPaths, includedFolders, includedFiles);
//
// Compute the list of folders and files that are to be excluded.
//
List<Folder> excludedFolders = <Folder>[];
List<File> excludedFiles = <File>[];
_resourcesFromPaths(
excludedPaths ?? const <String>[],
excludedFolders,
excludedFiles,
);
//
// Use the excluded folders and files to filter the included folders and
// files.
//
includedFolders = includedFolders
.where(
(Folder includedFolder) =>
!_containedInAny(excludedFolders, includedFolder),
)
.toList();
includedFiles = includedFiles
.where(
(File includedFile) =>
!_containedInAny(excludedFolders, includedFile) &&
!excludedFiles.contains(includedFile),
)
.toList();
//
// We now have a list of all of the files and folders that need to be
// analyzed. For each, walk the directory structure and figure out where to
// create context roots.
//
File? defaultOptionsFile;
if (optionsFile != null) {
defaultOptionsFile = resourceProvider.getFile(optionsFile);
if (!defaultOptionsFile.exists) {
defaultOptionsFile = null;
}
}
File? defaultPackagesFile;
if (packagesFile != null) {
defaultPackagesFile = resourceProvider.getFile(packagesFile);
if (!defaultPackagesFile.exists) {
defaultPackagesFile = null;
}
}
var workspaceResolutionRootMap = <String, List<Folder>>{};
var nonWorkspaceResolutionFolders = <Folder>[];
_sortIncludedFoldersIntoWorkspaceResolutions(
includedFolders,
defaultOptionsFile,
defaultPackagesFile,
nonWorkspaceResolutionFolders,
workspaceResolutionRootMap,
);
var roots = <ContextRootImpl>[];
for (var workspaceResolution in workspaceResolutionRootMap.entries) {
var workspaceRootFolder = resourceProvider.getFolder(
workspaceResolution.key,
);
var location = _contextRootLocation(
workspaceRootFolder,
defaultOptionsFile: defaultOptionsFile,
defaultPackagesFile: defaultPackagesFile,
defaultRootFolder: () => workspaceRootFolder,
);
ContextRootImpl root = _createContextRoot(
roots,
rootFolder: workspaceRootFolder,
workspace: location.workspace,
optionsFile: location.optionsFile,
packagesFile: location.packagesFile,
);
var rootEnabledLegacyPlugins = _getEnabledLegacyPlugins(
location.workspace,
location.optionsFile,
);
Set<String> visited = {};
bool usedRoot = false;
for (var folder in workspaceResolution.value) {
if (!root.isAnalyzed(folder.path)) {
root.included.add(folder);
}
usedRoot |= _createContextRoots(
roots,
visited,
folder,
excludedFolders,
root,
rootEnabledLegacyPlugins,
root.excludedGlobs,
defaultOptionsFile,
defaultPackagesFile,
);
}
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,
defaultOptionsFile: defaultOptionsFile,
defaultPackagesFile: defaultPackagesFile,
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(
roots,
rootFolder: folder,
workspace: location.workspace,
optionsFile: location.optionsFile,
packagesFile: location.packagesFile,
);
if (!root.isAnalyzed(folder.path)) {
root.included.add(folder);
}
var rootEnabledLegacyPlugins = _getEnabledLegacyPlugins(
location.workspace,
location.optionsFile,
);
_createContextRootsIn(
roots,
{},
folder,
excludedFolders,
root,
rootEnabledLegacyPlugins,
root.excludedGlobs,
defaultOptionsFile,
defaultPackagesFile,
);
}
for (File file in includedFiles) {
Folder parent = file.parent;
var location = _contextRootLocation(
parent,
defaultOptionsFile: defaultOptionsFile,
defaultPackagesFile: defaultPackagesFile,
defaultRootFolder: () => _fileSystemRoot(parent),
);
ContextRootImpl? root;
for (var existingRoot in roots) {
if (existingRoot.root.isOrContains(file.path) &&
_matchRootWithLocation(existingRoot, location)) {
root = existingRoot;
break;
}
}
root ??= _createContextRoot(
roots,
rootFolder: location.rootFolder,
workspace: location.workspace,
optionsFile: location.optionsFile,
packagesFile: location.packagesFile,
);
if (!root.isAnalyzed(file.path)) {
root.included.add(file);
}
}
return roots;
}
/// Return `true` if the given [resource] is contained in one or more of the
/// given [folders].
bool _containedInAny(Iterable<Folder> folders, Resource resource) =>
folders.any((Folder folder) => folder.contains(resource.path));
/// Return the location of a context root for a file in the [parent].
///
/// If the [defaultOptionsFile] is provided, it will be used, not a file
/// found relative to the [parent].
///
/// If the [defaultPackagesFile] is provided, 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 packages (grand-parent for `.dart_tool/package_config.json`) file,
/// whichever is lower.
_RootLocation _contextRootLocation(
Folder parent, {
required File? defaultOptionsFile,
required File? defaultPackagesFile,
required Folder Function() defaultRootFolder,
}) {
File? optionsFile;
Folder? optionsFolderToChooseRoot;
if (defaultOptionsFile != null) {
optionsFile = defaultOptionsFile;
} else {
optionsFile = parent.findAnalysisOptionsYamlFile();
optionsFolderToChooseRoot = optionsFile?.parent;
}
File? packagesFile;
Folder? packagesFolderToChooseRoot;
if (defaultPackagesFile != null) {
packagesFile = defaultPackagesFile;
// If the packages file is in .dart_tool directory, use the grandparent
// folder, else use the parent folder.
packagesFolderToChooseRoot =
_findPackagesFile(packagesFile.parent)?.parent ?? packagesFile.parent;
}
var buildGnFile = _findBuildGnFile(parent);
var rootFolder = _lowest([optionsFolderToChooseRoot, buildGnFile?.parent]);
// If default packages file is given, create workspace for it.
var workspace = _createWorkspace(
folder: parent,
packagesFile: packagesFile,
buildGnFile: buildGnFile,
);
if (workspace is! BasicWorkspace) {
rootFolder = _lowest([
rootFolder,
resourceProvider.getFolder(workspace.root),
]);
}
if (workspace is PackageConfigWorkspace) {
packagesFile ??= workspace.packageConfigFile;
// If the default packages folder is a parent of the workspace root,
// choose that as the root.
if (rootFolder != null && packagesFolderToChooseRoot != null) {
if (packagesFolderToChooseRoot.contains(rootFolder.path)) {
rootFolder = packagesFolderToChooseRoot;
}
}
}
if (rootFolder == null) {
rootFolder = defaultRootFolder();
if (workspace is BasicWorkspace) {
workspace = _createWorkspace(
folder: rootFolder,
packagesFile: packagesFile,
buildGnFile: buildGnFile,
);
}
}
return _RootLocation(
rootFolder: rootFolder,
workspace: workspace,
optionsFile: optionsFile,
packagesFile: packagesFile,
);
}
ContextRootImpl _createContextRoot(
List<ContextRootImpl> roots, {
required Folder rootFolder,
required Workspace workspace,
required File? optionsFile,
required File? packagesFile,
}) {
optionsFile ??= _findDefaultOptionsFile(workspace);
var root = ContextRootImpl(resourceProvider, rootFolder, workspace);
root.packagesFile = packagesFile;
root.optionsFile = optionsFile;
if (optionsFile != null) {
root.optionsFileMap[rootFolder] = optionsFile;
}
root.excludedGlobs = _getExcludedGlobs(optionsFile, workspace);
roots.add(root);
return root;
}
/// If the given [folder] should be the root of a new analysis context, then
/// create 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 [optionsFile] or [packagesFile] is non-`null` then the given
/// file will be used even if there is a local version of the file.
///
/// For each directory within the given [folder] that is neither in the list
/// of [excludedFolders] nor excluded by the [excludedGlobs], recursively
/// search 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(
List<ContextRoot> roots,
Set<String> visited,
Folder folder,
List<Folder> excludedFolders,
ContextRoot containingRoot,
Set<String> containingRootEnabledLegacyPlugins,
List<LocatedGlob> excludedGlobs,
File? optionsFile,
File? packagesFile,
) {
//
// If the options and packages files are allowed to be locally specified,
// then look to see whether they are.
//
File? localOptionsFile;
if (optionsFile == null) {
localOptionsFile = folder.existingAnalysisOptionsYamlFile;
}
File? localPackagesFile;
if (packagesFile == null) {
localPackagesFile = _getPackagesFile(folder);
}
var buildGnFile = folder.getExistingFile(file_paths.buildGn);
var localEnabledPlugins = _getEnabledLegacyPlugins(
containingRoot.workspace,
localOptionsFile,
);
// Legacy plugins differ only if there is an analysis_options and it
// contains a different set of plugins from the containing context.
var pluginsDiffer =
localOptionsFile != 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 || localPackagesFile != null || buildGnFile != null) {
if (optionsFile != null) {
localOptionsFile = optionsFile;
}
if (packagesFile != null) {
localPackagesFile = packagesFile;
}
var rootPackagesFile = localPackagesFile ?? containingRoot.packagesFile;
var workspace = _createWorkspace(
folder: folder,
packagesFile: rootPackagesFile,
buildGnFile: buildGnFile,
);
var root = ContextRootImpl(resourceProvider, folder, workspace);
root.packagesFile = rootPackagesFile;
// Check for analysis options file in the parent directories, from
// root folder to the containing root folder. Pick the one closest
// to the root.
if (localOptionsFile == null) {
var parentFolder = root.root.parent;
while (parentFolder != containingRoot.root) {
localOptionsFile = parentFolder.existingAnalysisOptionsYamlFile;
if (localOptionsFile != null) {
break;
}
parentFolder = parentFolder.parent;
}
}
root.optionsFile = localOptionsFile ?? containingRoot.optionsFile;
root.included.add(folder);
containingRoot.excluded.add(folder);
roots.add(root);
containingRoot = root;
containingRootEnabledLegacyPlugins = localEnabledPlugins;
excludedGlobs = _getExcludedGlobs(root.optionsFile, workspace);
root.excludedGlobs = excludedGlobs;
usedThisRoot = false;
}
if (localOptionsFile != null) {
(containingRoot as ContextRootImpl).optionsFileMap[folder] =
localOptionsFile;
// Add excluded globs.
var excludes = _getExcludedGlobs(
localOptionsFile,
containingRoot.workspace,
);
containingRoot.excludedGlobs.addAll(excludes);
}
_createContextRootsIn(
roots,
visited,
folder,
excludedFolders,
containingRoot,
containingRootEnabledLegacyPlugins,
excludedGlobs,
optionsFile,
packagesFile,
);
return usedThisRoot;
}
/// For each directory within the given [folder] that is neither in the list
/// of [excludedFolders] nor excluded by the [excludedGlobs], recursively
/// search for nested context roots and add them to the list of [roots].
///
/// If either the [optionsFile] or [packagesFile] is non-`null` then the given
/// file will be used even if there is a local version of the file.
void _createContextRootsIn(
List<ContextRoot> roots,
Set<String> visited,
Folder folder,
List<Folder> excludedFolders,
ContextRoot containingRoot,
Set<String> containingRootEnabledLegacyPlugins,
List<LocatedGlob> excludedGlobs,
File? optionsFile,
File? packagesFile,
) {
bool isExcluded(Folder folder) {
if (excludedFolders.contains(folder) ||
folder.shortName.startsWith('.')) {
return true;
}
// TODO(scheglov): Why not take it from `containingRoot`?
for (var pattern in excludedGlobs) {
if (pattern.matches(folder.path)) {
return true;
}
}
return false;
}
// Stop infinite recursion via links.
try {
var canonicalFolderPath = folder.resolveSymbolicLinksSync().path;
if (!visited.add(canonicalFolderPath)) {
return;
}
} on FileSystemException {
return;
}
//
// Check each of the subdirectories to see whether a context root needs to
// be added for it.
//
try {
for (Resource child in folder.getChildren()) {
if (child is Folder) {
if (excludedFolders.contains(child)) {
containingRoot.excluded.add(child);
} else if (!isExcluded(child)) {
_createContextRoots(
roots,
visited,
child,
excludedFolders,
containingRoot,
containingRootEnabledLegacyPlugins,
excludedGlobs,
optionsFile,
packagesFile,
);
}
}
}
} on FileSystemException {
// The directory either doesn't exist or cannot be read. Either way, there
// are no subdirectories that need to be added.
}
}
Workspace _createWorkspace({
required Folder folder,
required File? packagesFile,
required File? buildGnFile,
}) {
if (buildGnFile != null) {
var workspace = GnWorkspace.find(buildGnFile);
if (workspace != null) {
return workspace;
}
}
Packages packages;
if (packagesFile != null) {
packages = parsePackageConfigJsonFile(resourceProvider, packagesFile);
} else {
packages = Packages.empty;
}
var rootPath = folder.path;
Workspace? workspace;
workspace = BlazeWorkspace.find(
resourceProvider,
rootPath,
lookForBuildFileSubstitutes: false,
);
workspace = _mostSpecificWorkspace(
workspace,
PackageConfigWorkspace.find(resourceProvider, packages, rootPath),
);
workspace ??= BasicWorkspace.find(resourceProvider, packages, rootPath);
return workspace;
}
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(Workspace workspace) {
if (workspace is! WorkspaceWithDefaultAnalysisOptions) {
return null;
}
// TODO(scheglov): Create SourceFactory once.
var sourceFactory = workspace.createSourceFactory(null, null);
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;
}
/// Return the packages file to be used to analyze files in the given
/// [folder], or `null` if there is no packages file in the given folder or
/// any parent folder.
_PackagesFile? _findPackagesFile(Folder folder) {
for (var current in folder.withAncestors) {
var file = _getPackagesFile(current);
if (file != null) {
return _PackagesFile(current, file);
}
}
return null;
}
/// Gets the 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 provider = AnalysisOptionsProvider(
workspace.createSourceFactory(null, null),
);
var options = AnalysisOptionsImpl.fromYaml(
optionsMap: provider.getOptionsFromFile(optionsFile),
file: optionsFile,
resourceProvider: resourceProvider,
);
return options.enabledLegacyPluginNames.toSet();
} catch (_) {
// No legacy plugins will be enabled if the file doesn't parse or cannot
// be read for any reason.
return {};
}
}
/// 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.
List<LocatedGlob> _getExcludedGlobs(File? optionsFile, Workspace workspace) {
List<LocatedGlob> patterns = [];
if (optionsFile != null) {
try {
var doc = AnalysisOptionsProvider(
workspace.createSourceFactory(null, null),
).getOptionsFromFile(optionsFile);
var analyzerOptions = doc.valueAt(AnalysisOptionsFile.analyzer);
if (analyzerOptions is YamlMap) {
var excludeOptions = analyzerOptions.valueAt(
AnalysisOptionsFile.exclude,
);
if (excludeOptions is YamlList) {
var pathContext = resourceProvider.pathContext;
void addGlob(List<String> components) {
var pattern = posix.joinAll(components);
patterns.add(
LocatedGlob(
optionsFile.parent,
Glob(pattern, context: pathContext),
),
);
}
for (String excludedPath in excludeOptions.whereType<String>()) {
var excludedComponents = posix.split(excludedPath);
addGlob(excludedComponents);
if (excludedComponents.last == '**') {
addGlob(excludedComponents..removeLast());
}
}
}
}
} 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 patterns;
}
/// Return the packages file in the given [folder], or `null` if the folder
/// does not contain a packages file.
File? _getPackagesFile(Folder folder) {
var file = folder
.getChildAssumingFolder(file_paths.dotDartTool)
.getChildAssumingFile(file_paths.packageConfigJson);
if (file.exists) {
return file;
}
return null;
}
/// 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.getChildAssumingFile(
file_paths.pubspecYaml,
);
if (rootPubspecFile.exists) {
var rootPubspec = Pubspec.parse(rootPubspecFile.readAsStringSync());
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;
}
/// Add to the given lists of [folders] and [files] all of the resources in
/// the given list of [paths] that exist and are not contained within one of
/// the folders.
void _resourcesFromPaths(
List<String> paths,
List<Folder> folders,
List<File> files,
) {
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.
}
}
}
/// Sorts [includedFolders] into either pub workspace resolution or not.
///
/// For each [Folder] in [includedFolders] sort 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` files `workspace` list is
/// sorted into the [workspaceResolutionRootMap] map. Other folders end up in
/// [nonWorkspaceResolutionFolders].
void _sortIncludedFoldersIntoWorkspaceResolutions(
List<Folder> includedFolders,
File? defaultOptionsFile,
File? defaultPackagesFile,
List<Folder> nonWorkspaceResolutionFolders,
Map<String, List<Folder>> workspaceResolutionRootMap,
) {
var rootWorkspaceSpecification = <String, Set<Folder>>{};
for (Folder folder in includedFolders) {
var location = _contextRootLocation(
folder,
defaultOptionsFile: defaultOptionsFile,
defaultPackagesFile: defaultPackagesFile,
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.getChildAssumingFile(file_paths.pubspecYaml);
if (pubspecFile.exists) {
var pubspec = Pubspec.parse(pubspecFile.readAsStringSync());
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 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;
}
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.packagesFile) {
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) {
if (existingRoot.workspace.root != workspace.root) {
return false;
}
}
return true;
}
/// Pick a workspace with the most specific root. If the root of [first] is
/// non-null and is within the root of [second], return [second]. If any of
/// [first] and [second] is null, return the other one. If the roots aren't
/// within each other, return [first].
static Workspace? _mostSpecificWorkspace(
Workspace? first,
Workspace? second,
) {
if (first == null) return second;
if (second == null) return first;
if (isWithin(first.root, second.root)) {
return second;
}
return first;
}
}
/// The packages [file] found for the [parent].
///
/// In case of `.packages` file, [parent] is the parent of [file].
///
/// In case of `.dart_tool/package_config.json` it is a grand-parent.
class _PackagesFile {
final Folder parent;
final File file;
_PackagesFile(this.parent, this.file);
}
class _RootLocation {
final Folder rootFolder;
final Workspace workspace;
final File? optionsFile;
final File? packagesFile;
_RootLocation({
required this.rootFolder,
required this.workspace,
required this.optionsFile,
required this.packagesFile,
});
}