Index: sdk/lib/_internal/pub/lib/src/pubspec.dart |
diff --git a/sdk/lib/_internal/pub/lib/src/pubspec.dart b/sdk/lib/_internal/pub/lib/src/pubspec.dart |
index 9a3e22023f9e0e629561b837268d7d122a18775b..1989e17ff184ad63aaba95c9124f0eb5a2528285 100644 |
--- a/sdk/lib/_internal/pub/lib/src/pubspec.dart |
+++ b/sdk/lib/_internal/pub/lib/src/pubspec.dart |
@@ -4,8 +4,9 @@ |
library pub.pubspec; |
-import 'package:yaml/yaml.dart'; |
import 'package:path/path.dart' as path; |
+import 'package:source_maps/source_maps.dart'; |
+import 'package:yaml/yaml.dart'; |
import 'barback/transformer_config.dart'; |
import 'io.dart'; |
@@ -22,7 +23,7 @@ import 'version.dart'; |
/// [allErrors]. |
class Pubspec { |
// If a new lazily-initialized field is added to this class and the |
- // initialization can throw a [PubspecError], that error should also be |
+ // initialization can throw a [PubspecException], that error should also be |
// exposed through [allErrors]. |
/// The registry of sources to use when parsing [dependencies] and |
@@ -35,13 +36,14 @@ class Pubspec { |
/// 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 or can't be represented by a Uri. |
- final Uri _location; |
+ /// is unknown. |
+ Uri get _location => fields.span.sourceUrl == null ? null : |
+ Uri.parse(fields.span.sourceUrl); |
/// All pubspec fields. |
/// |
/// This includes the fields from which other properties are derived. |
- final Map fields; |
+ final YamlMap fields; |
/// The package's name. |
String get name { |
@@ -49,11 +51,11 @@ class Pubspec { |
var name = fields['name']; |
if (name == null) { |
- throw new PubspecException(null, _location, |
- 'Missing the required "name" field.'); |
+ throw new PubspecException( |
+ 'Missing the required "name" field.', fields.span); |
} else if (name is! String) { |
- throw new PubspecException(null, _location, |
- '"name" field must be a string, but was "$name".'); |
+ throw new PubspecException( |
+ '"name" field must be a string.', fields.nodes['name'].span); |
} |
_name = name; |
@@ -70,11 +72,13 @@ class Pubspec { |
_version = Version.none; |
return _version; |
} |
+ |
+ var span = fields.nodes['version'].span; |
if (version is! String) { |
- _error('"version" field must be a string, but was "$version".'); |
+ _error('"version" field must be a string.', span); |
} |
- _version = _wrapFormatException('version number', 'version', |
+ _version = _wrapFormatException('version number', span, |
() => new Version.parse(version)); |
return _version; |
} |
@@ -125,48 +129,46 @@ class Pubspec { |
} |
if (transformers is! List) { |
- _error('"transformers" field must be a list, but was "$transformers".'); |
+ _error('"transformers" field must be a list.', |
+ fields.nodes['transformers'].span); |
} |
var i = 0; |
- _transformers = transformers.map((phase) { |
- var field = "transformers"; |
- if (phase is! List) { |
- phase = [phase]; |
- } else { |
- field = "$field[${i++}]"; |
- } |
- |
- return phase.map((transformer) { |
+ _transformers = 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('"$field" field must be a string or map, but was ' |
- '"$transformer".'); |
+ _error('A transformer must be a string or map.', |
+ transformerNode.span); |
} |
- var library; |
- var configuration; |
+ var libraryNode; |
+ var configurationNode; |
if (transformer is String) { |
- library = transformer; |
+ libraryNode = transformerNode; |
} else { |
if (transformer.length != 1) { |
- _error('"$field" must have a single key: the transformer ' |
- 'identifier. Was "$transformer".'); |
+ _error('A transformer map must have a single key: the transformer ' |
+ 'identifier.', transformerNode.span); |
} else if (transformer.keys.single is! String) { |
- _error('"$field" transformer identifier must be a string, but was ' |
- '"$library".'); |
+ _error('A transformer identifier must be a string.', |
+ transformer.nodes.keys.single.span); |
} |
- library = transformer.keys.single; |
- configuration = transformer.values.single; |
- if (configuration is! Map) { |
- _error('"$field.$library" field must be a map, but was ' |
- '"$configuration".'); |
+ 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 = _wrapFormatException("transformer configuration", |
- "$field.$library", |
- () => new TransformerConfig.parse(library, configuration)); |
+ var config = _wrapSpanFormatException('transformer config', () { |
+ return new TransformerConfig.parse( |
+ libraryNode.value, libraryNode.span, |
+ configurationNode); |
+ }); |
var package = config.id.package; |
if (package != name && |
@@ -174,8 +176,8 @@ class Pubspec { |
!dependencies.any((ref) => ref.name == package) && |
!devDependencies.any((ref) => ref.name == package) && |
!dependencyOverrides.any((ref) => ref.name == package)) { |
- _error('"$field.$library" refers to a package that\'s not a ' |
- 'dependency.'); |
+ _error('"$package" is not a dependency.', |
+ libraryNode.span); |
} |
return config; |
@@ -197,11 +199,12 @@ class Pubspec { |
} |
if (yaml is! Map) { |
- _error('"environment" field must be a map, but was "$yaml".'); |
+ _error('"environment" field must be a map.', |
+ fields.nodes['environment'].span); |
} |
_environment = new PubspecEnvironment( |
- _parseVersionConstraint(yaml['sdk'], 'environment.sdk')); |
+ _parseVersionConstraint(yaml.nodes['sdk'])); |
return _environment; |
} |
PubspecEnvironment _environment; |
@@ -219,35 +222,28 @@ class Pubspec { |
var pubspecPath = path.join(packageDir, 'pubspec.yaml'); |
var pubspecUri = path.toUri(pubspecPath); |
if (!fileExists(pubspecPath)) { |
- throw new PubspecException(expectedName, pubspecUri, |
- 'Could not find a file named "pubspec.yaml" in "$packageDir".'); |
+ fail('Could not find a file named "pubspec.yaml" in "$packageDir".'); |
} |
- try { |
- return new Pubspec.parse(readTextFile(pubspecPath), sources, |
- expectedName: expectedName, location: pubspecUri); |
- } on YamlException catch (error) { |
- throw new PubspecException(expectedName, pubspecUri, error.toString()); |
- } |
+ return new Pubspec.parse(readTextFile(pubspecPath), sources, |
+ expectedName: expectedName, location: pubspecUri); |
} |
Pubspec(this._name, this._version, this._dependencies, this._devDependencies, |
this._dependencyOverrides, this._environment, this._transformers, |
[Map fields]) |
- : this.fields = fields == null ? {} : fields, |
- _sources = null, |
- _location = null; |
+ : this.fields = fields == null ? new YamlMap() : fields, |
+ _sources = null; |
Pubspec.empty() |
: _sources = null, |
- _location = null, |
_name = null, |
_version = Version.none, |
_dependencies = <PackageDep>[], |
_devDependencies = <PackageDep>[], |
_environment = new PubspecEnvironment(), |
_transformers = <Set<TransformerConfig>>[], |
- fields = {}; |
+ fields = new YamlMap(); |
/// Returns a Pubspec object for an already-parsed map representing its |
/// contents. |
@@ -256,33 +252,17 @@ class Pubspec { |
/// field, this will throw a [PubspecError]. |
/// |
/// [location] is the location from which this pubspec was loaded. |
- Pubspec.fromMap(this.fields, this._sources, {String expectedName, |
+ Pubspec.fromMap(Map fields, this._sources, {String expectedName, |
Uri location}) |
- : _location = location { |
- if (expectedName == null) return; |
- |
+ : fields = fields is YamlMap ? fields : new YamlMap.wrap(fields, |
+ sourceName: location == null ? null : location.toString()) { |
// If [expectedName] is passed, ensure that the actual 'name' field exists |
// and matches the expectation. |
+ if (expectedName == null) return; |
+ if (name == expectedName) return; |
- // If the 'name' field doesn't exist, manually throw an exception rather |
- // than relying on the exception thrown by [name] so that we can provide a |
- // suggested fix. |
- if (fields['name'] == null) { |
- throw new PubspecException(expectedName, _location, |
- 'Missing the required "name" field (e.g. "name: $expectedName").'); |
- } |
- |
- try { |
- if (name == expectedName) return; |
- throw new PubspecException(expectedName, _location, |
- '"name" field "$name" doesn\'t match expected name ' |
- '"$expectedName".'); |
- } on PubspecException catch (e) { |
- // Catch and re-throw any exceptions thrown by [name] so that they refer |
- // to [expectedName] for additional context. |
- throw new PubspecException(expectedName, e.location, |
- split1(e.message, '\n').last); |
- } |
+ 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]. |
@@ -293,15 +273,15 @@ class Pubspec { |
{String expectedName, Uri location}) { |
if (contents.trim() == '') return new Pubspec.empty(); |
- var parsedPubspec = loadYaml(contents); |
- if (parsedPubspec == null) return new Pubspec.empty(); |
- |
- if (parsedPubspec is! Map) { |
- throw new PubspecException(expectedName, location, |
- 'The pubspec must be a YAML mapping.'); |
+ var pubspecNode = loadYamlNode(contents, sourceName: location.toString()); |
+ if (pubspecNode is YamlScalar && pubspecNode.value == null) { |
+ pubspecNode = new YamlMap(); |
+ } else if (pubspecNode is! YamlMap) { |
+ throw new PubspecException( |
+ 'The pubspec must be a YAML mapping.', pubspecNode.span); |
} |
- return new Pubspec.fromMap(parsedPubspec, sources, |
+ return new Pubspec.fromMap(pubspecNode, sources, |
expectedName: expectedName, location: location); |
} |
@@ -336,65 +316,72 @@ class Pubspec { |
// Allow an empty dependencies key. |
if (yaml == null) return dependencies; |
- if (yaml is! Map || yaml.keys.any((e) => e is! String)) { |
- _error('"$field" field should be a map of package names, but was ' |
- '"$yaml".'); |
+ if (yaml is! Map) { |
+ _error('"$field" field must be a map.', fields.nodes[field].span); |
} |
- yaml.forEach((name, spec) { |
+ var nonStringNode = yaml.nodes.keys.firstWhere((e) => e.value is! String, |
+ orElse: () => null); |
+ if (nonStringNode != null) { |
+ _error('A dependency name must be a string.', nonStringNode.span); |
+ } |
+ |
+ yaml.nodes.forEach((nameNode, specNode) { |
+ var name = nameNode.value; |
+ var spec = specNode.value; |
if (fields['name'] != null && name == this.name) { |
- _error('"$field.$name": Package may not list itself as a ' |
- 'dependency.'); |
+ _error('A package may not list itself as a dependency.', |
+ nameNode.span); |
} |
- var description; |
+ var descriptionNode; |
var sourceName; |
var versionConstraint = new VersionRange(); |
if (spec == null) { |
- description = name; |
+ descriptionNode = nameNode; |
sourceName = _sources.defaultSource.name; |
} else if (spec is String) { |
- description = name; |
+ descriptionNode = nameNode; |
sourceName = _sources.defaultSource.name; |
- versionConstraint = _parseVersionConstraint(spec, "$field.$name"); |
+ versionConstraint = _parseVersionConstraint(specNode); |
} else if (spec is Map) { |
// Don't write to the immutable YAML map. |
spec = new Map.from(spec); |
if (spec.containsKey('version')) { |
- versionConstraint = _parseVersionConstraint(spec.remove('version'), |
- "$field.$name.version"); |
+ spec.remove('version'); |
+ versionConstraint = _parseVersionConstraint( |
+ specNode.nodes['version']); |
} |
var sourceNames = spec.keys.toList(); |
if (sourceNames.length > 1) { |
- _error('"$field.$name" field may only have one source, but it had ' |
- '${toSentence(sourceNames)}.'); |
+ _error('A dependency may only have one source.', specNode.span); |
} |
sourceName = sourceNames.single; |
if (sourceName is! String) { |
- _error('"$field.$name" source name must be a string, but was ' |
- '"$sourceName".'); |
+ _error('A source name must be a string.', |
+ specNode.nodes.keys.single.span); |
} |
- description = spec[sourceName]; |
+ descriptionNode = specNode.nodes[sourceName]; |
} else { |
- _error('"$field.$name" field must be a string or a mapping.'); |
+ _error('A dependency specification must be a string or a mapping.', |
+ specNode.span); |
} |
// Let the source validate the description. |
- var descriptionField = "$field.$name"; |
- if (spec is Map) descriptionField = "$descriptionField.$sourceName"; |
- _wrapFormatException('description', descriptionField, () { |
+ var description = _wrapFormatException('description', |
+ descriptionNode.span, () { |
var pubspecPath; |
if (_location != null && _isFileUri(_location)) { |
pubspecPath = path.fromUri(_location); |
} |
- description = _sources[sourceName].parseDescription( |
- pubspecPath, description, fromLockFile: false); |
+ return _sources[sourceName].parseDescription( |
+ pubspecPath, descriptionNode.value, fromLockFile: false); |
}); |
dependencies.add(new PackageDep( |
@@ -404,17 +391,15 @@ class Pubspec { |
return dependencies; |
} |
- /// Parses [yaml] to a [VersionConstraint]. |
- /// |
- /// If [yaml] is `null`, returns [VersionConstraint.any]. |
- VersionConstraint _parseVersionConstraint(yaml, String field) { |
- if (yaml == null) return VersionConstraint.any; |
- if (yaml is! String) { |
- _error('"$field" must be a string, but was "$yaml".'); |
+ /// Parses [node] to a [VersionConstraint]. |
+ VersionConstraint _parseVersionConstraint(YamlNode node) { |
+ if (node.value == null) return VersionConstraint.any; |
+ if (node.value is! String) { |
+ _error('A version constraint must be a string.', node.span); |
} |
- return _wrapFormatException('version constraint', field, |
- () => new VersionConstraint.parse(yaml)); |
+ return _wrapFormatException('version constraint', node.span, |
+ () => new VersionConstraint.parse(node.value)); |
} |
/// Makes sure the same package doesn't appear as both a regular and dev |
@@ -426,27 +411,41 @@ class Pubspec { |
devDependencies.map((dep) => dep.name).toSet()); |
if (collisions.isEmpty) return; |
+ var span = fields["dependencies"].nodes.keys |
+ .firstWhere((key) => collisions.contains(key.value)).span; |
+ |
+ // TODO(nweiz): associate source range info with PackageDeps and use it |
+ // here. |
_error('${pluralize('Package', collisions.length)} ' |
'${toSentence(collisions.map((package) => '"$package"'))} cannot ' |
- 'appear in both "dependencies" and "dev_dependencies".'); |
+ 'appear in both "dependencies" and "dev_dependencies".', |
+ 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]. [field] should be the location of whatever's |
+ /// parsed or processed by [fn]. [span] should be the location of whatever's |
/// being processed within the pubspec. |
- _wrapFormatException(String description, String field, fn()) { |
+ _wrapFormatException(String description, Span span, fn()) { |
try { |
return fn(); |
} on FormatException catch (e) { |
- _error('Invalid $description for "$field": ${e.message}'); |
+ _error('Invalid $description: ${e.message}', span); |
+ } |
+ } |
+ |
+ _wrapSpanFormatException(String description, fn()) { |
+ try { |
+ return fn(); |
+ } on SpanFormatException catch (e) { |
+ _error('Invalid $description: ${e.message}', e.span); |
} |
} |
/// Throws a [PubspecException] with the given message. |
- void _error(String message) { |
+ void _error(String message, Span span) { |
var name; |
try { |
name = this.name; |
@@ -454,7 +453,7 @@ class Pubspec { |
// [name] is null. |
} |
- throw new PubspecException(name, _location, message); |
+ throw new PubspecException(message, span); |
} |
} |
@@ -473,46 +472,13 @@ class PubspecEnvironment { |
/// An exception thrown when parsing a pubspec. |
/// |
/// These exceptions are often thrown lazily while accessing pubspec properties. |
-/// Their string representation contains additional contextual information about |
-/// the pubspec for which parsing failed. |
-class PubspecException extends ApplicationException { |
- /// The name of the package that the pubspec is for. |
- /// |
- /// This can be null if the pubspec didn't specify a name and no external name |
- /// was provided. |
- final String name; |
+class PubspecException extends SpanFormatException |
+ implements ApplicationException { |
+ final innerError = null; |
+ final innerTrace = null; |
- /// The location of the pubspec. |
- /// |
- /// This can be null if the pubspec has no physical location, or if the |
- /// location is unknown. |
- final Uri location; |
- |
- PubspecException(String name, Uri location, String subMessage) |
- : this.name = name, |
- this.location = location, |
- super(_computeMessage(name, location, subMessage)); |
- |
- static String _computeMessage(String name, Uri location, String subMessage) { |
- var str = 'Error in'; |
- |
- if (name != null) { |
- str += ' pubspec for package "$name"'; |
- if (location != null) str += ' loaded from'; |
- } else if (location == null) { |
- str += ' pubspec for an unknown package'; |
- } |
- |
- if (location != null) { |
- if (_isFileUri(location)) { |
- str += ' ${nicePath(path.fromUri(location))}'; |
- } else { |
- str += ' $location'; |
- } |
- } |
- |
- return "$str:\n$subMessage"; |
- } |
+ PubspecException(String message, Span span) |
+ : super(message, span); |
} |
/// Returns whether [uri] is a file URI. |