blob: d83f4666e86a0ca3574a7823f1951fa81267abda [file] [log] [blame]
// Copyright (c) 2017, 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:collection/collection.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package.dart';
import 'source.dart';
import 'source/hosted.dart';
import 'utils.dart';
/// The equality to use when comparing the feature sets of two package names.
const _featureEquality = MapEquality<String, FeatureDependency>();
/// The base class of [PackageRef], [PackageId], and [PackageRange].
abstract class PackageName {
/// The name of the package being identified.
final String name;
/// The [Source] used to look up this package.
///
/// If this is a root package, this will be `null`.
final Source? source;
/// The metadata used by the package's [source] to identify and locate it.
///
/// It contains whatever [Source]-specific data it needs to be able to get
/// the package. For example, the description of a git sourced package might
/// by the URL "git://github.com/dart/uilib.git".
final dynamic description;
/// Whether this package is the root package.
bool get isRoot => source == null;
PackageName._(this.name, this.source, this.description);
/// Returns a [PackageRef] with this one's [name], [source], and
/// [description].
PackageRef toRef() => PackageRef(name, source, description);
/// Returns a [PackageRange] for this package with the given version constraint.
PackageRange withConstraint(VersionConstraint constraint) =>
PackageRange(name, source, constraint, description);
/// Returns whether this refers to the same package as [other].
///
/// This doesn't compare any constraint information; it's equivalent to
/// `this.toRef() == other.toRef()`.
bool samePackage(PackageName other) {
if (other.name != name) return false;
var thisSource = source;
if (thisSource == null) return other.source == null;
return other.source == thisSource &&
thisSource.descriptionsEqual(description, other.description);
}
@override
bool operator ==(Object other) =>
throw UnimplementedError('Subclass should implement ==');
@override
int get hashCode {
var thisSource = source;
if (thisSource == null) return name.hashCode;
return name.hashCode ^
thisSource.hashCode ^
thisSource.hashDescription(description);
}
/// Returns a string representation of this package name.
///
/// If [detail] is passed, it controls exactly which details are included.
@override
String toString([PackageDetail? detail]);
}
/// A reference to a [Package], but not any particular version(s) of it.
class PackageRef extends PackageName {
/// Creates a reference to a package with the given [name], [source], and
/// [description].
///
/// Since an ID's description is an implementation detail of its source, this
/// should generally not be called outside of [Source] subclasses. A reference
/// can be obtained from a user-supplied description using [Source.parseRef].
PackageRef(String name, Source? source, description)
: super._(name, source, description);
/// Creates a reference to the given root package.
PackageRef.root(Package package) : super._(package.name, null, package.name);
@override
String toString([PackageDetail? detail]) {
detail ??= PackageDetail.defaults;
if (isRoot) return name;
var buffer = StringBuffer(name);
if (detail.showSource ?? source is! HostedSource) {
buffer.write(' from $source');
if (detail.showDescription) {
buffer.write(' ${source!.formatDescription(description)}');
}
}
return buffer.toString();
}
@override
bool operator ==(other) => other is PackageRef && samePackage(other);
@override
int get hashCode => super.hashCode ^ 'PackageRef'.hashCode;
}
/// A reference to a specific version of a package.
///
/// A package ID contains enough information to correctly get the package.
///
/// It's possible for multiple distinct package IDs to point to different
/// packages that have identical contents. For example, the same package may be
/// available from multiple sources. As far as Pub is concerned, those packages
/// are different.
///
/// Note that a package ID's [description] field has a different structure than
/// the [PackageRef.description] or [PackageRange.description] fields for some
/// sources. For example, the `git` source adds revision information to the
/// description to ensure that the same ID always points to the same source.
class PackageId extends PackageName {
/// The package's version.
final Version version;
/// Creates an ID for a package with the given [name], [source], [version],
/// and [description].
///
/// Since an ID's description is an implementation detail of its source, this
/// should generally not be called outside of [Source] subclasses.
PackageId(String name, Source? source, this.version, description)
: super._(name, source, description);
/// Creates an ID for the given root package.
PackageId.root(Package package)
: version = package.version,
super._(package.name, null, package.name);
@override
int get hashCode => super.hashCode ^ version.hashCode;
@override
bool operator ==(other) =>
other is PackageId && samePackage(other) && other.version == version;
/// Returns a [PackageRange] that allows only [version] of this package.
PackageRange toRange() => withConstraint(version);
@override
String toString([PackageDetail? detail]) {
detail ??= PackageDetail.defaults;
var buffer = StringBuffer(name);
if (detail.showVersion ?? !isRoot) buffer.write(' $version');
if (!isRoot && (detail.showSource ?? source is! HostedSource)) {
buffer.write(' from $source');
if (detail.showDescription) {
buffer.write(' ${source!.formatDescription(description)}');
}
}
return buffer.toString();
}
}
/// A reference to a constrained range of versions of one package.
class PackageRange extends PackageName {
/// The allowed package versions.
final VersionConstraint constraint;
/// The dependencies declared on features of the target package.
final Map<String, FeatureDependency> features;
/// Creates a reference to package with the given [name], [source],
/// [constraint], and [description].
///
/// Since an ID's description is an implementation detail of its source, this
/// should generally not be called outside of [Source] subclasses.
PackageRange(String name, Source? source, this.constraint, description,
{Map<String, FeatureDependency>? features})
: features = features == null
? const {}
: UnmodifiableMapView(Map.from(features)),
super._(name, source, description);
/// Creates a range that selects the root package.
PackageRange.root(Package package)
: constraint = package.version,
features = const {},
super._(package.name, null, package.name);
/// Returns a description of [features], or the empty string if [features] is
/// empty.
String get featureDescription {
if (features.isEmpty) return '';
var enabledFeatures = <String>[];
var disabledFeatures = <String>[];
features.forEach((name, type) {
if (type == FeatureDependency.unused) {
disabledFeatures.add(name);
} else {
enabledFeatures.add(name);
}
});
var description = '';
if (enabledFeatures.isNotEmpty) {
description += 'with ${toSentence(enabledFeatures)}';
if (disabledFeatures.isNotEmpty) description += ', ';
}
if (disabledFeatures.isNotEmpty) {
description += 'without ${toSentence(disabledFeatures)}';
}
return description;
}
@override
String toString([PackageDetail? detail]) {
detail ??= PackageDetail.defaults;
var buffer = StringBuffer(name);
if (detail.showVersion ?? _showVersionConstraint) {
buffer.write(' $constraint');
}
if (!isRoot && (detail.showSource ?? source is! HostedSource)) {
buffer.write(' from $source');
if (detail.showDescription) {
buffer.write(' ${source!.formatDescription(description)}');
}
}
if (detail.showFeatures && features.isNotEmpty) {
buffer.write(' $featureDescription');
}
return buffer.toString();
}
/// Whether to include the version constraint in [toString] by default.
bool get _showVersionConstraint {
if (isRoot) return false;
if (!constraint.isAny) return true;
return source!.hasMultipleVersions;
}
/// Returns a new [PackageRange] with [features] merged with [this.features].
PackageRange withFeatures(Map<String, FeatureDependency> features) {
if (features.isEmpty) return this;
return PackageRange(name, source, constraint, description,
features: Map.from(this.features)..addAll(features));
}
/// Returns a copy of [this] with the same semantics, but with a `^`-style
/// constraint if possible.
PackageRange withTerseConstraint() {
if (constraint is! VersionRange) return this;
if (constraint.toString().startsWith('^')) return this;
var range = constraint as VersionRange;
if (!range.includeMin) return this;
if (range.includeMax) return this;
var min = range.min;
if (min == null) return this;
if (range.max == min.nextBreaking.firstPreRelease) {
return withConstraint(VersionConstraint.compatibleWith(min));
} else {
return this;
}
}
/// Whether [id] satisfies this dependency.
///
/// Specifically, whether [id] refers to the same package as [this] *and*
/// [constraint] allows `id.version`.
bool allows(PackageId id) => samePackage(id) && constraint.allows(id.version);
@override
int get hashCode =>
super.hashCode ^ constraint.hashCode ^ _featureEquality.hash(features);
@override
bool operator ==(other) =>
other is PackageRange &&
samePackage(other) &&
other.constraint == constraint &&
_featureEquality.equals(other.features, features);
}
/// An enum of types of dependencies on a [Feature].
class FeatureDependency {
/// The feature must exist and be enabled for this dependency to be satisfied.
static const required = FeatureDependency._('required');
/// The feature must be enabled if it exists, but is not required to exist for
/// this dependency to be satisfied.
static const ifAvailable = FeatureDependency._('if available');
/// The feature is neither required to exist nor to be enabled for this
/// feature to be satisfied.
static const unused = FeatureDependency._('unused');
final String _name;
/// Whether this type of dependency enables the feature it depends on.
bool get isEnabled => this != unused;
const FeatureDependency._(this._name);
@override
String toString() => _name;
}
/// An enum of different levels of detail that can be used when displaying a
/// terse package name.
class PackageDetail {
/// The default [PackageDetail] configuration.
static const defaults = PackageDetail();
/// Whether to show the package version or version range.
///
/// If this is `null`, the version is shown for all packages other than root
/// [PackageId]s or [PackageRange]s with `git` or `path` sources and `any`
/// constraints.
final bool? showVersion;
/// Whether to show the package source.
///
/// If this is `null`, the source is shown for all non-hosted, non-root
/// packages. It's always `true` if [showDescription] is `true`.
final bool? showSource;
/// Whether to show the package description.
///
/// This defaults to `false`.
final bool showDescription;
/// Whether to show the package features.
///
/// This defaults to `true`.
final bool showFeatures;
const PackageDetail(
{this.showVersion,
bool? showSource,
bool? showDescription,
bool? showFeatures})
: showSource = showDescription == true ? true : showSource,
showDescription = showDescription ?? false,
showFeatures = showFeatures ?? true;
/// Returns a [PackageDetail] with the maximum amount of detail between [this]
/// and [other].
PackageDetail max(PackageDetail other) => PackageDetail(
showVersion: showVersion! || other.showVersion!,
showSource: showSource! || other.showSource!,
showDescription: showDescription || other.showDescription,
showFeatures: showFeatures || other.showFeatures);
}