// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:async'; import 'dart:collection'; import 'package:analyzer/file_system/file_system.dart'; import 'package:analyzer/source/file_source.dart'; import 'package:analyzer/source/source.dart'; import 'package:analyzer/src/context/packages.dart'; import 'package:analyzer/src/file_system/file_system.dart'; import 'package:analyzer/src/generated/sdk.dart'; import 'package:analyzer/src/generated/source.dart'; import 'package:analyzer/src/summary/package_bundle_reader.dart'; import 'package:analyzer/src/util/file_paths.dart' as file_paths; import 'package:analyzer/src/util/uri.dart'; import 'package:analyzer/src/utilities/uri_cache.dart'; import 'package:analyzer/src/workspace/blaze_watcher.dart'; import 'package:analyzer/src/workspace/workspace.dart'; import 'package:path/path.dart' as path; import 'package:pub_semver/pub_semver.dart'; /// Instances of the class `BlazeFileUriResolver` resolve `file` URI's by first /// resolving file uri's in the expected way, and then by looking in the /// corresponding generated directories. class BlazeFileUriResolver extends ResourceUriResolver { final BlazeWorkspace workspace; BlazeFileUriResolver(this.workspace) : super(workspace.provider); @override Uri pathToUri(String path) { var pathContext = workspace.provider.pathContext; for (var genRoot in [...workspace.binPaths, workspace.genfiles]) { if (pathContext.isWithin(genRoot, path)) { String relative = pathContext.relative(path, from: genRoot); var writablePath = pathContext.join(workspace.root, relative); return pathContext.toUri(writablePath); } } return workspace.provider.pathContext.toUri(path); } @override Source? resolveAbsolute(Uri uri) { if (!ResourceUriResolver.isFileUri(uri)) { return null; } String filePath = fileUriToNormalizedPath(provider.pathContext, uri); var file = workspace.findFile(filePath); if (file != null) { return FileSource(file, uri); } return null; } } /// The [UriResolver] that can resolve `package` URIs in [BlazeWorkspace]. class BlazePackageUriResolver extends UriResolver { final BlazeWorkspace _workspace; final path.Context _context; /// The cache of absolute [Uri]s to [Source]s mappings. final Map _sourceCache = HashMap(); BlazePackageUriResolver(BlazeWorkspace workspace) : _workspace = workspace, _context = workspace.provider.pathContext; @override Uri? pathToUri(String path) { // Search in each root. for (var root in [ ..._workspace.binPaths, _workspace.genfiles, _workspace.root, ]) { var uriParts = _restoreUriParts(root, path); if (uriParts != null) { return uriCache.parse('package:${uriParts[0]}/${uriParts[1]}'); } } return null; } @override Source? resolveAbsolute(Uri uri) { if (_sourceCache.containsKey(uri)) { return _sourceCache[uri]; } Source? source; var fullFilePath = resolveAbsolutePath(uri); if (fullFilePath != null) { var file = _workspace.findFile(fullFilePath); if (file != null) { return FileSource(file, uri); } } _sourceCache[uri] = source; return source; } String? resolveAbsolutePath(Uri uri) { if (uri.isScheme('file')) { var path = fileUriToNormalizedPath(_context, uri); var pathRelativeToRoot = _workspace._relativeToRoot(path); if (pathRelativeToRoot == null) return null; return _context.join(_workspace.root, pathRelativeToRoot); } if (!uri.isScheme('package')) { return null; } String uriPath = Uri.decodeComponent(uri.path); int slash = uriPath.indexOf('/'); // If the path either starts with a slash or has no slash, it is invalid. if (slash < 1) { return null; } if (uriPath.contains('//') || uriPath.contains('..')) { return null; } String packageName = uriPath.substring(0, slash); String fileUriPart = uriPath.substring(slash + 1); if (fileUriPart.isEmpty) { return null; } String filePath = fileUriPart.replaceAll('/', _context.separator); if (!packageName.contains('.')) { return _context.join( _workspace.root, 'third_party', 'dart', packageName, 'lib', filePath, ); } else { String packagePath = packageName.replaceAll('.', _context.separator); return _context.join(_workspace.root, packagePath, 'lib', filePath); } } /// Restore [filePath] to its 'package:' URI parts. /// /// Returns `null` if [root] is null or if [filePath] is not within [root]. List? _restoreUriParts(String? root, String filePath) { path.Context context = _workspace.provider.pathContext; if (root != null && context.isWithin(root, filePath)) { String relative = context.relative(filePath, from: root); List components = context.split(relative); if (components.length > 4 && components[0] == 'third_party' && components[1] == 'dart' && components[3] == 'lib') { String packageName = components[2]; String pathInLib = components.skip(4).join('/'); return [packageName, pathInLib]; } else { for (int i = components.length - 2; i >= 2; i--) { String component = components[i]; if (component == 'lib') { String packageName = components.getRange(0, i).join('.'); String pathInLib = components.skip(i + 1).join('/'); return [packageName, pathInLib]; } } } } return null; } } /// Information about a Blaze workspace. class BlazeWorkspace extends Workspace implements WorkspaceWithDefaultAnalysisOptions { /// The name of the file that identifies a set of Blaze Targets. /// /// For Dart package purposes, a BUILD file identifies a package. static const String _buildFileName = 'BUILD'; final ResourceProvider provider; /// The absolute workspace root path. /// /// It contains the `WORKSPACE` file or its parent contains the `READONLY` /// folder. @override final String root; /// The absolute paths to all `blaze-bin` folders. /// /// In practice, there is usually one "bin" path, and sometimes there are two, /// on distributed build systems. It is very rare to have more than two. final List binPaths; /// The absolute path to the `blaze-genfiles` folder. final String genfiles; /// Sometimes `BUILD` files are not preserved, and `xyz.packages` files /// are created instead. But looking for them is expensive, so we want /// to avoid this in cases when `BUILD` files are always available. final bool _lookForBuildFileSubstitutes; /// If `true`, the language version for packages will be provided. /// /// If `false`, [BlazeWorkspacePackage.languageVersion] will return `null`, /// so that [Packages] will be used to get the language versions. /// /// See https://buganizer.corp.google.com/issues/279909837 final bool _provideLanguageVersion; /// The language version for this workspace, `null` if cannot be read. final Version? _languageVersion; /// The cache of packages. The key is the directory path, the value is /// the corresponding package. final Map _directoryToPackage = {}; final _blazeCandidateFiles = StreamController.broadcast(); BlazeWorkspace._( this.provider, this.root, this.binPaths, this.genfiles, { required bool lookForBuildFileSubstitutes, required bool provideLanguageVersion, }) : _lookForBuildFileSubstitutes = lookForBuildFileSubstitutes, _provideLanguageVersion = provideLanguageVersion, _languageVersion = provideLanguageVersion ? _readLanguageVersion(provider, root) : null; /// Stream of files that we tried to find along with their potential or actual /// paths. Stream get blazeCandidateFiles => _blazeCandidateFiles.stream; @override bool get isBlaze => true; @override BlazePackageUriResolver get packageUriResolver => BlazePackageUriResolver(this); @override SourceFactory createSourceFactory( DartSdk? sdk, SummaryDataStore? summaryData, ) { List resolvers = []; if (sdk != null) { resolvers.add(DartUriResolver(sdk)); } resolvers.add(packageUriResolver); resolvers.add(BlazeFileUriResolver(this)); if (summaryData != null) { resolvers.add(InSummaryUriResolver(summaryData)); } return SourceFactory(resolvers); } /// Return the file with the given [absolutePath], looking first into /// directories for generated files: `blaze-bin` and `blaze-genfiles`, and /// then into the workspace root. The file in the workspace root is returned /// even if it does not exist. Return `null` if the given [absolutePath] is /// not in the workspace [root]. File? findFile(String absolutePath) { path.Context context = provider.pathContext; try { String relative = context.relative(absolutePath, from: root); if (relative == '.') { return null; } // First check genfiles and bin directories. Note that we always use the // symlinks and not the [binPaths] or [genfiles] to make sure we use the // files corresponding to the most recent build configuration and get // consistent view of all the generated files. var generatedCandidates = [ 'blaze-genfiles', 'blaze-bin', ].map((prefix) => context.join(root, context.join(prefix, relative))); for (var path in generatedCandidates) { File file = provider.getFile(path); if (file.exists) { _blazeCandidateFiles.add( BlazeSearchInfo(relative, generatedCandidates.toList()), ); return file; } } // Writable File writableFile = provider.getFile(absolutePath); if (writableFile.exists) { return writableFile; } // If we couldn't find the file, assume that it has not yet been // generated, so send an event with all the paths that we tried. _blazeCandidateFiles.add( BlazeSearchInfo(relative, generatedCandidates.toList()), ); // Not generated, return the default one. return writableFile; } catch (_) { return null; } } @override BlazeWorkspacePackage? findPackageFor(String filePath) { path.Context context = provider.pathContext; var directoryPath = context.dirname(filePath); var cachedPackage = _directoryToPackage[directoryPath]; if (cachedPackage != null) { return cachedPackage; } if (!context.isWithin(root, directoryPath)) { return null; } // Handle files which are given with their location in "blaze-bin", etc. // This does not typically happen during usual analysis, but it still could, // and it can come up in tests. for (var binPath in [genfiles, ...binPaths]) { if (context.isWithin(binPath, directoryPath)) { return findPackageFor( context.join(root, context.relative(filePath, from: binPath)), ); } } /// Return the package rooted at [folder]. BlazeWorkspacePackage? packageRootedAt(Folder folder) { var uriParts = packageUriResolver._restoreUriParts( root, '${folder.path}/lib/__fake__.dart', ); String? packageName; if (uriParts != null && uriParts.isNotEmpty) { packageName = uriParts[0]; } // TODO(srawlins): If [packageName] could not be derived from [uriParts], // I imagine this should throw. if (packageName == null) { return null; } var package = BlazeWorkspacePackage(packageName, folder, this); _directoryToPackage[directoryPath] = package; return package; } var startFolder = provider.getFolder(directoryPath); for (var folder in startFolder.withAncestors) { if (folder.path.length < root.length) { // We've walked up outside of [root], so [path] is definitely not // defined in any package in this workspace. return null; } if (folder.getChildAssumingFile(_buildFileName).exists) { // Found the BUILD file, denoting a Dart package. return packageRootedAt(folder); } if (_hasBuildFileSubstitute(folder)) { return packageRootedAt(folder); } } return null; } /// In some distributed build environments, BUILD files are not preserved. /// We can still look for a ".packages" file in order to determine a /// package's root. A ".packages" file found in [folder]'s sister path /// under a "bin" path among [binPaths] denotes a Dart package. /// /// For example, if the [root] of this BlazeWorkspace is /// "/build/work/abc123/workspace" with two "bin" folders, /// "/build/work/abc123/workspace/blaze-out/host/bin/" and /// "/build/work/abc123/workspace/blaze-out/k8-opt/bin/", and [folder] /// is at "/build/work/abc123/workspace/foo/bar", then we must look for a /// file ending in ".packages" in the folders /// "/build/work/abc123/workspace/blaze-out/host/bin/foo/bar" and /// "/build/work/abc123/workspace/blaze-out/k8-opt/bin/foo/bar". bool _hasBuildFileSubstitute(Folder folder) { if (!_lookForBuildFileSubstitutes) { return false; } path.Context context = provider.pathContext; // [folder]'s path, relative to [root]. For example, "foo/bar". String relative = context.relative(folder.path, from: root); for (String bin in binPaths) { Folder binChild = provider.getFolder( context.normalize(context.join(bin, relative)), ); if (binChild.exists && binChild.getChildren().any((c) => c.path.endsWith('.packages'))) { // [folder]'s sister folder within [bin] contains a ".packages" file. return true; } } return false; } String? _relativeToRoot(String p) { path.Context context = provider.pathContext; // genfiles if (context.isWithin(genfiles, p)) { return context.relative(p, from: genfiles); } // bin for (String bin in binPaths) { if (context.isWithin(bin, p)) { return context.relative(p, from: bin); } } // Not generated if (context.isWithin(root, p)) { return context.relative(p, from: root); } // Failed reverse lookup return null; } /// Find the Blaze workspace that contains the given [filePath]. /// /// This method walks up the file system from [filePath], looking for various /// "marker" files which indicate a Blaze workspace. /// /// At each folder _f_ with parent _p_, starting with [filePath]: /// /// * If _f_ has a sibling folder named "READONLY", and that folder has a /// child folder with the same name as _f_, then a [BlazeWorkspace] rooted at /// _f_ is returned. /// * If _f_ has a child folder named "blaze-out", then a [BlazeWorkspace] /// rooted at _f_ is returned. /// * If _f_ has a child file named [file_paths.blazeWorkspaceMarker], then /// a [BlazeWorkspace] rooted at _f_ is returned. static BlazeWorkspace? find( ResourceProvider provider, String filePath, { bool lookForBuildFileSubstitutes = true, bool provideLanguageVersion = true, }) { var context = provider.pathContext; var startFolder = provider.getFolder(filePath); for (var folder in startFolder.withAncestors) { var parent = folder.parent; var blazeOutFolder = parent.getChildAssumingFolder('blaze-out'); if (blazeOutFolder.exists) { // Found the "out" folder; must be a Blaze workspace. String root = parent.path; var binPaths = _findBinFolderPaths(parent); binPaths = binPaths..add(context.join(root, 'blaze-bin')); return BlazeWorkspace._( provider, root, binPaths, context.join(root, 'blaze-genfiles'), lookForBuildFileSubstitutes: lookForBuildFileSubstitutes, provideLanguageVersion: provideLanguageVersion, ); } // Found the WORKSPACE file, must be a non-git workspace. if (folder.getChildAssumingFile(file_paths.blazeWorkspaceMarker).exists) { String root = folder.path; var binPaths = _findBinFolderPaths(folder); binPaths = binPaths..add(context.join(root, 'blaze-bin')); return BlazeWorkspace._( provider, root, binPaths, context.join(root, 'blaze-genfiles'), lookForBuildFileSubstitutes: lookForBuildFileSubstitutes, provideLanguageVersion: provideLanguageVersion, ); } } return null; } /// Create the Blaze workspace with the given [root]. /// /// This method should be used during build in google3, when we know for /// sure the workspace root, but [file_paths.blazeWorkspaceMarker] is not /// available. static BlazeWorkspace forBuild({required Folder root}) { var provider = root.provider; var blazeBin = root.getChildAssumingFolder('blaze-bin'); var blazeGenfiles = root.getChildAssumingFolder('blaze-genfiles'); var binPaths = _findBinFolderPaths(root); binPaths.add(blazeBin.path); return BlazeWorkspace._( provider, root.path, binPaths, blazeGenfiles.path, lookForBuildFileSubstitutes: true, provideLanguageVersion: true, ); } /// Find the "bin" folder path, by searching for it. /// /// Depending on the environment we're working in (source code tree, build /// environment subtree of sources, local workspace, blaze), the "bin" /// folder may be available at a symlink found at `$root/blaze-bin/`. If that /// symlink is not available, then we must search the immediate folders found /// in `$root/blaze-out/` for folders named "bin". /// /// If no "bin" folder is found in any of those locations, empty list is /// returned. static List _findBinFolderPaths(Folder root) { var out = root.getChildAssumingFolder('blaze-out'); if (!out.exists) { return []; } List binPaths = []; for (var child in out.getChildren().whereType()) { // Children are folders denoting architectures and build flags, like // 'k8-opt', 'k8-fastbuild', perhaps 'host'. Folder possibleBin = child.getChildAssumingFolder('bin'); if (possibleBin.exists) { binPaths.add(possibleBin.path); } } return binPaths; } /// Return the default language version of the workspace. /// /// Return `null` if cannot be read, for example because the file does not /// exist, or is not available in this build configuration (batch analysis). static Version? _readLanguageVersion( ResourceProvider resourceProvider, String rootPath, ) { var file = resourceProvider.getFile( resourceProvider.pathContext.joinAll([ rootPath, 'dart', 'build_defs', 'bzl', 'language.bzl', ]), ); String content; try { content = file.readAsStringSync(); } on FileSystemException { return null; } var pattern = RegExp(r'(^|\s+)_version\s*=\s*"(\d+\.\d+)"'); for (var match in pattern.allMatches(content)) { return Version.parse('${match.group(2)}.0'); } return null; } } /// Information about a package defined in a [BlazeWorkspace]. /// /// Separate from [Packages] or package maps, this class is designed to simply /// understand whether arbitrary file paths represent libraries declared within /// a given package in a [BlazeWorkspace]. class BlazeWorkspacePackage extends WorkspacePackageImpl { /// A prefix for any URI of a path in this package. final String _uriPrefix; @override final Folder root; @override final BlazeWorkspace workspace; List? _enabledExperiments; Version? _languageVersion; BlazeWorkspacePackage(String packageName, this.root, this.workspace) : _uriPrefix = 'package:$packageName/'; @override List? get enabledExperiments { return _enabledExperiments; } @override Version? get languageVersion { if (!workspace._provideLanguageVersion) { return null; } return _languageVersion ?? workspace._languageVersion; } @override bool contains(Source source) { var uri = source.uri; if (uri.isScheme('package')) { return uri.toString().startsWith(_uriPrefix); } var path = source.fullName; return workspace.findPackageFor(path)?.root == root; } @override bool isInTestDirectory(File file) { // If the package itself is a "testing" package, then [file] counts as being // "in a test directory." return root.shortName == 'testing' || root.path.contains('/testing/') || root.getChildAssumingFolder('test').contains(file.path); } @override // TODO(brianwilkerson): Implement this by looking in the BUILD file for 'deps' // lists. Packages packagesAvailableTo(String libraryPath) => Packages.empty; @override bool sourceIsInPublicApi(Source source) { var filePath = filePathFromSource(source); if (filePath == null) return false; var libFolder = root.getChildAssumingFolder('lib'); if (libFolder.contains(filePath)) { // A file in "$root/lib" is public iff it is not in "$root/lib/src". var libSrcFolder = libFolder.getChildAssumingFolder('src'); return !libSrcFolder.contains(filePath); } var relativeRoot = workspace.provider.pathContext.relative( root.path, from: workspace.root, ); for (var binPath in workspace.binPaths) { Folder bin = workspace.provider.getFolder(binPath); libFolder = bin .getChildAssumingFolder(relativeRoot) .getChildAssumingFolder('lib'); if (libFolder.contains(filePath)) { // A file in "$bin/lib" is public iff it is not in "$bin/lib/src". var libSrcFolder = libFolder.getChildAssumingFolder('src'); return !libSrcFolder.contains(filePath); } } libFolder = workspace.provider .getFolder(workspace.genfiles) .getChildAssumingFolder(relativeRoot) .getChildAssumingFolder('lib'); if (libFolder.contains(filePath)) { // A file in "$genfiles/lib" is public iff it is not in // "$genfiles/lib/src". var libSrcFolder = libFolder.getChildAssumingFolder('src'); return !libSrcFolder.contains(filePath); } return false; } }