blob: ae5935ea57317bdcc3e1b6298430687bafb7de53 [file] [log] [blame]
// Copyright (c) 2012, 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:io';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:pub_semver/pub_semver.dart';
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';
import 'barback/transformer_config.dart';
import 'compiler.dart';
import 'exceptions.dart';
import 'feature.dart';
import 'io.dart';
import 'log.dart';
import 'package_name.dart';
import 'sdk.dart' as sdk;
import 'source_registry.dart';
import 'utils.dart';
/// A regular expression matching allowed package names.
///
/// This allows dot-separated valid Dart identifiers. The dots are there for
/// compatibility with Google's internal Dart packages, but they may not be used
/// when publishing a package to pub.dartlang.org.
final _packageName = new RegExp(
"^${identifierRegExp.pattern}(\\.${identifierRegExp.pattern})*\$");
/// The default SDK upper bound constraint for packages that don't declare one.
///
/// This provides a sane default for packages that don't have an upper bound.
final VersionRange _defaultUpperBoundSdkConstraint =
new VersionConstraint.parse("<2.0.0");
/// Whether or not to allow the pre-release SDK for packages that have an
/// upper bound Dart SDK constraint of <2.0.0.
///
/// If enabled then a Dart SDK upper bound of <2.0.0 is always converted to
/// <2.0.0-dev.infinity.
///
/// This has a default value of `true` but can be overridden with the
/// PUB_ALLOW_PRERELEASE_SDK system environment variable.
bool get allowPreReleaseSdk => allowPreReleaseSdkValue != 'false';
/// The value of the PUB_ALLOW_PRERELEASE_SDK environment variable, defaulted
/// to `true`.
final String allowPreReleaseSdkValue = () {
var value =
Platform.environment["PUB_ALLOW_PRERELEASE_SDK"]?.toLowerCase() ?? 'true';
if (!['true', 'quiet', 'false'].contains(value)) {
warning(yellow('''
The environment variable PUB_ALLOW_PRERELEASE_SDK is set as `$value`.
The expected value is either `true`, `quiet` (true but no logging), or `false`.
Using a default value of `true`.
'''));
value = 'true';
}
return value;
}();
/// Whether or not to warn about pre-release SDK overrides.
bool get warnAboutPreReleaseSdkOverrides => allowPreReleaseSdkValue != 'quiet';
/// The parsed contents of a pubspec file.
///
/// The fields of a pubspec are, for the most part, validated when they're first
/// accessed. This allows a partially-invalid pubspec to be used if only the
/// valid portions are relevant. To get a list of all errors in the pubspec, use
/// [allErrors].
class Pubspec {
// If a new lazily-initialized field is added to this class and the
// initialization can throw a [PubspecException], that error should also be
// exposed through [allErrors].
/// The registry of sources to use when parsing [dependencies] and
/// [devDependencies].
///
/// This will be null if this was created using [new Pubspec] or [new
/// Pubspec.empty].
final SourceRegistry _sources;
/// The location from which the pubspec was loaded.
///
/// This can be null if the pubspec was created in-memory or if its location
/// is unknown.
Uri get _location => fields.span.sourceUrl;
/// All pubspec fields.
///
/// This includes the fields from which other properties are derived.
final YamlMap fields;
/// Whether or not to apply the [_defaultUpperBoundsSdkConstraint] to this
/// pubspec.
final bool _includeDefaultSdkConstraint;
/// Whether or not the SDK version was overridden from <2.0.0 to
/// <2.0.0-dev.infinity.
bool get dartSdkWasOverridden => _dartSdkWasOverridden;
bool _dartSdkWasOverridden = false;
/// The package's name.
String get name {
if (_name != null) return _name;
var name = fields['name'];
if (name == null) {
throw new PubspecException(
'Missing the required "name" field.', fields.span);
} else if (name is! String) {
throw new PubspecException(
'"name" field must be a string.', fields.nodes['name'].span);
} else if (!_packageName.hasMatch(name)) {
throw new PubspecException(
'"name" field must be a valid Dart identifier.',
fields.nodes['name'].span);
} else if (reservedWords.contains(name)) {
throw new PubspecException(
'"name" field may not be a Dart reserved word.',
fields.nodes['name'].span);
}
_name = name;
return _name;
}
String _name;
/// The package's version.
Version get version {
if (_version != null) return _version;
var version = fields['version'];
if (version == null) {
_version = Version.none;
return _version;
}
var span = fields.nodes['version'].span;
if (version is num) {
var fixed = '$version.0';
if (version is int) {
fixed = '$fixed.0';
}
_error(
'"version" field must have three numeric components: major, '
'minor, and patch. Instead of "$version", consider "$fixed".',
span);
}
if (version is! String) {
_error('"version" field must be a string.', span);
}
_version = _wrapFormatException(
'version number', span, () => new Version.parse(version));
return _version;
}
Version _version;
/// The additional packages this package depends on.
List<PackageRange> get dependencies {
if (_dependencies != null) return _dependencies;
_dependencies =
_parseDependencies('dependencies', fields.nodes['dependencies']);
return _dependencies;
}
List<PackageRange> _dependencies;
/// The packages this package depends on when it is the root package.
List<PackageRange> get devDependencies {
if (_devDependencies != null) return _devDependencies;
_devDependencies = _parseDependencies(
'dev_dependencies', fields.nodes['dev_dependencies']);
return _devDependencies;
}
List<PackageRange> _devDependencies;
/// The dependency constraints that this package overrides when it is the
/// root package.
///
/// Dependencies here will replace any dependency on a package with the same
/// name anywhere in the dependency graph.
List<PackageRange> get dependencyOverrides {
if (_dependencyOverrides != null) return _dependencyOverrides;
_dependencyOverrides = _parseDependencies(
'dependency_overrides', fields.nodes['dependency_overrides']);
return _dependencyOverrides;
}
List<PackageRange> _dependencyOverrides;
Map<String, Feature> get features {
if (_features != null) return _features;
var features = fields['features'];
if (features == null) {
_features = const {};
return _features;
}
if (features is! Map) {
_error('"features" field must be a map.', fields.nodes['features'].span);
}
_features = mapMap(features.nodes,
key: (nameNode, _) => _validateFeatureName(nameNode),
value: (nameNode, specNode) {
if (specNode.value == null) {
return new Feature(nameNode.value, const []);
}
if (specNode is! Map) {
_error('A feature specification must be a map.', specNode.span);
}
var onByDefault = specNode['default'] ?? true;
if (onByDefault is! bool) {
_error('Default must be true or false.',
specNode.nodes['default'].span);
}
var requires = _parseStringList(specNode.nodes['requires'],
validate: (name, span) {
if (!features.containsKey(name)) _error('Undefined feature.', span);
});
var dependencies = _parseDependencies(
'dependencies', specNode.nodes['dependencies']);
var sdkConstraints = _parseEnvironment(specNode);
return new Feature(nameNode.value, dependencies,
requires: requires,
dartSdkConstraint: sdkConstraints.first,
flutterSdkConstraint: sdkConstraints.last,
onByDefault: onByDefault);
});
return _features;
}
Map<String, Feature> _features;
/// The configurations of the transformers to use for this package.
List<Set<TransformerConfig>> get transformers {
if (_transformers != null) return _transformers;
_transformers = _parseList(fields.nodes['transformers']).nodes.map((phase) {
var phaseNodes = phase is YamlList ? phase.nodes : [phase];
return phaseNodes.map((transformerNode) {
var transformer = transformerNode.value;
if (transformer is! String && transformer is! Map) {
_error(
'A transformer must be a string or map.', transformerNode.span);
}
var libraryNode;
var configurationNode;
if (transformer is String) {
libraryNode = transformerNode;
} else {
if (transformer.length != 1) {
_error(
'A transformer map must have a single key: the transformer '
'identifier.',
transformerNode.span);
} else if (transformer.keys.single is! String) {
_error('A transformer identifier must be a string.',
transformer.nodes.keys.single.span);
}
libraryNode = transformer.nodes.keys.single;
configurationNode = transformer.nodes.values.single;
if (configurationNode is! YamlMap) {
_error("A transformer's configuration must be a map.",
configurationNode.span);
}
}
var config = _wrapSpanFormatException('transformer config', () {
return new TransformerConfig.parse(
libraryNode.value, libraryNode.span, configurationNode);
});
var package = config.id.package;
if (package != name &&
!config.id.isBuiltInTransformer &&
!_hasDependency(package)) {
_error('"$package" is not a dependency.', libraryNode.span);
}
return config;
}).toSet();
}).toList();
return _transformers;
}
List<Set<TransformerConfig>> _transformers;
/// Returns whether this pubspec has any kind of dependency on [package].
///
/// This explicitly avoids calling [_parseDependencies] because parsing dev
/// dependencies can fail for a hosted package's pubspec (e.g. if that package
/// has a relative path dev dependency).
bool _hasDependency(String package) {
return ['dependencies', 'dev_dependencies', 'dependency_overrides']
.any((field) {
var map = fields[field];
if (map == null) return false;
if (map is! Map) {
_error('"$field" field must be a map.', fields.nodes[field].span);
}
return map.containsKey(package);
});
}
/// The constraint on the Dart SDK, with [_defaultUpperBoundSdkConstraint] if
/// none is specified.
///
/// This also includes the pre-release override if
/// [allowPreReleaseTwoDotZeroSdk] is `true`.
VersionConstraint get dartSdkConstraint {
_ensureEnvironment();
return _dartSdkConstraint;
}
VersionConstraint _dartSdkConstraint;
/// The original Dart SDK constraint, if [dartSdkWasOverridden] is `true`,
/// otherwise this will be identical to [dartSdkConstraint].
VersionConstraint get originalDartSdkConstraint {
_ensureEnvironment();
return _originalDartSdkConstraint ?? dartSdkConstraint;
}
VersionConstraint _originalDartSdkConstraint;
/// The constraint on the Flutter SDK, or `null` if none is specified.
VersionConstraint get flutterSdkConstraint {
_ensureEnvironment();
return _flutterSdkConstraint;
}
VersionConstraint _flutterSdkConstraint;
/// Ensures that the top-level "environment" field has been parsed and
/// [_dartSdkConstraint] and [_flutterSdkConstraint] are set accordingly.
void _ensureEnvironment() {
if (_dartSdkConstraint != null) return;
var pair = _parseEnvironment(fields);
var parsedDartSdkConstraint = pair.first;
if (parsedDartSdkConstraint is VersionRange &&
_shouldEnableCurrentSdk(parsedDartSdkConstraint)) {
_originalDartSdkConstraint = parsedDartSdkConstraint;
_dartSdkWasOverridden = true;
_dartSdkConstraint = new VersionRange(
min: parsedDartSdkConstraint.min,
includeMin: parsedDartSdkConstraint.includeMin,
max: sdk.version,
includeMax: true);
} else {
_dartSdkConstraint = parsedDartSdkConstraint;
}
_flutterSdkConstraint = pair.last;
}
/// Whether or not we should override [sdkConstraint] to be <= the user's
/// current SDK version.
///
/// This is true if the following conditions are met:
///
/// - [allowPreReleaseSdk] is `true`
/// - The user's current SDK is a pre-release version.
/// - The original [sdkConstraint] max version is exclusive (`includeMax`
/// is `false`).
/// - The original [sdkConstraint] is not a pre-release version.
/// - The original [sdkConstraint] matches the exact same major, minor, and
/// patch versions as the user's current SDK.
bool _shouldEnableCurrentSdk(VersionRange sdkConstraint) {
if (!allowPreReleaseSdk) return false;
if (!sdk.version.isPreRelease) return false;
if (sdkConstraint.includeMax) return false;
if (sdkConstraint.min != null &&
sdkConstraint.min.isPreRelease &&
sdkConstraint.min.major == sdk.version.major &&
sdkConstraint.min.minor == sdk.version.minor &&
sdkConstraint.min.patch == sdk.version.patch) {
return false;
}
if (sdkConstraint.max == null) return false;
if (sdkConstraint.max.isPreRelease) return false;
return sdkConstraint.max.major == sdk.version.major &&
sdkConstraint.max.minor == sdk.version.minor &&
sdkConstraint.max.patch == sdk.version.patch;
}
/// Parses the "environment" field in [parent] and returns the Dart and
/// Flutter SDK constraints, respectively.
Pair<VersionConstraint, VersionConstraint> _parseEnvironment(YamlMap parent) {
var yaml = parent['environment'];
if (yaml == null) {
return new Pair(
_includeDefaultSdkConstraint
? _defaultUpperBoundSdkConstraint
: VersionConstraint.any,
null);
}
if (yaml is! Map) {
_error('"environment" field must be a map.',
parent.nodes['environment'].span);
}
return new Pair(
_parseVersionConstraint(yaml.nodes['sdk'],
defaultUpperBoundConstraint: _includeDefaultSdkConstraint
? _defaultUpperBoundSdkConstraint
: null),
yaml.containsKey('flutter')
? _parseVersionConstraint(yaml.nodes['flutter'])
: null);
}
/// The URL of the server that the package should default to being published
/// to, "none" if the package should not be published, or `null` if it should
/// be published to the default server.
///
/// If this does return a URL string, it will be a valid parseable URL.
String get publishTo {
if (_parsedPublishTo) return _publishTo;
var publishTo = fields['publish_to'];
if (publishTo != null) {
var span = fields.nodes['publish_to'].span;
if (publishTo is! String) {
_error('"publish_to" field must be a string.', span);
}
// It must be "none" or a valid URL.
if (publishTo != "none") {
_wrapFormatException('"publish_to" field', span, () {
var url = Uri.parse(publishTo);
if (url.scheme.isEmpty) {
throw new FormatException("must be an absolute URL.");
}
});
}
}
_parsedPublishTo = true;
_publishTo = publishTo;
return _publishTo;
}
bool _parsedPublishTo = false;
String _publishTo;
/// The executables that should be placed on the user's PATH when this
/// package is globally activated.
///
/// It is a map of strings to string. Each key is the name of the command
/// that will be placed on the user's PATH. The value is the name of the
/// .dart script (without extension) in the package's `bin` directory that
/// should be run for that command. Both key and value must be "simple"
/// strings: alphanumerics, underscores and hypens only. If a value is
/// omitted, it is inferred to use the same name as the key.
Map<String, String> get executables {
if (_executables != null) return _executables;
_executables = {};
var yaml = fields['executables'];
if (yaml == null) return _executables;
if (yaml is! Map) {
_error('"executables" field must be a map.',
fields.nodes['executables'].span);
}
yaml.nodes.forEach((key, value) {
if (key.value is! String) {
_error('"executables" keys must be strings.', key.span);
}
final keyPattern = new RegExp(r"^[a-zA-Z0-9_-]+$");
if (!keyPattern.hasMatch(key.value)) {
_error(
'"executables" keys may only contain letters, '
'numbers, hyphens and underscores.',
key.span);
}
if (value.value == null) {
value = key;
} else if (value.value is! String) {
_error('"executables" values must be strings or null.', value.span);
}
final valuePattern = new RegExp(r"[/\\]");
if (valuePattern.hasMatch(value.value)) {
_error('"executables" values may not contain path separators.',
value.span);
}
_executables[key.value] = value.value;
});
return _executables;
}
Map<String, String> _executables;
/// The settings for which web compiler to use in which mode.
///
/// It is a map of [String] to [Compiler]. Each key is the name of a mode, and
/// the value is the web compiler to use in that mode.
///
/// Valid compiler values are all of [Compiler.names].
Map<String, Compiler> get webCompiler {
if (_webCompiler != null) return _webCompiler;
_webCompiler = <String, Compiler>{};
var webYaml = fields.nodes['web'];
if (webYaml?.value == null) return _webCompiler;
if (webYaml is! Map) {
_error('"web" field must be a map.', webYaml.span);
}
var compilerYaml = (webYaml as YamlMap)['compiler'];
if (compilerYaml == null) return _webCompiler;
if (compilerYaml is! Map) {
_error('"compiler" field must be a map.',
(webYaml as YamlMap).nodes['compiler'].span);
}
compilerYaml.nodes.forEach((key, value) {
if (key.value is! String) {
_error('"compiler" keys must be strings.', key.span);
}
if (!Compiler.names.contains(value.value)) {
_error(
'"compiler" values must be one of ${Compiler.names}.', value.span);
}
_webCompiler[key.value] = Compiler.byName(value.value);
});
return _webCompiler;
}
Map<String, Compiler> _webCompiler;
/// Whether the package is private and cannot be published.
///
/// This is specified in the pubspec by setting "publish_to" to "none".
bool get isPrivate => publishTo == "none";
/// Whether or not the pubspec has no contents.
bool get isEmpty =>
name == null && version == Version.none && dependencies.isEmpty;
/// Loads the pubspec for a package located in [packageDir].
///
/// If [expectedName] is passed and the pubspec doesn't have a matching name
/// field, this will throw a [PubspecError].
factory Pubspec.load(String packageDir, SourceRegistry sources,
{String expectedName, bool includeDefaultSdkConstraint}) {
var pubspecPath = path.join(packageDir, 'pubspec.yaml');
var pubspecUri = path.toUri(pubspecPath);
if (!fileExists(pubspecPath)) {
throw new FileException(
// Make the package dir absolute because for the entrypoint it'll just
// be ".", which may be confusing.
'Could not find a file named "pubspec.yaml" in '
'"${canonicalize(packageDir)}".',
pubspecPath);
}
return new Pubspec.parse(readTextFile(pubspecPath), sources,
expectedName: expectedName,
includeDefaultSdkConstraint: includeDefaultSdkConstraint,
location: pubspecUri);
}
Pubspec(this._name,
{Version version,
Iterable<PackageRange> dependencies,
Iterable<PackageRange> devDependencies,
Iterable<PackageRange> dependencyOverrides,
VersionConstraint dartSdkConstraint,
bool includeDefaultSdkConstraint,
VersionConstraint flutterSdkConstraint,
Iterable<Iterable<TransformerConfig>> transformers,
Map fields,
SourceRegistry sources})
: _version = version,
_dependencies = dependencies == null ? null : dependencies.toList(),
_devDependencies =
devDependencies == null ? null : devDependencies.toList(),
_dependencyOverrides =
dependencyOverrides == null ? null : dependencyOverrides.toList(),
_dartSdkConstraint =
dartSdkConstraint ?? includeDefaultSdkConstraint == true
? _defaultUpperBoundSdkConstraint
: VersionConstraint.any,
_flutterSdkConstraint = flutterSdkConstraint,
_includeDefaultSdkConstraint = includeDefaultSdkConstraint,
_transformers = transformers == null
? []
: transformers.map((phase) => phase.toSet()).toList(),
fields = fields == null ? new YamlMap() : new YamlMap.wrap(fields),
_sources = sources;
Pubspec.empty()
: _sources = null,
_name = null,
_version = Version.none,
_dependencies = <PackageRange>[],
_devDependencies = <PackageRange>[],
_dartSdkConstraint = VersionConstraint.any,
_flutterSdkConstraint = null,
_includeDefaultSdkConstraint = false,
_transformers = <Set<TransformerConfig>>[],
fields = new YamlMap();
/// Returns a Pubspec object for an already-parsed map representing its
/// contents.
///
/// If [expectedName] is passed and the pubspec doesn't have a matching name
/// field, this will throw a [PubspecError].
///
/// [location] is the location from which this pubspec was loaded.
Pubspec.fromMap(Map fields, this._sources,
{String expectedName, bool includeDefaultSdkConstraint, Uri location})
: fields = fields is YamlMap
? fields
: new YamlMap.wrap(fields, sourceUrl: location),
_includeDefaultSdkConstraint = includeDefaultSdkConstraint ?? true {
// If [expectedName] is passed, ensure that the actual 'name' field exists
// and matches the expectation.
if (expectedName == null) return;
if (name == expectedName) return;
throw new PubspecException(
'"name" field doesn\'t match expected name '
'"$expectedName".',
this.fields.nodes["name"].span);
}
/// Parses the pubspec stored at [filePath] whose text is [contents].
///
/// If the pubspec doesn't define a version for itself, it defaults to
/// [Version.none].
factory Pubspec.parse(String contents, SourceRegistry sources,
{String expectedName, bool includeDefaultSdkConstraint, Uri location}) {
YamlNode pubspecNode;
try {
pubspecNode = loadYamlNode(contents, sourceUrl: location);
} on YamlException catch (error) {
throw new PubspecException(error.message, error.span);
}
Map pubspecMap;
if (pubspecNode is YamlScalar && pubspecNode.value == null) {
pubspecMap = new YamlMap(sourceUrl: location);
} else if (pubspecNode is YamlMap) {
pubspecMap = pubspecNode;
} else {
throw new PubspecException(
'The pubspec must be a YAML mapping.', pubspecNode.span);
}
return new Pubspec.fromMap(pubspecMap, sources,
expectedName: expectedName,
includeDefaultSdkConstraint: includeDefaultSdkConstraint,
location: location);
}
/// Returns a list of most errors in this pubspec.
///
/// This will return at most one error for each field.
List<PubspecException> get allErrors {
var errors = <PubspecException>[];
_getError(fn()) {
try {
fn();
} on PubspecException catch (e) {
errors.add(e);
}
}
_getError(() => this.name);
_getError(() => this.version);
_getError(() => this.dependencies);
_getError(() => this.devDependencies);
_getError(() => this.transformers);
_getError(() => this.publishTo);
_getError(() => this.features);
_getError(() => this._ensureEnvironment());
return errors;
}
/// Parses the dependency field named [field], and returns the corresponding
/// list of dependencies.
List<PackageRange> _parseDependencies(String field, YamlNode node) {
var dependencies = <PackageRange>[];
// Allow an empty dependencies key.
if (node == null || node.value == null) return dependencies;
if (node is! YamlMap) {
_error('"$field" field must be a map.', node.span);
}
var map = node as YamlMap;
var nonStringNode = map.nodes.keys
.firstWhere((e) => e.value is! String, orElse: () => null);
if (nonStringNode != null) {
_error('A dependency name must be a string.', nonStringNode.span);
}
map.nodes.forEach((nameNode, specNode) {
var name = nameNode.value;
var spec = specNode.value;
if (fields['name'] != null && name == this.name) {
_error('A package may not list itself as a dependency.', nameNode.span);
}
var descriptionNode;
var sourceName;
var versionConstraint = new VersionRange();
Map<String, FeatureDependency> features = const {};
if (spec == null) {
descriptionNode = nameNode;
sourceName = _sources.defaultSource.name;
} else if (spec is String) {
descriptionNode = nameNode;
sourceName = _sources.defaultSource.name;
versionConstraint = _parseVersionConstraint(specNode);
} else if (spec is Map) {
// Don't write to the immutable YAML map.
spec = new Map.from(spec);
var specMap = specNode as YamlMap;
if (spec.containsKey('version')) {
spec.remove('version');
versionConstraint = _parseVersionConstraint(specMap.nodes['version']);
}
if (spec.containsKey('features')) {
spec.remove('features');
features = _parseDependencyFeatures(specMap.nodes['features']);
}
var sourceNames = spec.keys.toList();
if (sourceNames.length > 1) {
_error('A dependency may only have one source.', specNode.span);
} else if (sourceNames.isEmpty) {
// Default to a hosted dependency if no source is specifid.
sourceName = 'hosted';
descriptionNode = nameNode;
}
sourceName ??= sourceNames.single;
if (sourceName is! String) {
_error('A source name must be a string.',
specMap.nodes.keys.single.span);
}
descriptionNode ??= specMap.nodes[sourceName];
} else {
_error('A dependency specification must be a string or a mapping.',
specNode.span);
}
// Let the source validate the description.
var ref = _wrapFormatException('description', descriptionNode?.span, () {
var pubspecPath;
if (_location != null && _isFileUri(_location)) {
pubspecPath = path.fromUri(_location);
}
return _sources[sourceName].parseRef(name, descriptionNode?.value,
containingPath: pubspecPath);
});
dependencies
.add(ref.withConstraint(versionConstraint).withFeatures(features));
});
return dependencies;
}
/// Parses [node] to a [VersionConstraint].
///
/// If or [defaultUpperBoundConstraint] is specified then it will be set as
/// the max constraint if the original constraint doesn't have an upper
/// bound and it is compatible with [defaultUpperBoundConstraint].
VersionConstraint _parseVersionConstraint(YamlNode node,
{VersionConstraint defaultUpperBoundConstraint}) {
if (node?.value == null) {
return defaultUpperBoundConstraint ?? VersionConstraint.any;
}
if (node.value is! String) {
_error('A version constraint must be a string.', node.span);
}
return _wrapFormatException('version constraint', node.span, () {
var constraint = new VersionConstraint.parse(node.value);
if (defaultUpperBoundConstraint != null &&
constraint is VersionRange &&
constraint.max == null &&
defaultUpperBoundConstraint.allowsAny(constraint)) {
constraint = new VersionConstraint.intersection(
[constraint, defaultUpperBoundConstraint]);
}
return constraint;
});
}
/// Parses [node] to a map from feature names to whether those features are
/// enabled.
Map<String, FeatureDependency> _parseDependencyFeatures(YamlNode node) {
if (node?.value == null) return const {};
if (node is! YamlMap) _error('Features must be a map.', node.span);
return mapMap((node as YamlMap).nodes,
key: (nameNode, _) => _validateFeatureName(nameNode),
value: (_, valueNode) {
var value = valueNode.value;
if (value is bool) {
return value
? FeatureDependency.required
: FeatureDependency.unused;
} else if (value is String && value == "if available") {
return FeatureDependency.ifAvailable;
} else {
_error('Features must be true, false, or "if available".',
valueNode.span);
}
});
}
/// Verifies that [node] is a string and a valid feature name, and returns it
/// if so.
String _validateFeatureName(YamlNode node) {
var name = node.value;
if (name is! String) {
_error('A feature name must be a string.', node.span);
} else if (!_packageName.hasMatch(name)) {
_error('A feature name must be a valid Dart identifier.', node.span);
}
return name;
}
/// Verifies that [node] is a list of strings and returns it.
///
/// If [validate] is passed, it's called for each string in [node].
List<String> _parseStringList(YamlNode node,
{void validate(String value, SourceSpan span)}) {
var list = _parseList(node);
for (var element in list.nodes) {
var value = element.value;
if (value is String) {
if (validate != null) validate(value, element.span);
} else {
_error('Must be a string.', element.span);
}
}
return DelegatingList.typed(list);
}
/// Verifies that [node] is a list and returns it.
YamlList _parseList(YamlNode node) {
if (node == null || node.value == null) return new YamlList();
if (node is YamlList) return node;
_error('Must be a list.', node.span);
}
/// Runs [fn] and wraps any [FormatException] it throws in a
/// [PubspecException].
///
/// [description] should be a noun phrase that describes whatever's being
/// parsed or processed by [fn]. [span] should be the location of whatever's
/// being processed within the pubspec.
T _wrapFormatException<T>(String description, SourceSpan span, T fn()) {
try {
return fn();
} on FormatException catch (e) {
_error('Invalid $description: ${e.message}', span);
}
}
T _wrapSpanFormatException<T>(String description, T fn()) {
try {
return fn();
} on SourceSpanFormatException catch (e) {
_error('Invalid $description: ${e.message}', e.span);
}
}
/// Throws a [PubspecException] with the given message.
@alwaysThrows
void _error(String message, SourceSpan span) {
throw new PubspecException(message, span);
}
}
/// An exception thrown when parsing a pubspec.
///
/// These exceptions are often thrown lazily while accessing pubspec properties.
class PubspecException extends SourceSpanFormatException
implements ApplicationException {
PubspecException(String message, SourceSpan span) : super(message, span);
}
/// Returns whether [uri] is a file URI.
///
/// This is slightly more complicated than just checking if the scheme is
/// 'file', since relative URIs also refer to the filesystem on the VM.
bool _isFileUri(Uri uri) => uri.scheme == 'file' || uri.scheme == '';