| // 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/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/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/basic.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'; | |
| import 'package:pub_semver/pub_semver.dart'; | |
| /// Return the content of the [file], `null` if cannot be read. | |
| String? _fileContentOrNull(File file) { | |
| try { | |
| return file.readAsStringSync(); | |
| } catch (_) { | |
| return null; | |
| } | |
| } | |
| /// 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 PackageConfigFileUriResolver extends ResourceUriResolver { | |
| final PackageConfigWorkspace workspace; | |
| PackageConfigFileUriResolver(this.workspace) : super(workspace.provider); | |
| @override | |
| Uri pathToUri(String path) { | |
| var pathContext = workspace.provider.pathContext; | |
| if (pathContext.isWithin(workspace.root, path)) { | |
| var package = workspace.findPackageFor(path); | |
| if (package is PubPackage) { | |
| var relative = pathContext.relative(path, from: workspace.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 FileSource(file, uri); | |
| } | |
| return null; | |
| } | |
| } | |
| /// The [UriResolver] that can resolve `package` URIs in a | |
| /// [PackageConfigWorkspace]. | |
| class PackageConfigPackageUriResolver extends UriResolver { | |
| final PackageConfigWorkspace _workspace; | |
| final UriResolver _normalUriResolver; | |
| final Context _context; | |
| PackageConfigPackageUriResolver( | |
| PackageConfigWorkspace workspace, | |
| this._normalUriResolver, | |
| ) : _workspace = workspace, | |
| _context = workspace.provider.pathContext; | |
| // TODO(scheglov): Finish switching to [Packages]. | |
| Map<String, 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 FileSource(file, 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 Package Config workspace. | |
| class PackageConfigWorkspace extends SimpleWorkspace { | |
| /// The associated package config file. | |
| final File packageConfigFile; | |
| /// The contents of the package config file. | |
| late final String? _packageConfigContent; | |
| /// Indicates whether this is a PubWorkspace as per | |
| /// https://dart.dev/tools/pub/workspaces | |
| bool _isPubWorkspace = false; | |
| final Map<String, WorkspacePackageImpl> _workspacePackages = {}; | |
| factory PackageConfigWorkspace( | |
| ResourceProvider provider, //Packages packages, | |
| String root, | |
| File packageConfigFile, | |
| Packages packages, | |
| ) { | |
| // Use the default packages if there is one. | |
| var newPackages = packages == Packages.empty | |
| ? parsePackageConfigJsonFile(provider, packageConfigFile) | |
| : packages; | |
| return PackageConfigWorkspace._( | |
| provider, | |
| newPackages, | |
| root, | |
| packageConfigFile, | |
| ); | |
| } | |
| PackageConfigWorkspace._( | |
| super.provider, | |
| super.packages, | |
| super.root, | |
| this.packageConfigFile, | |
| ) { | |
| _packageConfigContent = packageConfigFile.readAsStringSync(); | |
| var pubspecFile = provider.getFolder(root).getFile(file_paths.pubspecYaml); | |
| if (pubspecFile.exists) { | |
| var pubspec = Pubspec.parse( | |
| pubspecFile.readAsStringSync(), | |
| resourceProvider: provider, | |
| ); | |
| if (pubspec.workspace != null) { | |
| _isPubWorkspace = true; | |
| } | |
| } | |
| } | |
| Iterable<WorkspacePackageImpl> get allPackages => | |
| _workspacePackages.values.toSet(); | |
| @override | |
| bool get isConsistentWithFileSystem { | |
| return _fileContentOrNull(packageConfigFile) == _packageConfigContent; | |
| } | |
| bool get isPubWorkspace => _isPubWorkspace; | |
| @override | |
| UriResolver get packageUriResolver { | |
| return PackageConfigPackageUriResolver( | |
| 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 | |
| /// it's 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(_packageConfigContent ?? ''); | |
| } | |
| @override | |
| SourceFactory createSourceFactory( | |
| DartSdk? sdk, | |
| SummaryDataStore? summaryData, | |
| ) { | |
| var resolvers = <UriResolver>[]; | |
| if (sdk != null) { | |
| resolvers.add(DartUriResolver(sdk)); | |
| } | |
| if (summaryData != null) { | |
| resolvers.add(InSummaryUriResolver(summaryData)); | |
| } | |
| resolvers.add(packageUriResolver); | |
| resolvers.add(PackageConfigFileUriResolver(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 is PubPackage) { | |
| var relativePath = context.relative(filePath, from: package.root.path); | |
| var file = builtFile(relativePath, package._name ?? ''); | |
| if (file!.exists) { | |
| return file; | |
| } | |
| } | |
| return provider.getFile(filePath); | |
| } catch (_) { | |
| return null; | |
| } | |
| } | |
| /// Find the [PubPackage] 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 | |
| WorkspacePackageImpl? findPackageFor(String filePath) { | |
| var pathContext = provider.pathContext; | |
| // Must be in this workspace. | |
| if (!pathContext.isWithin(root, filePath)) { | |
| return null; | |
| } | |
| List<String> paths = []; | |
| var folder = provider.getFile(filePath).parent; | |
| for (var current in folder.withAncestors) { | |
| var package = _workspacePackages[current.path]; | |
| if (package != null) { | |
| for (var path in paths) { | |
| _workspacePackages[path] = package; | |
| } | |
| return package; | |
| } | |
| var pubspec = current.getFile(file_paths.pubspecYaml); | |
| if (pubspec.exists) { | |
| if (_isInThirdPartyDart(pubspec)) { | |
| return null; | |
| } | |
| var package = PubPackage(current, this, pubspec); | |
| for (var path in paths) { | |
| _workspacePackages[path] = package; | |
| } | |
| _workspacePackages[current.path] = package; | |
| return package; | |
| } | |
| if (current.path == root) { | |
| var package = BasicWorkspacePackage(current, this); | |
| for (var path in paths) { | |
| _workspacePackages[path] = package; | |
| } | |
| return package; | |
| } | |
| paths.add(current.path); | |
| } | |
| 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 package config workspace that contains the given [filePath]. | |
| /// A [PackageConfigWorkspace] is rooted at the innermost package-config file. | |
| static PackageConfigWorkspace? find( | |
| ResourceProvider provider, | |
| Packages packages, | |
| String filePath, | |
| ) { | |
| return PackageConfigWorkspaceFindSession(provider).find( | |
| filePath, | |
| providedPackages: packages == Packages.empty ? null : packages, | |
| ); | |
| } | |
| /// 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) { | |
| var path = pubspec.path; | |
| var pathContext = pubspec.provider.pathContext; | |
| var pathComponents = pathContext.split(path); | |
| return pathComponents.length > 4 && | |
| pathComponents[pathComponents.length - 3] == 'dart' && | |
| pathComponents[pathComponents.length - 4] == 'third_party'; | |
| } | |
| } | |
| /// Finds package-config workspaces during one filesystem discovery operation. | |
| /// | |
| /// A session must not outlive the operation because each cached workspace is a | |
| /// snapshot of its package configuration and pubspec. | |
| final class PackageConfigWorkspaceFindSession { | |
| final ResourceProvider _resourceProvider; | |
| /// Workspaces are configuration snapshots, so reuse them only within this | |
| /// discovery operation. The [Packages] identity distinguishes a mapping | |
| /// supplied by the caller from the mapping parsed from the discovered file. | |
| final Map< | |
| ({File packageConfigFile, Packages? providedPackages}), | |
| PackageConfigWorkspace | |
| > | |
| _workspaces = {}; | |
| PackageConfigWorkspaceFindSession(this._resourceProvider); | |
| /// Finds the package config workspace containing [filePath]. | |
| /// | |
| /// If [providedPackages] is `null`, the discovered package config file is | |
| /// parsed. Otherwise, the supplied package mapping is used. | |
| PackageConfigWorkspace? find(String filePath, {Packages? providedPackages}) { | |
| var start = _resourceProvider.getFolder(filePath); | |
| for (var current in start.withAncestors) { | |
| var packageConfigFile = current | |
| .getFolder(file_paths.dotDartTool) | |
| .getFile(file_paths.packageConfigJson); | |
| if (packageConfigFile.exists) { | |
| var key = ( | |
| packageConfigFile: packageConfigFile, | |
| providedPackages: providedPackages, | |
| ); | |
| return _workspaces.putIfAbsent(key, () { | |
| var packages = | |
| providedPackages ?? | |
| parsePackageConfigJsonFile(_resourceProvider, packageConfigFile); | |
| return PackageConfigWorkspace._( | |
| _resourceProvider, | |
| packages, | |
| current.path, | |
| packageConfigFile, | |
| ); | |
| }); | |
| } | |
| } | |
| return null; | |
| } | |
| } | |
| /// Information about a package defined in a [PackageConfigWorkspace]. | |
| /// | |
| /// 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 [PackageConfigWorkspace]. | |
| class PubPackage extends WorkspacePackageImpl { | |
| static const List<String> _generatedPathParts = [ | |
| file_paths.dotDartTool, | |
| file_paths.packageBuild, | |
| file_paths.packageBuildGenerated, | |
| ]; | |
| @override | |
| final Folder root; | |
| final String? _name; | |
| final String? pubspecContent; | |
| final Pubspec? pubspec; | |
| final File pubspecFile; | |
| VersionConstraint? _sdkVersionConstraint; | |
| /// A flag to indicate if we've tried to parse the sdk constraint. | |
| bool _parsedSdkConstraint = false; | |
| @override | |
| final PackageConfigWorkspace workspace; | |
| factory PubPackage( | |
| Folder root, | |
| PackageConfigWorkspace workspace, | |
| File pubspecFile, | |
| ) { | |
| var pubspecContent = pubspecFile.readAsStringSync(); | |
| var pubspec = Pubspec.parse( | |
| pubspecContent, | |
| resourceProvider: pubspecFile.provider, | |
| ); | |
| var packageName = pubspec.name?.value.text; | |
| return PubPackage._( | |
| root, | |
| workspace, | |
| pubspecContent, | |
| pubspecFile, | |
| pubspec, | |
| packageName, | |
| ); | |
| } | |
| PubPackage._( | |
| this.root, | |
| this.workspace, | |
| this.pubspecContent, | |
| this.pubspecFile, | |
| this.pubspec, | |
| this._name, | |
| ); | |
| /// The version range for the SDK specified for this package , or `null` if | |
| /// it is ill-formatted or not set. | |
| VersionConstraint? get sdkVersionConstraint { | |
| if (!_parsedSdkConstraint) { | |
| _parsedSdkConstraint = true; | |
| var sdkValue = pubspec?.environment?.sdk?.value.text; | |
| if (sdkValue != null) { | |
| try { | |
| _sdkVersionConstraint = VersionConstraint.parse(sdkValue); | |
| } catch (_) { | |
| // Ill-formatted constraints, default to a `null` value. | |
| } | |
| } | |
| } | |
| return _sdkVersionConstraint; | |
| } | |
| Folder? get _generatedPackageRoot { | |
| var packageName = _name; | |
| if (packageName == null) { | |
| return null; | |
| } | |
| var generatedRoot = _generatedPathParts.fold(root, (current, segment) { | |
| return current.getFolder(segment); | |
| }); | |
| return generatedRoot.getFolder(packageName); | |
| } | |
| @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 packageName == _name; | |
| } | |
| if (uri.isScheme('file')) { | |
| var path = source.fullName; | |
| return workspace.findPackageFor(path) != null; | |
| } | |
| return false; | |
| } | |
| @override | |
| bool isInTestDirectory(File file) { | |
| if (super.isInTestDirectory(file)) { | |
| return true; | |
| } | |
| var generatedPackageRoot = _generatedPackageRoot; | |
| return generatedPackageRoot != null && | |
| isInTestDirectoryUnder(generatedPackageRoot, file); | |
| } | |
| @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; | |
| bool sourceIsInPublicApiUnder(Folder root, String filePath) { | |
| var libFolder = root.getFolder('lib'); | |
| if (!libFolder.contains(filePath)) { | |
| return false; | |
| } | |
| // A file in "$root/lib" is public iff it is not in "$root/lib/src". | |
| var libSrcFolder = libFolder.getFolder('src'); | |
| return !libSrcFolder.contains(filePath); | |
| } | |
| if (sourceIsInPublicApiUnder(root, filePath)) { | |
| return true; | |
| } | |
| var generatedPackageRoot = _generatedPackageRoot; | |
| return generatedPackageRoot != null && | |
| sourceIsInPublicApiUnder(generatedPackageRoot, filePath); | |
| } | |
| } |