blob: b3e42d6263ad3360cf3539dff0c422dc9acdb687 [file] [log] [blame]
// Copyright (c) 2019, 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/src/context/packages.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/lint/pub.dart';
import 'package:analyzer/src/source/package_map_resolver.dart';
import 'package:analyzer/src/summary/api_signature.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/simple.dart';
import 'package:analyzer/src/workspace/workspace.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart';
/// Check if the given list of path components contains a package build
/// generated directory, it would have the following path segments,
/// '.dart_tool/build/generated'.
bool _isPackageBuildGeneratedPath(List<String> pathComponents, int startIndex) {
if (pathComponents.length > startIndex + 2) {
if (pathComponents[startIndex] == file_paths.dotDartTool &&
pathComponents[startIndex + 1] == file_paths.packageBuild &&
pathComponents[startIndex + 2] == file_paths.packageBuildGenerated) {
return true;
}
}
return false;
}
/// Instances of the class `PackageBuildFileUriResolver` resolve `file` URI's by
/// first resolving file uri's in the expected way, and then by looking in the
/// corresponding generated directories.
class PackageBuildFileUriResolver extends ResourceUriResolver {
final PubWorkspace workspace;
PackageBuildFileUriResolver(this.workspace) : super(workspace.provider);
@override
Uri pathToUri(String path) {
var pathContext = workspace.provider.pathContext;
var package = workspace.findPackageFor(path);
if (package != null) {
if (pathContext.isWithin(package.root, path)) {
var relative = pathContext.relative(path, from: package.root);
var components = pathContext.split(relative);
if (components.length > 4 &&
_isPackageBuildGeneratedPath(components, 0) &&
components[3] == package._name) {
var canonicalPath = pathContext.joinAll([
workspace.root,
...components.skip(4),
]);
return pathContext.toUri(canonicalPath);
}
}
}
return super.pathToUri(path);
}
@override
Source? resolveAbsolute(Uri uri) {
if (!ResourceUriResolver.isFileUri(uri)) {
return null;
}
String filePath = fileUriToNormalizedPath(provider.pathContext, uri);
Resource resource = provider.getResource(filePath);
if (resource is! File) {
return null;
}
var file = workspace.findFile(filePath);
if (file != null) {
return file.createSource(uri);
}
return null;
}
}
/// The [UriResolver] that can resolve `package` URIs in
/// [PackageBuildWorkspace].
class PackageBuildPackageUriResolver extends UriResolver {
final PubWorkspace _workspace;
final UriResolver _normalUriResolver;
final Context _context;
PackageBuildPackageUriResolver(
PubWorkspace workspace, this._normalUriResolver)
: _workspace = workspace,
_context = workspace.provider.pathContext;
// TODO(scheglov): Finish switching to [Packages].
Map<String, List<Folder>> get packageMap => _workspace.packageMap;
@override
Uri? pathToUri(String path) {
if (_context.isWithin(_workspace.root, path)) {
var uriParts = _restoreUriParts(path);
if (uriParts != null) {
return uriCache.parse('package:${uriParts[0]}/${uriParts[1]}');
}
}
return _normalUriResolver.pathToUri(path);
}
@override
Source? resolveAbsolute(Uri uri) {
if (!uri.isScheme('package')) {
return null;
}
var basicResolverSource = _normalUriResolver.resolveAbsolute(uri);
if (basicResolverSource != null && basicResolverSource.exists()) {
return basicResolverSource;
}
String uriPath = 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;
}
String packageName = uriPath.substring(0, slash);
String fileUriPart = uriPath.substring(slash + 1);
String filePath = fileUriPart.replaceAll('/', _context.separator);
var file = _workspace.builtFile(
_workspace._builtPackageSourcePath(filePath), packageName);
if (file != null && file.exists) {
return file.createSource(uri);
}
return basicResolverSource;
}
List<String>? _restoreUriParts(String filePath) {
String relative = _context.relative(filePath, from: _workspace.root);
List<String> components = _context.split(relative);
if (components.length > 5 &&
components[0] == '.dart_tool' &&
components[1] == 'build' &&
components[2] == 'generated' &&
components[4] == 'lib') {
String packageName = components[3];
String pathInLib = components.skip(5).join('/');
return [packageName, pathInLib];
}
return null;
}
}
/// Information about a Pub workspace.
class PubWorkspace extends SimpleWorkspace {
/// A Pub workspace can have just one package or multiple packages. The
/// single package in the workspace is the _rootPackage.
/// A Pub workspace contains a single package when both the pubspec.yaml and
/// a package_config.json files are present at the root.
late final PubWorkspacePackage _rootPackage;
/// A map of paths to packages defined in a [PubWorkspace]. This map is
/// populated when there are multiple packages in a workspace.
final Map<Folder, PubWorkspacePackage> containedPackages = {};
/// The associated pubspec file.
final File _pubspecFile;
/// The content of the `pubspec.yaml` file.
/// We read it once, so that all usages return consistent results.
final String? _pubspecContent;
PubWorkspace._(
ResourceProvider provider,
Packages packages,
String root,
this._pubspecFile,
this._pubspecContent,
) : super(provider, packages, root) {
_rootPackage = PubWorkspacePackage(root, this, _pubspecFile);
}
@override
bool get isConsistentWithFileSystem {
return _fileContentOrNull(_pubspecFile) == _pubspecContent;
}
@override
UriResolver get packageUriResolver {
return PackageBuildPackageUriResolver(
this, PackageMapUriResolver(provider, packageMap));
}
/// For some package file, which may or may not be a package source (it could
/// be in `bin/`, `web/`, etc), find where its built counterpart will exist if
/// its a generated source.
///
/// To get a [builtPath] for a package source file to use in this method,
/// use [_builtPackageSourcePath]. For `bin/`, `web/`, etc, it must be relative
/// to the project root.
@visibleForTesting
File? builtFile(String builtPath, String packageName) {
if (packages[packageName] == null) {
return null;
}
var context = provider.pathContext;
var fullBuiltPath = context.normalize(context.join(
root,
file_paths.dotDartTool,
file_paths.packageBuild,
file_paths.packageBuildGenerated,
packageName,
builtPath));
return provider.getFile(fullBuiltPath);
}
@internal
@override
void contributeToResolutionSalt(ApiSignature buffer) {
buffer.addString(_pubspecContent ?? '');
}
@override
SourceFactory createSourceFactory(
DartSdk? sdk,
SummaryDataStore? summaryData,
) {
var resolvers = <UriResolver>[];
if (sdk != null) {
resolvers.add(DartUriResolver(sdk));
}
resolvers.add(packageUriResolver);
resolvers.add(PackageBuildFileUriResolver(this));
resolvers.add(ResourceUriResolver(provider));
return SourceFactory(resolvers);
}
/// Return the file with the given [filePath], looking first in the generated
/// directory `.dart_tool/build/generated/$projectPackageName/`, then in
/// source directories.
///
/// The file in the workspace [root] is returned even if it does not exist.
/// Return `null` if the given [filePath] is not in the workspace root.
File? findFile(String filePath) {
var context = provider.pathContext;
assert(context.isAbsolute(filePath), 'Not an absolute path: $filePath');
try {
var package = findPackageFor(filePath);
if (package != null) {
final relativePath = context.relative(filePath, from: package.root);
final file = builtFile(relativePath, package._name);
if (file == null) {
return null;
}
if (file.exists) {
return file;
}
}
return provider.getFile(filePath);
} catch (_) {
return null;
}
}
/// Find the [PubWorkspacePackage] that contains the given file path. The path
/// can be for a source file or a generated file. Generated files are located
/// in the '.dart_tool/build/generated' folder of the containing package.
@override
PubWorkspacePackage? findPackageFor(String filePath) {
var pathContext = provider.pathContext;
// Must be in this workspace.
if (!pathContext.isWithin(root, filePath)) {
return null;
}
// If given path is for a generated file, check to make sure it is in the
// generated directory and package name are the same.
// For eg. roots are not the same here.
// 'workspace/my/.dart_tool/build/generated/foo/lib/a.dart'
var segments = pathContext.split(filePath);
var buildRootIndex = segments.indexOf(file_paths.dotDartTool);
if (buildRootIndex != -1) {
if (_isPackageBuildGeneratedPath(segments, buildRootIndex)) {
var packageName = segments[buildRootIndex - 1];
if (packageName != segments[buildRootIndex + 3]) {
return null;
}
}
}
PubWorkspacePackage? result;
int resultPathLength = 1 << 20;
for (var package in containedPackages.entries) {
if (pathContext.isWithin(package.key.path, filePath)) {
var packagePathLength = package.key.path.length;
if (result == null || resultPathLength < packagePathLength) {
result = package.value;
resultPathLength = packagePathLength;
}
}
}
if (result != null) {
return result;
}
var folder = provider.getFile(filePath).parent;
// Look for pubspec in folder and ancestors.
for (var current in folder.withAncestors) {
if (current.path == root) {
return _rootPackage;
}
var pubspec = current.getChildAssumingFile(file_paths.pubspecYaml);
if (pubspec.exists) {
var package = PubWorkspacePackage(current.path, this, pubspec);
containedPackages[current] = package;
return package;
}
}
return null;
}
/// Unlike the way that sources are resolved against `.packages` (if foo
/// points to folder bar, then `foo:baz.dart` is found at `bar/baz.dart`), the
/// built sources for a package require the `lib/` prefix first. This is
/// because `bin/`, `web/`, and `test/` etc can all be built as well. This
/// method exists to give a name to that prefix processing step.
String _builtPackageSourcePath(String filePath) {
var context = provider.pathContext;
assert(context.isRelative(filePath), 'Not a relative path: $filePath');
return context.join('lib', filePath);
}
/// Find the pub workspace that contains the given [filePath].
/// A [PubWorkspace] is rooted at the innermost pubspec/package-config pair,
/// or if that's not found, then the outermost pubspec.
static PubWorkspace? find(
ResourceProvider provider,
Packages packages,
String filePath,
) {
File? pubspec;
var start = provider.getFolder(filePath);
// Walking up from filePath, look for files named pubspec.yaml.
// When we find one, if there is an accompanying .dart_tool/package_config.json file,
// then we've found the workspace.
// If we never found a package config, then use the highest (closest to the
// root) pubspec we ever found.
for (var current in start.withAncestors) {
var currentPubspec = current.getChildAssumingFile(file_paths.pubspecYaml);
if (currentPubspec.exists) {
if (_isInThirdPartyDart(currentPubspec)) {
return null;
}
// Check for package config file.
var packagesFile = current
.getChildAssumingFolder(file_paths.dotDartTool)
.getChildAssumingFile(file_paths.packageConfigJson);
if (packagesFile.exists) {
var root = current.path;
return PubWorkspace._(
provider,
packages,
root,
currentPubspec,
_fileContentOrNull(currentPubspec),
);
}
pubspec = currentPubspec;
}
}
// We found a pubspec but no package config file.
if (pubspec != null) {
return PubWorkspace._(provider, packages, pubspec.parent.path, pubspec,
_fileContentOrNull(pubspec));
}
return null;
}
/// Return the content of the [file], `null` if cannot be read.
static String? _fileContentOrNull(File file) {
try {
return file.readAsStringSync();
} catch (_) {
return null;
}
}
/// See https://buganizer.corp.google.com/issues/273584249
///
/// Check if `/home/workspace/third_party/dart/my/pubspec.yaml`
/// If so, we are in a Blaze workspace, and should not create Pub.
static bool _isInThirdPartyDart(File pubspec) {
final path = pubspec.path;
final pathContext = pubspec.provider.pathContext;
final pathComponents = pathContext.split(path);
return pathComponents.length > 4 &&
pathComponents[pathComponents.length - 3] == 'dart' &&
pathComponents[pathComponents.length - 4] == 'third_party';
}
}
/// Information about a package defined in a [PubWorkspace].
///
/// 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 [PubWorkspace].
class PubWorkspacePackage extends WorkspacePackage {
static const List<String> _generatedPathParts = [
file_paths.dotDartTool,
file_paths.packageBuild,
file_paths.packageBuildGenerated,
];
@override
final String root;
late final String _name;
late final String? _pubspecContent;
Pubspec? _pubspec;
/// A flag to indicate if we've tried to parse the pubspec.
bool _parsedPubspec = false;
@override
final PubWorkspace workspace;
late final String _generatedThisPath;
PubWorkspacePackage(this.root, this.workspace, File pubspecFile) {
_pubspecContent = PubWorkspace._fileContentOrNull(pubspecFile);
_name = pubspec?.name?.value.text ?? '';
_generatedThisPath = workspace.provider.pathContext
.joinAll([root, ...PubWorkspacePackage._generatedPathParts, _name]);
}
/// Get the associated parsed [Pubspec], or `null` if there was an error in
/// reading or parsing.
Pubspec? get pubspec {
if (!_parsedPubspec) {
_parsedPubspec = true;
final content = _pubspecContent;
if (content != null) {
_pubspec = Pubspec.parse(content);
}
}
return _pubspec;
}
@override
bool contains(Source source) {
var uri = source.uri;
if (uri.isScheme('package')) {
// TODO(keertip): Check to see if we can use information from package
// config to find out if a file is in this package.
var packageName = uri.pathSegments[0];
return _name == packageName;
}
if (uri.isScheme('file')) {
var path = source.fullName;
if (path.contains(file_paths.dotDartTool)) {
return workspace.provider.pathContext
.isWithin(_generatedThisPath, path);
}
return workspace.provider.pathContext.isWithin(root, path);
}
return false;
}
@override
Packages packagesAvailableTo(String libraryPath) {
// TODO(brianwilkerson): Consider differentiating based on whether the
// [libraryPath] is inside the `lib` directory.
return workspace.packages;
}
@override
/// A Pub package's public API consists of libraries found in the top-level
/// "lib" directory, and any subdirectories, excluding the "src" directory
/// just inside the top-level "lib" directory.
bool sourceIsInPublicApi(Source source) {
var filePath = filePathFromSource(source);
if (filePath == null) return false;
var libFolder = workspace.provider.pathContext.join(root, 'lib');
if (workspace.provider.pathContext.isWithin(libFolder, filePath)) {
// A file in "$root/lib" is public iff it is not in "$root/lib/src".
var libSrcFolder = workspace.provider.pathContext.join(libFolder, 'src');
return !workspace.provider.pathContext.isWithin(libSrcFolder, filePath);
}
libFolder = workspace.provider.pathContext
.joinAll([root, ..._generatedPathParts, 'test', 'lib']);
if (workspace.provider.pathContext.isWithin(libFolder, filePath)) {
// A file in "$generated/lib" is public iff it is not in
// "$generated/lib/src".
var libSrcFolder = workspace.provider.pathContext.join(libFolder, 'src');
return !workspace.provider.pathContext.isWithin(libSrcFolder, filePath);
}
return false;
}
}