blob: f68a9ea7540cb76c1e4c5612f820157aa9a4e53a [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";
// Implementations of the main data types exposed by the API of this package.
class SimplePackageConfig implements PackageConfig {
final int version;
final Map<String, Package> _packages;
final PackageTree _packageTree;
final dynamic extraData;
factory SimplePackageConfig(int version, Iterable<Package> packages,
[dynamic extraData, void onError(Object error)]) {
onError ??= throwError;
var validVersion = _validateVersion(version, onError);
var sortedPackages = [...packages]..sort(_compareRoot);
var packageTree = _validatePackages(packages, sortedPackages, onError);
return SimplePackageConfig._(validVersion, packageTree,
{for (var p in packageTree.allPackages) p.name: p}, extraData);
}
SimplePackageConfig._(
this.version, this._packageTree, this._packages, this.extraData);
/// 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, void onError(Object error)) {
if (version < 0 || version > PackageConfig.maxVersion) {
onError(PackageConfigArgumentError(version, "version",
"Must be in the range 1 to ${PackageConfig.maxVersion}"));
return 2; // The minimal version supporting a SimplePackageConfig.
}
return version;
}
static PackageTree _validatePackages(Iterable<Package> originalPackages,
List<Package> packages, void onError(Object error)) {
var packageNames = <String>{};
var tree = MutablePackageTree();
for (var originalPackage in packages) {
if (originalPackage == null) {
onError(ArgumentError.notNull("element of packages"));
continue;
}
SimplePackage package;
if (originalPackage is! SimplePackage) {
// SimplePackage validates these properties.
package = SimplePackage.validate(
originalPackage.name,
originalPackage.root,
originalPackage.packageUriRoot,
originalPackage.languageVersion,
originalPackage.extraData, (error) {
if (error is PackageConfigArgumentError) {
onError(PackageConfigArgumentError(packages, "packages",
"Package ${package.name}: ${error.message}"));
} else {
onError(error);
}
});
if (package == null) continue;
} else {
package = originalPackage;
}
var name = package.name;
if (packageNames.contains(name)) {
onError(PackageConfigArgumentError(
name, "packages", "Duplicate package name"));
continue;
}
packageNames.add(name);
tree.add(0, package, (error) {
if (error is ConflictException) {
// There is a conflict with an existing package.
var existingPackage = error.existingPackage;
if (error.isRootConflict) {
onError(PackageConfigArgumentError(
originalPackages,
"packages",
"Packages ${package.name} and ${existingPackage.name} "
"have the same root directory: ${package.root}.\n"));
} else {
assert(error.isPackageRootConflict);
// Package is inside the package URI root of the existing package.
onError(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"));
}
} else {
// Any other error.
onError(error);
}
});
}
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) {
var 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 LanguageVersion /*?*/ 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.
///
/// If the arguments are invalid then the error is reported by
/// calling [onError], then the erroneous entry is ignored.
///
/// If [onError] is provided, the user is expected to be able to handle
/// errors themselves. An invalid [languageVersion] string
/// will be replaced with the string `"invalid"`. This allows
/// users to detect the difference between an absent version and
/// an invalid one.
///
/// Returns `null` if the input is invalid and an approximately valid package
/// cannot be salvaged from the input.
static SimplePackage /*?*/ validate(
String name,
Uri root,
Uri packageUriRoot,
LanguageVersion /*?*/ languageVersion,
dynamic extraData,
void onError(Object error)) {
var fatalError = false;
var invalidIndex = checkPackageName(name);
if (invalidIndex >= 0) {
onError(PackageConfigFormatException(
"Not a valid package name", name, invalidIndex));
fatalError = true;
}
if (root.isScheme("package")) {
onError(PackageConfigArgumentError(
"$root", "root", "Must not be a package URI"));
fatalError = true;
} else if (!isAbsoluteDirectoryUri(root)) {
onError(PackageConfigArgumentError(
"$root",
"root",
"In package $name: Not an absolute URI with no query or fragment "
"with a path ending in /"));
// Try to recover. If the URI has a scheme,
// then ensure that the path ends with `/`.
if (!root.hasScheme) {
fatalError = true;
} else if (!root.path.endsWith("/")) {
root = root.replace(path: root.path + "/");
}
}
if (!fatalError) {
if (!isAbsoluteDirectoryUri(packageUriRoot)) {
onError(PackageConfigArgumentError(
packageUriRoot,
"packageUriRoot",
"In package $name: Not an absolute URI with no query or fragment "
"with a path ending in /"));
packageUriRoot = root;
} else if (!isUriPrefix(root, packageUriRoot)) {
onError(PackageConfigArgumentError(packageUriRoot, "packageUriRoot",
"The package URI root is not below the package root"));
packageUriRoot = root;
}
}
if (fatalError) return null;
return SimplePackage._(
name, root, packageUriRoot, languageVersion, extraData);
}
}
/// Checks whether [version] is a valid Dart language version string.
///
/// The format is (as RegExp) `^(0|[1-9]\d+)\.(0|[1-9]\d+)$`.
///
/// Reports a format exception on [onError] if not, or if the numbers
/// are too large (at most 32-bit signed integers).
LanguageVersion parseLanguageVersion(
String source, void onError(Object error)) {
var index = 0;
// Reads a positive decimal numeral. Returns the value of the numeral,
// or a negative number in case of an error.
// Starts at [index] and increments the index to the position after
// the numeral.
// It is an error if the numeral value is greater than 0x7FFFFFFFF.
// It is a recoverable error if the numeral starts with leading zeros.
int readNumeral() {
const maxValue = 0x7FFFFFFF;
if (index == source.length) {
onError(PackageConfigFormatException("Missing number", source, index));
return -1;
}
var start = index;
var char = source.codeUnitAt(index);
var digit = char ^ 0x30;
if (digit > 9) {
onError(PackageConfigFormatException("Missing number", source, index));
return -1;
}
var firstDigit = digit;
var value = 0;
do {
value = value * 10 + digit;
if (value > maxValue) {
onError(
PackageConfigFormatException("Number too large", source, start));
return -1;
}
index++;
if (index == source.length) break;
char = source.codeUnitAt(index);
digit = char ^ 0x30;
} while (digit <= 9);
if (firstDigit == 0 && index > start + 1) {
onError(PackageConfigFormatException(
"Leading zero not allowed", source, start));
}
return value;
}
var major = readNumeral();
if (major < 0) {
return SimpleInvalidLanguageVersion(source);
}
if (index == source.length || source.codeUnitAt(index) != $dot) {
onError(PackageConfigFormatException("Missing '.'", source, index));
return SimpleInvalidLanguageVersion(source);
}
index++;
var minor = readNumeral();
if (minor < 0) {
return SimpleInvalidLanguageVersion(source);
}
if (index != source.length) {
onError(PackageConfigFormatException(
"Unexpected trailing character", source, index));
return SimpleInvalidLanguageVersion(source);
}
return SimpleLanguageVersion(major, minor, source);
}
abstract class _SimpleLanguageVersionBase implements LanguageVersion {
int compareTo(LanguageVersion other) {
var result = major.compareTo(other.major);
if (result != 0) return result;
return minor.compareTo(other.minor);
}
}
class SimpleLanguageVersion extends _SimpleLanguageVersionBase {
final int major;
final int minor;
String /*?*/ _source;
SimpleLanguageVersion(this.major, this.minor, this._source);
bool operator ==(Object other) =>
other is LanguageVersion && major == other.major && minor == other.minor;
int get hashCode => (major * 17 ^ minor * 37) & 0x3FFFFFFF;
String toString() => _source ??= "$major.$minor";
}
class SimpleInvalidLanguageVersion extends _SimpleLanguageVersionBase
implements InvalidLanguageVersion {
final String _source;
SimpleInvalidLanguageVersion(this._source);
int get major => -1;
int get minor => -1;
String toString() => _source;
}
abstract class PackageTree {
Iterable<Package> get allPackages;
SimplePackage /*?*/ packageOf(Uri file);
}
/// Packages of a package configuration ordered by root path.
///
/// A package has a root path and a package root path, where the latter
/// contains the files exposed by `package:` URIs.
///
/// 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 root path of a package must not be inside another package's
/// root path.
/// Entire other packages are allowed inside a package's root or
/// package root path.
///
/// The package tree contains an ordered mapping of unrelated packages
/// (represented by their name) to their immediately nested packages' names.
class MutablePackageTree implements PackageTree {
/// A list of packages that are not nested inside each other.
final List<SimplePackage> packages = [];
/// The tree of the immediately nested packages inside each package.
///
/// Indexed by [Package.name].
/// If a package has no nested packages (which is most often the case),
/// there is no tree object associated with it.
Map<String, MutablePackageTree /*?*/ > /*?*/ _packageChildren;
Iterable<Package> get allPackages sync* {
for (var package in packages) yield package;
if (_packageChildren != null) {
for (var tree in _packageChildren.values) yield* tree.allPackages;
}
}
/// Tries to (add) `package` to the tree.
///
/// Reports a [ConflictException] if the added package conflicts with an
/// existing package.
/// It conflicts if its root or package root is the same as another
/// package's root or package root, or is between the two.
///
/// If a conflict is detected between [package] and a previous package,
/// then [onError] is called with a [ConflictException] object
/// and the [package] is not added to the tree.
///
/// The packages are added in order of their root path.
/// It is never necessary to insert a node between two existing levels.
void add(int start, SimplePackage package, void onError(Object error)) {
var path = package.root.toString();
for (var treePackage in packages) {
// Check is package is inside treePackage.
var treePackagePath = treePackage.root.toString();
assert(treePackagePath.length > start);
assert(path.startsWith(treePackagePath.substring(0, start)));
if (_beginsWith(start, treePackagePath, path)) {
// Package *is* inside treePackage.
var treePackagePathLength = treePackagePath.length;
if (path.length == treePackagePathLength) {
// Has same root. Do not add package.
onError(ConflictException.root(package, treePackage));
return;
}
var treePackageUriRoot = treePackage.packageUriRoot.toString();
if (_beginsWith(treePackagePathLength, path, treePackageUriRoot)) {
// The treePackage's package root is inside package, which is inside
// the treePackage. This is not allowed.
onError(ConflictException.packageRoot(package, treePackage));
return;
}
_treeOf(treePackage).add(treePackagePathLength, package, onError);
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();
Iterable<Package> get allPackages => const Iterable<Package>.empty();
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 (var 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());