blob: 62579610c8d34249ad2bd80d829747f0a4e19cda [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 'errors.dart';
import "package_config.dart";
import "util.dart";
export "package_config.dart";
class SimplePackageConfig implements PackageConfig {
final int version;
final Map<String, Package> _packages;
final PackageTree _packageTree;
final dynamic extraData;
SimplePackageConfig(int version, Iterable<Package> packages,
[dynamic extraData])
: this._(_validateVersion(version), packages,
[...packages]..sort(_compareRoot), extraData);
/// Expects a list of [packages] sorted on root path.
SimplePackageConfig._(this.version, Iterable<Package> originalPackages,
List<Package> packages, this.extraData)
: _packageTree = _validatePackages(originalPackages, packages),
_packages = {for (var p in packages) p.name: p};
/// Creates empty configuration.
///
/// The empty configuration can be used in cases where no configuration is
/// found, but code expects a non-null configuration.
const SimplePackageConfig.empty()
: version = 1,
_packageTree = const EmptyPackageTree(),
_packages = const <String, Package>{},
extraData = null;
static int _validateVersion(int version) {
if (version < 0 || version > PackageConfig.maxVersion) {
throw PackageConfigArgumentError(version, "version",
"Must be in the range 1 to ${PackageConfig.maxVersion}");
}
return version;
}
static PackageTree _validatePackages(
Iterable<Package> originalPackages, List<Package> packages) {
// Assumes packages are sorted.
Map<String, Package> result = {};
var tree = MutablePackageTree();
SimplePackage package;
for (var originalPackage in packages) {
if (originalPackage is! SimplePackage) {
// SimplePackage validates these properties.
try {
package = SimplePackage(
originalPackage.name,
originalPackage.root,
originalPackage.packageUriRoot,
originalPackage.languageVersion,
originalPackage.extraData);
} catch (e) {
throw PackageConfigArgumentError(
packages, "packages", "Package ${package.name}: ${e.message}");
}
} else {
package = originalPackage;
}
var name = package.name;
if (result.containsKey(name)) {
throw PackageConfigArgumentError(
name, "packages", "Duplicate package name");
}
result[name] = package;
try {
tree.add(0, package);
} on ConflictException catch (e) {
// There is a conflict with an existing package.
var existingPackage = e.existingPackage;
if (e.isRootConflict) {
throw PackageConfigArgumentError(
originalPackages,
"packages",
"Packages ${package.name} and ${existingPackage.name}"
"have the same root directory: ${package.root}.\n");
}
assert(e.isPackageRootConflict);
// Or package is inside the package URI root of the existing package.
throw PackageConfigArgumentError(
originalPackages,
"packages",
"Package ${package.name} is inside the package URI root of "
"package ${existingPackage.name}.\n"
"${existingPackage.name} URI root: "
"${existingPackage.packageUriRoot}\n"
"${package.name} root: ${package.root}\n");
}
}
return tree;
}
Iterable<Package> get packages => _packages.values;
Package /*?*/ operator [](String packageName) => _packages[packageName];
/// Provides the associated package for a specific [file] (or directory).
///
/// Returns a [Package] which contains the [file]'s path.
/// That is, the [Package.rootUri] directory is a parent directory
/// of the [file]'s location.
/// Returns `null` if the file does not belong to any package.
Package /*?*/ packageOf(Uri file) => _packageTree.packageOf(file);
Uri /*?*/ resolve(Uri packageUri) {
String packageName = checkValidPackageUri(packageUri, "packageUri");
return _packages[packageName]?.packageUriRoot?.resolveUri(
Uri(path: packageUri.path.substring(packageName.length + 1)));
}
Uri /*?*/ toPackageUri(Uri nonPackageUri) {
if (nonPackageUri.isScheme("package")) {
throw PackageConfigArgumentError(
nonPackageUri, "nonPackageUri", "Must not be a package URI");
}
if (nonPackageUri.hasQuery || nonPackageUri.hasFragment) {
throw PackageConfigArgumentError(nonPackageUri, "nonPackageUri",
"Must not have query or fragment part");
}
// Find package that file belongs to.
var package = _packageTree.packageOf(nonPackageUri);
if (package == null) return null;
// Check if it is inside the package URI root.
var path = nonPackageUri.toString();
var root = package.packageUriRoot.toString();
if (_beginsWith(package.root.toString().length, root, path)) {
var rest = path.substring(root.length);
return Uri(scheme: "package", path: "${package.name}/$rest");
}
return null;
}
}
/// Configuration data for a single package.
class SimplePackage implements Package {
final String name;
final Uri root;
final Uri packageUriRoot;
final String /*?*/ languageVersion;
final dynamic extraData;
SimplePackage._(this.name, this.root, this.packageUriRoot,
this.languageVersion, this.extraData);
/// Creates a [SimplePackage] with the provided content.
///
/// The provided arguments must be valid.
factory SimplePackage(String name, Uri root, Uri packageUriRoot,
String /*?*/ languageVersion, dynamic extraData) {
_validatePackageData(name, root, packageUriRoot, languageVersion);
return SimplePackage._(
name, root, packageUriRoot, languageVersion, extraData);
}
}
void _validatePackageData(
String name, Uri root, Uri packageUriRoot, String /*?*/ languageVersion) {
if (!isValidPackageName(name)) {
throw PackageConfigArgumentError(name, "name", "Not a valid package name");
}
if (!isAbsoluteDirectoryUri(root)) {
throw PackageConfigArgumentError(
"$root",
"root",
"Not an absolute URI with no query or fragment "
"with a path ending in /");
}
if (!isAbsoluteDirectoryUri(packageUriRoot)) {
throw PackageConfigArgumentError(
packageUriRoot,
"packageUriRoot",
"Not an absolute URI with no query or fragment "
"with a path ending in /");
}
if (!isUriPrefix(root, packageUriRoot)) {
throw PackageConfigArgumentError(packageUriRoot, "packageUriRoot",
"The package URI root is not below the package root");
}
if (languageVersion != null &&
checkValidVersionNumber(languageVersion) >= 0) {
throw PackageConfigArgumentError(
languageVersion, "languageVersion", "Invalid language version format");
}
}
abstract class PackageTree {
SimplePackage /*?*/ packageOf(Uri file);
}
/// Packages of a package configuration ordered by root path.
///
/// A package is said to be inside another package if the root path URI of
/// the latter is a prefix of the root path URI of the former.
/// No two packages of a package may have the same root path, so this
/// path prefix ordering defines a tree-like partial ordering on packages
/// of a configuration.
///
/// The package tree contains an ordered mapping of unrelated packages
/// (represented by their name) to their immediately nested packages' names.
class MutablePackageTree implements PackageTree {
final List<SimplePackage> packages = [];
Map<String, MutablePackageTree /*?*/ > /*?*/ _packageChildren;
/// Tries to (add) `package` to the tree.
///
/// Throws [ConflictException] if the added package conflicts with an
/// existing package.
/// It conflicts if it has the same root path, or if the new package
/// contains the existing package's package root.
void add(int start, SimplePackage package) {
var path = package.root.toString();
for (var childPackage in packages) {
var childPath = childPackage.root.toString();
assert(childPath.length > start);
assert(path.startsWith(childPath.substring(0, start)));
if (_beginsWith(start, childPath, path)) {
var childPathLength = childPath.length;
if (path.length == childPathLength) {
throw ConflictException.root(package, childPackage);
}
var childPackageRoot = childPackage.packageUriRoot.toString();
if (_beginsWith(childPathLength, childPackageRoot, path)) {
throw ConflictException.packageRoot(package, childPackage);
}
_treeOf(childPackage).add(childPathLength, package);
return;
}
}
packages.add(package);
}
SimplePackage /*?*/ packageOf(Uri file) {
return findPackageOf(0, file.toString());
}
/// Finds package containing [path] in this tree.
///
/// Returns `null` if no such package is found.
///
/// Assumes the first [start] characters of path agrees with all
/// the packages at this level of the tree.
SimplePackage /*?*/ findPackageOf(int start, String path) {
for (var childPackage in packages) {
var childPath = childPackage.root.toString();
if (_beginsWith(start, childPath, path)) {
// The [package] is inside [childPackage].
var childPathLength = childPath.length;
if (path.length == childPathLength) return childPackage;
var uriRoot = childPackage.packageUriRoot.toString();
// Is [package] is inside the URI root of [childPackage].
if (uriRoot.length == childPathLength ||
_beginsWith(childPathLength, uriRoot, path)) {
return childPackage;
}
// Otherwise add [package] as child of [childPackage].
// TODO(lrn): When NNBD comes, convert to:
// return _packageChildren?[childPackage.name]
// ?.packageOf(childPathLength, path) ?? childPackage;
if (_packageChildren == null) return childPackage;
var childTree = _packageChildren[childPackage.name];
if (childTree == null) return childPackage;
return childTree.findPackageOf(childPathLength, path) ?? childPackage;
}
}
return null;
}
/// Returns the [PackageTree] of the children of [package].
///
/// Ensures that the object is allocated if necessary.
MutablePackageTree _treeOf(SimplePackage package) {
var children = _packageChildren ??= {};
return children[package.name] ??= MutablePackageTree();
}
}
class EmptyPackageTree implements PackageTree {
const EmptyPackageTree();
SimplePackage packageOf(Uri file) => null;
}
/// Checks whether [longerPath] begins with [parentPath].
///
/// Skips checking the [start] first characters which are assumed to
/// already have been matched.
bool _beginsWith(int start, String parentPath, String longerPath) {
if (longerPath.length < parentPath.length) return false;
for (int i = start; i < parentPath.length; i++) {
if (longerPath.codeUnitAt(i) != parentPath.codeUnitAt(i)) return false;
}
return true;
}
/// Conflict between packages added to the same configuration.
///
/// The [package] conflicts with [existingPackage] if it has
/// the same root path ([isRootConflict]) or the package URI root path
/// of [existingPackage] is inside the root path of [package]
/// ([isPackageRootConflict]).
class ConflictException {
/// The existing package that [package] conflicts with.
final SimplePackage existingPackage;
/// The package that could not be added without a conflict.
final SimplePackage package;
/// Whether the conflict is with the package URI root of [existingPackage].
final bool isPackageRootConflict;
/// Creates a root conflict between [package] and [existingPackage].
ConflictException.root(this.package, this.existingPackage)
: isPackageRootConflict = false;
/// Creates a package root conflict between [package] and [existingPackage].
ConflictException.packageRoot(this.package, this.existingPackage)
: isPackageRootConflict = true;
/// WHether the conflict is with the root URI of [existingPackage].
bool get isRootConflict => !isPackageRootConflict;
}
/// Used for sorting packages by root path.
int _compareRoot(Package p1, Package p2) =>
p1.root.toString().compareTo(p2.root.toString());