| // 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 'dart:core'; |
| |
| 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/generated/source_io.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/uri.dart'; |
| import 'package:analyzer/src/workspace/pub.dart'; |
| import 'package:analyzer/src/workspace/workspace.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:yaml/yaml.dart'; |
| |
| /// 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 PackageBuildWorkspace workspace; |
| |
| PackageBuildFileUriResolver(PackageBuildWorkspace workspace) |
| : workspace = workspace, |
| super(workspace.provider); |
| |
| @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 PackageBuildWorkspace _workspace; |
| final UriResolver _normalUriResolver; |
| final path.Context _context; |
| |
| PackageBuildPackageUriResolver( |
| PackageBuildWorkspace workspace, this._normalUriResolver) |
| : _workspace = workspace, |
| _context = workspace.provider.pathContext; |
| |
| Map<String, List<Folder>> get packageMap => _workspace.packageMap; |
| |
| @override |
| Source? resolveAbsolute(Uri uri) { |
| if (uri.scheme != '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; |
| } |
| |
| @override |
| Uri? restoreAbsolute(Source source) { |
| String filePath = source.fullName; |
| |
| if (_context.isWithin(_workspace.root, filePath)) { |
| var uriParts = _restoreUriParts(filePath); |
| if (uriParts != null) { |
| return Uri.parse('package:${uriParts[0]}/${uriParts[1]}'); |
| } |
| } |
| |
| return _normalUriResolver.restoreAbsolute(source); |
| } |
| |
| 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:build workspace. |
| class PackageBuildWorkspace extends Workspace implements PubWorkspace { |
| /// The name of the directory that identifies the root of the workspace. Note, |
| /// the presence of this file does not show package:build is used. For that, |
| /// the subdirectory [_dartToolBuildName] must exist. A `pub` subdirectory |
| /// will usually exist in non-package:build projects too. |
| static const String _dartToolRootName = '.dart_tool'; |
| |
| /// The name of the subdirectory in [_dartToolName] that distinguishes |
| /// projects built with package:build. |
| static const String _dartToolBuildName = 'build'; |
| |
| static const List<String> _generatedPathParts = [ |
| '.dart_tool', |
| 'build', |
| 'generated' |
| ]; |
| |
| /// We use pubspec.yaml to get the package name to be consistent with how |
| /// package:build does it. |
| static const String _pubspecName = 'pubspec.yaml'; |
| |
| /// 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; |
| |
| /// The map from a package name to the list of its `lib/` folders. |
| @override |
| final Map<String, List<Folder>> packageMap; |
| |
| /// The resource provider used to access the file system. |
| @override |
| final ResourceProvider provider; |
| |
| /// The absolute workspace root path (the directory containing the |
| /// `.dart_tool` directory). |
| @override |
| final String root; |
| |
| /// The name of the package under development as defined in pubspec.yaml. This |
| /// matches the behavior of package:build. |
| final String projectPackageName; |
| |
| /// `.dart_tool/build/generated` in [root]. |
| final String generatedRootPath; |
| |
| /// [projectPackageName] in [generatedRootPath]. |
| final String generatedThisPath; |
| |
| /// The singular package in this workspace. |
| /// |
| /// Each "package:build" workspace is itself one package. |
| late final PackageBuildWorkspacePackage _theOnlyPackage; |
| |
| PackageBuildWorkspace._( |
| this.provider, |
| this.packageMap, |
| this.root, |
| this.projectPackageName, |
| this.generatedRootPath, |
| this.generatedThisPath, |
| File pubspecFile, |
| ) : _pubspecFile = pubspecFile, |
| _pubspecContent = _fileContentOrNull(pubspecFile) { |
| _theOnlyPackage = PackageBuildWorkspacePackage(root, this); |
| } |
| |
| @override |
| bool get isConsistentWithFileSystem { |
| return _fileContentOrNull(_pubspecFile) == _pubspecContent; |
| } |
| |
| @override |
| UriResolver get packageUriResolver => 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. |
| File? builtFile(String builtPath, String packageName) { |
| if (!packageMap.containsKey(packageName)) { |
| return null; |
| } |
| path.Context context = provider.pathContext; |
| String fullBuiltPath = context.normalize(context.join( |
| root, _dartToolRootName, 'build', 'generated', packageName, builtPath)); |
| return provider.getFile(fullBuiltPath); |
| } |
| |
| /// 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) { |
| path.Context context = provider.pathContext; |
| assert(context.isRelative(filePath), 'Not a relative path: $filePath'); |
| return context.join('lib', filePath); |
| } |
| |
| @internal |
| @override |
| void contributeToResolutionSalt(ApiSignature buffer) { |
| buffer.addString(_pubspecContent ?? ''); |
| } |
| |
| @override |
| SourceFactory createSourceFactory( |
| DartSdk? sdk, |
| SummaryDataStore? summaryData, |
| ) { |
| if (summaryData != null) { |
| throw UnsupportedError( |
| 'Summary files are not supported in a package:build workspace.'); |
| } |
| List<UriResolver> resolvers = <UriResolver>[]; |
| if (sdk != null) { |
| resolvers.add(DartUriResolver(sdk)); |
| } |
| resolvers.add(packageUriResolver); |
| resolvers.add(PackageBuildFileUriResolver(this)); |
| 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) { |
| path.Context context = provider.pathContext; |
| assert(context.isAbsolute(filePath), 'Not an absolute path: $filePath'); |
| try { |
| final String relativePath = context.relative(filePath, from: root); |
| final file = builtFile(relativePath, projectPackageName); |
| |
| if (file!.exists) { |
| return file; |
| } |
| |
| return provider.getFile(filePath); |
| } catch (_) { |
| return null; |
| } |
| } |
| |
| @override |
| PackageBuildWorkspacePackage? findPackageFor(String path) { |
| var pathContext = provider.pathContext; |
| |
| // Must be in this workspace. |
| if (!pathContext.isWithin(root, path)) { |
| return null; |
| } |
| |
| // If generated, must be for this package. |
| if (pathContext.isWithin(generatedRootPath, path)) { |
| if (!pathContext.isWithin(generatedThisPath, path)) { |
| return null; |
| } |
| } |
| |
| return _theOnlyPackage; |
| } |
| |
| /// Find the package:build workspace that contains the given [filePath]. |
| /// |
| /// Return `null` if the filePath is not in a package:build workspace. |
| static PackageBuildWorkspace? find(ResourceProvider provider, |
| Map<String, List<Folder>> packageMap, String filePath) { |
| var startFolder = provider.getFolder(filePath); |
| for (var folder in startFolder.withAncestors) { |
| final File pubspec = folder.getChildAssumingFile(_pubspecName); |
| final Folder dartToolDir = |
| folder.getChildAssumingFolder(_dartToolRootName); |
| final Folder dartToolBuildDir = |
| dartToolDir.getChildAssumingFolder(_dartToolBuildName); |
| |
| // Found the .dart_tool file, that's our project root. We also require a |
| // pubspec, to know the package name that package:build will assume. |
| if (dartToolBuildDir.exists && pubspec.exists) { |
| try { |
| final yaml = loadYaml(pubspec.readAsStringSync()) as YamlMap; |
| final packageName = yaml['name'] as String; |
| final generatedRootPath = provider.pathContext |
| .joinAll([folder.path, ..._generatedPathParts]); |
| final generatedThisPath = |
| provider.pathContext.join(generatedRootPath, packageName); |
| return PackageBuildWorkspace._(provider, packageMap, folder.path, |
| packageName, generatedRootPath, generatedThisPath, pubspec); |
| } catch (_) { |
| return null; |
| } |
| } |
| |
| // We found `pubspec.yaml`, but not `.dart_tool/build`. |
| // Stop going up, this package does not have package:build results. |
| // We don't want to find results of a parent package. |
| if (pubspec.exists) { |
| return null; |
| } |
| } |
| } |
| |
| /// Return the content of the [file], `null` if cannot be read. |
| static String? _fileContentOrNull(File file) { |
| try { |
| return file.readAsStringSync(); |
| } catch (_) {} |
| } |
| } |
| |
| /// Information about a package defined in a PackageBuildWorkspace. |
| /// |
| /// 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 PackageBuildWorkspace. |
| class PackageBuildWorkspacePackage extends WorkspacePackage |
| implements PubWorkspacePackage { |
| @override |
| late final Pubspec? pubspec = () { |
| final content = workspace._pubspecContent; |
| if (content != null) { |
| return Pubspec.parse(content); |
| } |
| }(); |
| |
| @override |
| final String root; |
| |
| @override |
| final PackageBuildWorkspace workspace; |
| |
| PackageBuildWorkspacePackage(this.root, this.workspace); |
| |
| @override |
| bool contains(Source source) { |
| var uri = source.uri; |
| |
| if (uri.isScheme('package')) { |
| var packageName = uri.pathSegments[0]; |
| return packageName == workspace.projectPackageName; |
| } |
| |
| if (uri.isScheme('file')) { |
| var path = source.fullName; |
| return workspace.findPackageFor(path) != null; |
| } |
| |
| return false; |
| } |
| |
| @override |
| Map<String, List<Folder>> packagesAvailableTo(String libraryPath) => |
| workspace.packageMap; |
| |
| @override |
| 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, ...PackageBuildWorkspace._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; |
| } |
| } |