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 0813315facd062dd45b52f0130ea45d3aac712d2..c586c81c67daabeaec2cc8f5934fa2a3f55f04d3 100644 |
--- a/sdk/lib/_internal/pub/lib/src/pubspec.dart |
+++ b/sdk/lib/_internal/pub/lib/src/pubspec.dart |
@@ -16,373 +16,425 @@ import 'source_registry.dart'; |
import 'utils.dart'; |
import 'version.dart'; |
-/// The parsed and validated contents of a pubspec file. |
-class Pubspec { |
- /// This package's name. |
- final String name; |
- |
- /// This package's version. |
- final Version version; |
+import 'log.dart' as log; |
- /// The packages this package depends on. |
- final List<PackageDep> dependencies; |
- |
- /// The packages this package depends on when it is the root package. |
- final List<PackageDep> devDependencies; |
+/// 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 [PubspecError], that error should also be |
+ // exposed through [allErrors]. |
- /// The ids of the transformers to use for this package. |
- final List<Set<TransformerId>> transformers; |
+ /// 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 environment-related metadata. |
- final PubspecEnvironment environment; |
+ /// 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; |
/// All pubspec fields. This includes the fields from which other properties |
/// are derived. |
- final Map<String, Object> fields; |
- |
- /// Loads the pubspec for a package [name] located in [packageDir]. |
- factory Pubspec.load(String name, String packageDir, SourceRegistry sources) { |
- var pubspecPath = path.join(packageDir, 'pubspec.yaml'); |
- if (!fileExists(pubspecPath)) throw new PubspecNotFoundException(name); |
- |
- try { |
- var pubspec = new Pubspec.parse(pubspecPath, readTextFile(pubspecPath), |
- sources); |
- |
- if (pubspec.name == null) { |
- throw new PubspecHasNoNameException(name); |
- } |
- |
- if (name != null && pubspec.name != name) { |
- throw new PubspecNameMismatchException(name, pubspec.name); |
- } |
- |
- return pubspec; |
- } on FormatException catch (ex) { |
- fail('Could not parse $pubspecPath:\n${ex.message}'); |
+ final Map fields; |
+ |
+ /// The package's name. |
+ String get name { |
+ if (_name != null) return _name; |
+ |
+ var name = fields['name']; |
+ if (name == null) { |
+ throw new PubspecException(null, _location, |
+ 'Missing the required "name" field.'); |
+ } else if (name is! String) { |
+ throw new PubspecException(null, _location, |
+ '"name" field must be a string, but was "$name".'); |
} |
- } |
- |
- Pubspec(this.name, this.version, this.dependencies, this.devDependencies, |
- this.environment, this.transformers, [Map<String, Object> fields]) |
- : this.fields = fields == null ? {} : fields; |
- |
- Pubspec.empty() |
- : name = null, |
- version = Version.none, |
- dependencies = <PackageDep>[], |
- devDependencies = <PackageDep>[], |
- environment = new PubspecEnvironment(), |
- transformers = <Set<TransformerId>>[], |
- fields = {}; |
- |
- /// Whether or not the pubspec has no contents. |
- bool get isEmpty => |
- name == null && version == Version.none && dependencies.isEmpty; |
- /// Returns a Pubspec object for an already-parsed map representing its |
- /// contents. |
- /// |
- /// This will validate that [contents] is a valid pubspec. |
- factory Pubspec.fromMap(Map contents, SourceRegistry sources) => |
- _parseMap(null, contents, sources); |
- |
- // TODO(rnystrom): Instead of allowing a null argument here, split this up |
- // into load(), parse(), and _parse() like LockFile does. |
- /// Parses the pubspec stored at [filePath] whose text is [contents]. If the |
- /// pubspec doesn't define version for itself, it defaults to [Version.none]. |
- /// [filePath] may be `null` if the pubspec is not on the user's local |
- /// file system. |
- factory Pubspec.parse(String filePath, String contents, |
- SourceRegistry sources) { |
- if (contents.trim() == '') return new Pubspec.empty(); |
+ _name = name; |
+ return _name; |
+ } |
+ String _name; |
- var parsedPubspec = loadYaml(contents); |
- if (parsedPubspec == null) return new Pubspec.empty(); |
+ /// The package's version. |
+ Version get version { |
+ if (_version != null) return _version; |
- if (parsedPubspec is! Map) { |
- throw new FormatException('The pubspec must be a YAML mapping.'); |
+ var version = fields['version']; |
+ if (version == null) { |
+ _version = Version.none; |
+ return _version; |
+ } |
+ if (version is! String) { |
+ _error('"version" field must be a string, but was "$version".'); |
} |
- return _parseMap(filePath, parsedPubspec, sources); |
- } |
-} |
- |
-/// Evaluates whether the given [url] for [field] is valid. |
-/// |
-/// Throws [FormatException] on an invalid url. |
-void _validateFieldUrl(url, String field) { |
- if (url is! String) { |
- throw new FormatException( |
- 'The "$field" field should be a string, but was "$url".'); |
+ _version = _wrapFormatException('version number', 'version', |
+ () => new Version.parse(version)); |
+ return _version; |
} |
- |
- var goodScheme = new RegExp(r'^https?:'); |
- if (!goodScheme.hasMatch(url)) { |
- throw new FormatException( |
- 'The "$field" field should be an "http:" or "https:" URL, but ' |
- 'was "$url".'); |
+ Version _version; |
+ |
+ /// The additional packages this package depends on. |
+ List<PackageDep> get dependencies { |
+ if (_dependencies != null) return _dependencies; |
+ _dependencies = _parseDependencies('dependencies'); |
+ if (_devDependencies == null) { |
+ _checkDependencyOverlap(_dependencies, devDependencies); |
+ } |
+ return _dependencies; |
} |
-} |
- |
-Pubspec _parseMap(String filePath, Map map, SourceRegistry sources) { |
- var name = null; |
+ List<PackageDep> _dependencies; |
- if (map.containsKey('name')) { |
- name = map['name']; |
- if (name is! String) { |
- throw new FormatException( |
- 'The pubspec "name" field should be a string, but was "$name".'); |
+ /// The packages this package depends on when it is the root package. |
+ List<PackageDep> get devDependencies { |
+ if (_devDependencies != null) return _devDependencies; |
+ _devDependencies = _parseDependencies('dev_dependencies'); |
+ if (_dependencies == null) { |
+ _checkDependencyOverlap(dependencies, _devDependencies); |
} |
+ return _devDependencies; |
} |
+ List<PackageDep> _devDependencies; |
- var version = _parseVersion(map['version'], (v) => |
- 'The pubspec "version" field should be a semantic version number, ' |
- 'but was "$v".'); |
- |
- var dependencies = _parseDependencies(name, filePath, sources, |
- map['dependencies']); |
- |
- var devDependencies = _parseDependencies(name, filePath, sources, |
- map['dev_dependencies']); |
- |
- // Make sure the same package doesn't appear as both a regular and dev |
- // dependency. |
- var dependencyNames = dependencies.map((dep) => dep.name).toSet(); |
- var collisions = dependencyNames.intersection( |
- devDependencies.map((dep) => dep.name).toSet()); |
- |
- if (!collisions.isEmpty) { |
- var packageNames; |
- if (collisions.length == 1) { |
- packageNames = 'Package "${collisions.first}"'; |
- } else { |
- var names = ordered(collisions); |
- var buffer = new StringBuffer(); |
- buffer.write("Packages "); |
- for (var i = 0; i < names.length; i++) { |
- buffer.write('"'); |
- buffer.write(names[i]); |
- buffer.write('"'); |
- if (i == names.length - 2) { |
- buffer.write(", "); |
- } else if (i == names.length - 1) { |
- buffer.write(", and "); |
- } |
- } |
+ /// The ids of the transformers to use for this package. |
+ List<Set<TransformerId>> get transformers { |
+ if (_transformers != null) return _transformers; |
- packageNames = buffer.toString(); |
+ var transformers = fields['transformers']; |
+ if (transformers == null) { |
+ _transformers = []; |
+ return _transformers; |
} |
- throw new FormatException( |
- '$packageNames cannot appear in both "dependencies" and ' |
- '"dev_dependencies".'); |
- } |
- |
- var transformers = map['transformers']; |
- if (transformers != null) { |
if (transformers is! List) { |
- throw new FormatException('"transformers" field must be a list, but was ' |
- '"$transformers".'); |
+ _error('"transformers" field must be a list, but was "$transformers".'); |
} |
- transformers = transformers.map((phase) { |
- if (phase is! List) phase = [phase]; |
+ var i = 0; |
+ _transformers = transformers.map((phase) { |
+ var field = "transformers"; |
+ if (phase is! List) { |
+ phase = [phase]; |
+ } else { |
+ field = "$field[${i++}]"; |
+ } |
+ |
return phase.map((transformer) { |
if (transformer is! String && transformer is! Map) { |
- throw new FormatException( |
- 'Transformer "$transformer" must be a string or map.'); |
+ _error('"$field" field must be a string or map, but was ' |
+ '"$transformer".'); |
} |
var id; |
var configuration; |
if (transformer is String) { |
- id = libraryIdentifierToId(transformer); |
+ id = _wrapFormatException('library identifier', field, |
+ () => libraryIdentifierToId(transformer)); |
} else { |
if (transformer.length != 1) { |
- throw new FormatException('Transformer map "$transformer" must ' |
- 'have a single key: the library identifier.'); |
+ _error('"$field" must have a single key: the library identifier.'); |
+ } else if (transformer.keys.single is! String) { |
+ _error('"$field" library identifier must be a string, but was ' |
+ '"$id".'); |
} |
- id = libraryIdentifierToId(transformer.keys.single); |
+ id = _wrapFormatException('library identifier', field, |
+ () => libraryIdentifierToId(transformer.keys.single)); |
configuration = transformer.values.single; |
if (configuration is! Map) { |
- throw new FormatException('Configuration for transformer "$id" ' |
- 'must be a map, but was "$configuration".'); |
+ _error('"$field.${idToLibraryIdentifier(id)}" field must be a map, ' |
+ 'but was "$configuration".'); |
} |
} |
if (id.package != name && |
!dependencies.any((ref) => ref.name == id.package)) { |
- throw new FormatException('Could not find package for transformer ' |
- '"$transformer".'); |
+ _error('"$field.${idToLibraryIdentifier(id)}" refers to a package ' |
+ 'that\'s not listed in "dependencies".'); |
} |
- return new TransformerId(id, configuration); |
+ return _wrapFormatException("transformer identifier", |
+ "$field.${idToLibraryIdentifier(id)}", |
+ () => new TransformerId(id, configuration)); |
}).toSet(); |
}).toList(); |
- } else { |
- transformers = []; |
+ return _transformers; |
} |
+ List<Set<TransformerId>> _transformers; |
- var environmentYaml = map['environment']; |
- var sdkConstraint = VersionConstraint.any; |
- if (environmentYaml != null) { |
- if (environmentYaml is! Map) { |
- throw new FormatException( |
- 'The pubspec "environment" field should be a map, but was ' |
- '"$environmentYaml".'); |
+ /// The environment-related metadata. |
+ PubspecEnvironment get environment { |
+ if (_environment != null) return _environment; |
+ |
+ var yaml = fields['environment']; |
+ if (yaml == null) { |
+ _environment = new PubspecEnvironment(VersionConstraint.any); |
+ return _environment; |
} |
- sdkConstraint = _parseVersionConstraint(environmentYaml['sdk'], (v) => |
- 'The "sdk" field of "environment" should be a semantic version ' |
- 'constraint, but was "$v".'); |
- } |
- var environment = new PubspecEnvironment(sdkConstraint); |
- |
- // Even though the pub app itself doesn't use these fields, we validate |
- // them here so that users find errors early before they try to upload to |
- // the server: |
- // TODO(rnystrom): We should split this validation into separate layers: |
- // 1. Stuff that is required in any pubspec to perform any command. Things |
- // like "must have a name". That should go here. |
- // 2. Stuff that is required to upload a package. Things like "homepage |
- // must use a valid scheme". That should go elsewhere. pub upload should |
- // call it, and we should provide a separate command to show the user, |
- // and also expose it to the editor in some way. |
- |
- if (map.containsKey('homepage')) { |
- _validateFieldUrl(map['homepage'], 'homepage'); |
- } |
- if (map.containsKey('documentation')) { |
- _validateFieldUrl(map['documentation'], 'documentation'); |
+ if (yaml is! Map) { |
+ _error('"environment" field must be a map, but was "$yaml".'); |
+ } |
+ |
+ _environment = new PubspecEnvironment( |
+ _parseVersionConstraint(yaml['sdk'], 'environment.sdk')); |
+ return _environment; |
} |
+ PubspecEnvironment _environment; |
+ |
+ /// 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}) { |
+ 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".'); |
+ } |
- if (map.containsKey('author') && map['author'] is! String) { |
- throw new FormatException( |
- 'The "author" field should be a string, but was ' |
- '${map["author"]}.'); |
+ return new Pubspec.parse(readTextFile(pubspecPath), sources, |
+ expectedName: expectedName, location: pubspecUri); |
} |
- if (map.containsKey('authors')) { |
- var authors = map['authors']; |
- if (authors is List) { |
- // All of the elements must be strings. |
- if (!authors.every((author) => author is String)) { |
- throw new FormatException('The "authors" field should be a string ' |
- 'or a list of strings, but was "$authors".'); |
- } |
- } else if (authors is! String) { |
- throw new FormatException('The pubspec "authors" field should be a ' |
- 'string or a list of strings, but was "$authors".'); |
+ Pubspec(this._name, this._version, this._dependencies, this._devDependencies, |
+ this._environment, this._transformers, [Map fields]) |
+ : this.fields = fields == null ? {} : fields, |
+ _sources = null, |
+ _location = null; |
+ |
+ Pubspec.empty() |
+ : _sources = null, |
+ _location = null, |
+ _name = null, |
+ _version = Version.none, |
+ _dependencies = <PackageDep>[], |
+ _devDependencies = <PackageDep>[], |
+ _environment = new PubspecEnvironment(), |
+ _transformers = <Set<TransformerId>>[], |
+ fields = {}; |
+ |
+ /// 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(this.fields, this._sources, {String expectedName, |
+ Uri location}) |
+ : _location = location { |
+ if (expectedName == null) return; |
+ |
+ // If [expectedName] is passed, ensure that the actual 'name' field exists |
+ // and matches the expectation. |
+ |
+ // 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").'); |
} |
- if (map.containsKey('author')) { |
- throw new FormatException('A pubspec should not have both an "author" ' |
- 'and an "authors" field.'); |
+ 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); |
} |
} |
- return new Pubspec(name, version, dependencies, devDependencies, |
- environment, transformers, map); |
-} |
+ /// Parses the pubspec stored at [filePath] whose text is [contents]. If the |
+ /// pubspec doesn't define version for itself, it defaults to [Version.none]. |
+ /// [filePath] may be `null` if the pubspec is not on the user's local |
+ /// file system. |
+ factory Pubspec.parse(String contents, SourceRegistry sources, |
+ {String expectedName, Uri location}) { |
+ if (contents.trim() == '') return new Pubspec.empty(); |
-/// Parses [yaml] to a [Version] or throws a [FormatException] with the result |
-/// of calling [message] if it isn't valid. |
-/// |
-/// If [yaml] is `null`, returns [Version.none]. |
-Version _parseVersion(yaml, String message(yaml)) { |
- if (yaml == null) return Version.none; |
- if (yaml is! String) throw new FormatException(message(yaml)); |
- |
- try { |
- return new Version.parse(yaml); |
- } on FormatException catch(_) { |
- throw new FormatException(message(yaml)); |
- } |
-} |
+ var parsedPubspec = loadYaml(contents); |
+ if (parsedPubspec == null) return new Pubspec.empty(); |
-/// Parses [yaml] to a [VersionConstraint] or throws a [FormatException] with |
-/// the result of calling [message] if it isn't valid. |
-/// |
-/// If [yaml] is `null`, returns [VersionConstraint.any]. |
-VersionConstraint _parseVersionConstraint(yaml, String getMessage(yaml)) { |
- if (yaml == null) return VersionConstraint.any; |
- if (yaml is! String) throw new FormatException(getMessage(yaml)); |
- |
- try { |
- return new VersionConstraint.parse(yaml); |
- } on FormatException catch(_) { |
- throw new FormatException(getMessage(yaml)); |
- } |
-} |
+ if (parsedPubspec is! Map) { |
+ throw new PubspecException(expectedName, location, |
+ 'The pubspec must be a YAML mapping.'); |
+ } |
-List<PackageDep> _parseDependencies(String packageName, String pubspecPath, |
- SourceRegistry sources, yaml) { |
- var dependencies = <PackageDep>[]; |
+ return new Pubspec.fromMap(parsedPubspec, sources, |
+ expectedName: expectedName, location: location); |
+ } |
- // Allow an empty dependencies key. |
- if (yaml == null) return dependencies; |
+ /// 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); |
+ } |
+ } |
- if (yaml is! Map || yaml.keys.any((e) => e is! String)) { |
- throw new FormatException( |
- 'The pubspec dependencies should be a map of package names, but ' |
- 'was ${yaml}.'); |
+ _getError(() => this.name); |
+ _getError(() => this.version); |
+ _getError(() => this.dependencies); |
+ _getError(() => this.devDependencies); |
+ _getError(() => this.transformers); |
+ _getError(() => this.environment); |
+ return errors; |
} |
- yaml.forEach((name, spec) { |
- if (name == packageName) { |
- throw new FormatException("Package '$name' cannot depend on itself."); |
+ /// Parses the dependency field named [field], and returns the corresponding |
+ /// list of dependencies. |
+ List<PackageDep> _parseDependencies(String field) { |
+ var dependencies = <PackageDep>[]; |
+ |
+ var yaml = fields[field]; |
+ // 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".'); |
} |
- var description; |
- var sourceName; |
- |
- var versionConstraint = new VersionRange(); |
- if (spec == null) { |
- description = name; |
- sourceName = sources.defaultSource.name; |
- } else if (spec is String) { |
- description = name; |
- sourceName = sources.defaultSource.name; |
- versionConstraint = new VersionConstraint.parse(spec); |
- } else if (spec is Map) { |
- if (spec.containsKey('version')) { |
- versionConstraint = _parseVersionConstraint(spec.remove('version'), |
- (v) => 'The "version" field for $name should be a semantic ' |
- 'version constraint, but was "$v".'); |
+ yaml.forEach((name, spec) { |
+ if (fields['name'] != null && name == this.name) { |
+ _error('"$field.$name": Package may not list itself as a ' |
+ 'dependency.'); |
} |
- var sourceNames = spec.keys.toList(); |
- if (sourceNames.length > 1) { |
- throw new FormatException( |
- 'Dependency $name may only have one source: $sourceNames.'); |
+ var description; |
+ var sourceName; |
+ |
+ var versionConstraint = new VersionRange(); |
+ if (spec == null) { |
+ description = name; |
+ sourceName = _sources.defaultSource.name; |
+ } else if (spec is String) { |
+ description = name; |
+ sourceName = _sources.defaultSource.name; |
+ versionConstraint = _parseVersionConstraint(spec, "$field.$name"); |
+ } else if (spec is Map) { |
+ if (spec.containsKey('version')) { |
+ versionConstraint = _parseVersionConstraint(spec.remove('version'), |
+ "$field.$name.version"); |
+ } |
+ |
+ var sourceNames = spec.keys.toList(); |
+ if (sourceNames.length > 1) { |
+ _error('"$field.$name" field may only have one source, but it had ' |
+ '${toSentence(sourceNames)}.'); |
+ } |
+ |
+ sourceName = sourceNames.single; |
+ if (sourceName is! String) { |
+ _error('"$field.$name" source name must be a string, but was ' |
+ '"$sourceName".'); |
+ } |
+ |
+ description = spec[sourceName]; |
+ } else { |
+ _error('"$field.$name" field must be a string or a mapping.'); |
} |
- sourceName = only(sourceNames); |
- if (sourceName is! String) { |
- throw new FormatException( |
- 'Source name $sourceName should be a string.'); |
+ // If we have a valid source, use it to process the description. Allow |
+ // unknown sources so pub doesn't choke on old pubspecs. |
+ if (_sources.contains(sourceName)) { |
+ var descriptionField = "$field.$name"; |
+ if (spec is Map) descriptionField = "$descriptionField.$sourceName"; |
+ _wrapFormatException('description', descriptionField, () { |
+ var pubspecPath; |
+ if (_location != null && _isFileUri(_location)) { |
+ pubspecPath = path.fromUri(_location); |
+ } |
+ description = _sources[sourceName].parseDescription( |
+ pubspecPath, description, fromLockFile: false); |
+ }); |
} |
- description = spec[sourceName]; |
- } else { |
- throw new FormatException( |
- 'Dependency specification $spec should be a string or a mapping.'); |
+ dependencies.add(new PackageDep( |
+ name, sourceName, versionConstraint, description)); |
+ }); |
+ |
+ 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".'); |
} |
- // If we have a valid source, use it to process the description. Allow |
- // unknown sources so pub doesn't choke on old pubspecs. |
- if (sources.contains(sourceName)) { |
- description = sources[sourceName].parseDescription( |
- pubspecPath, description, fromLockFile: false); |
+ return _wrapFormatException('version constraint', field, |
+ () => new VersionConstraint.parse(yaml)); |
+ } |
+ |
+ // Make sure the same package doesn't appear as both a regular and dev |
+ // dependency. |
+ void _checkDependencyOverlap(List<PackageDep> dependencies, |
+ List<PackageDep> devDependencies) { |
+ var dependencyNames = dependencies.map((dep) => dep.name).toSet(); |
+ var collisions = dependencyNames.intersection( |
+ devDependencies.map((dep) => dep.name).toSet()); |
+ if (collisions.isEmpty) return; |
+ |
+ _error('${pluralize('Package', collisions.length)} ' |
+ '${toSentence(collisions.map((package) => '"$package"'))} cannot ' |
+ 'appear in both "dependencies" and "dev_dependencies".'); |
+ } |
+ |
+ /// 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 |
+ /// being processed within the pubspec. |
+ _wrapFormatException(String description, String field, fn()) { |
+ try { |
+ return fn(); |
+ } on FormatException catch (e) { |
+ _error('Invalid $description for "$field": ${e.message}'); |
} |
+ } |
- dependencies.add(new PackageDep( |
- name, sourceName, versionConstraint, description)); |
- }); |
+ /// Throws a [PubspecException] with the given message. |
+ void _error(String message) { |
+ var name; |
+ try { |
+ name = this.name; |
+ } on PubspecException catch (_) { |
+ // [name] is null. |
+ } |
- return dependencies; |
+ throw new PubspecException(name, _location, message); |
+ } |
} |
/// The environment-related metadata in the pubspec. Corresponds to the data |
@@ -395,3 +447,54 @@ class PubspecEnvironment { |
PubspecEnvironment([VersionConstraint sdk]) |
: sdkVersion = sdk != null ? sdk : VersionConstraint.any; |
} |
+ |
+/// 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; |
+ |
+ /// 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"; |
+ } |
+} |
+ |
+/// 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 == ''; |