| OLD | NEW |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library pubspec; | 5 library pubspec; |
| 6 | 6 |
| 7 import 'package:yaml/yaml.dart'; | 7 import 'package:yaml/yaml.dart'; |
| 8 import 'package:pathos/path.dart' as path; | 8 import 'package:pathos/path.dart' as path; |
| 9 | 9 |
| 10 import 'io.dart'; | 10 import 'io.dart'; |
| (...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 67 version = Version.none, | 67 version = Version.none, |
| 68 dependencies = <PackageDep>[], | 68 dependencies = <PackageDep>[], |
| 69 devDependencies = <PackageDep>[], | 69 devDependencies = <PackageDep>[], |
| 70 environment = new PubspecEnvironment(), | 70 environment = new PubspecEnvironment(), |
| 71 fields = {}; | 71 fields = {}; |
| 72 | 72 |
| 73 /// Whether or not the pubspec has no contents. | 73 /// Whether or not the pubspec has no contents. |
| 74 bool get isEmpty => | 74 bool get isEmpty => |
| 75 name == null && version == Version.none && dependencies.isEmpty; | 75 name == null && version == Version.none && dependencies.isEmpty; |
| 76 | 76 |
| 77 /// Returns a Pubspec object for an already-parsed map representing its |
| 78 /// contents. |
| 79 /// |
| 80 /// This will validate that [contents] is a valid pubspec. |
| 81 factory Pubspec.fromMap(Map contents, SourceRegistry sources) => |
| 82 _parseMap(null, contents, sources); |
| 83 |
| 77 // TODO(rnystrom): Instead of allowing a null argument here, split this up | 84 // TODO(rnystrom): Instead of allowing a null argument here, split this up |
| 78 // into load(), parse(), and _parse() like LockFile does. | 85 // into load(), parse(), and _parse() like LockFile does. |
| 79 /// Parses the pubspec stored at [filePath] whose text is [contents]. If the | 86 /// Parses the pubspec stored at [filePath] whose text is [contents]. If the |
| 80 /// pubspec doesn't define version for itself, it defaults to [Version.none]. | 87 /// pubspec doesn't define version for itself, it defaults to [Version.none]. |
| 81 /// [filePath] may be `null` if the pubspec is not on the user's local | 88 /// [filePath] may be `null` if the pubspec is not on the user's local |
| 82 /// file system. | 89 /// file system. |
| 83 factory Pubspec.parse(String filePath, String contents, | 90 factory Pubspec.parse(String filePath, String contents, |
| 84 SourceRegistry sources) { | 91 SourceRegistry sources) { |
| 85 var name = null; | |
| 86 var version = Version.none; | |
| 87 | |
| 88 if (contents.trim() == '') return new Pubspec.empty(); | 92 if (contents.trim() == '') return new Pubspec.empty(); |
| 89 | 93 |
| 90 var parsedPubspec = loadYaml(contents); | 94 var parsedPubspec = loadYaml(contents); |
| 91 if (parsedPubspec == null) return new Pubspec.empty(); | 95 if (parsedPubspec == null) return new Pubspec.empty(); |
| 92 | 96 |
| 93 if (parsedPubspec is! Map) { | 97 if (parsedPubspec is! Map) { |
| 94 throw new FormatException('The pubspec must be a YAML mapping.'); | 98 throw new FormatException('The pubspec must be a YAML mapping.'); |
| 95 } | 99 } |
| 96 | 100 |
| 97 if (parsedPubspec.containsKey('name')) { | 101 return _parseMap(filePath, parsedPubspec, sources); |
| 98 name = parsedPubspec['name']; | |
| 99 if (name is! String) { | |
| 100 throw new FormatException( | |
| 101 'The pubspec "name" field should be a string, but was "$name".'); | |
| 102 } | |
| 103 } | |
| 104 | |
| 105 if (parsedPubspec.containsKey('version')) { | |
| 106 version = new Version.parse(parsedPubspec['version']); | |
| 107 } | |
| 108 | |
| 109 var dependencies = _parseDependencies(filePath, sources, | |
| 110 parsedPubspec['dependencies']); | |
| 111 | |
| 112 var devDependencies = _parseDependencies(filePath, sources, | |
| 113 parsedPubspec['dev_dependencies']); | |
| 114 | |
| 115 // Make sure the same package doesn't appear as both a regular and dev | |
| 116 // dependency. | |
| 117 var dependencyNames = dependencies.map((dep) => dep.name).toSet(); | |
| 118 var collisions = dependencyNames.intersection( | |
| 119 devDependencies.map((dep) => dep.name).toSet()); | |
| 120 | |
| 121 if (!collisions.isEmpty) { | |
| 122 var packageNames; | |
| 123 if (collisions.length == 1) { | |
| 124 packageNames = 'Package "${collisions.first}"'; | |
| 125 } else { | |
| 126 var names = collisions.toList(); | |
| 127 names.sort(); | |
| 128 var buffer = new StringBuffer(); | |
| 129 buffer.write("Packages "); | |
| 130 for (var i = 0; i < names.length; i++) { | |
| 131 buffer.write('"'); | |
| 132 buffer.write(names[i]); | |
| 133 buffer.write('"'); | |
| 134 if (i == names.length - 2) { | |
| 135 buffer.write(", "); | |
| 136 } else if (i == names.length - 1) { | |
| 137 buffer.write(", and "); | |
| 138 } | |
| 139 } | |
| 140 | |
| 141 packageNames = buffer.toString(); | |
| 142 } | |
| 143 throw new FormatException( | |
| 144 '$packageNames cannot appear in both "dependencies" and ' | |
| 145 '"dev_dependencies".'); | |
| 146 } | |
| 147 | |
| 148 var environmentYaml = parsedPubspec['environment']; | |
| 149 var sdkConstraint = VersionConstraint.any; | |
| 150 if (environmentYaml != null) { | |
| 151 if (environmentYaml is! Map) { | |
| 152 throw new FormatException( | |
| 153 'The pubspec "environment" field should be a map, but was ' | |
| 154 '"$environmentYaml".'); | |
| 155 } | |
| 156 | |
| 157 var sdkYaml = environmentYaml['sdk']; | |
| 158 if (sdkYaml is! String) { | |
| 159 throw new FormatException( | |
| 160 'The "sdk" field of "environment" should be a string, but was ' | |
| 161 '"$sdkYaml".'); | |
| 162 } | |
| 163 | |
| 164 sdkConstraint = new VersionConstraint.parse(sdkYaml); | |
| 165 } | |
| 166 var environment = new PubspecEnvironment(sdkConstraint); | |
| 167 | |
| 168 // Even though the pub app itself doesn't use these fields, we validate | |
| 169 // them here so that users find errors early before they try to upload to | |
| 170 // the server: | |
| 171 // TODO(rnystrom): We should split this validation into separate layers: | |
| 172 // 1. Stuff that is required in any pubspec to perform any command. Things | |
| 173 // like "must have a name". That should go here. | |
| 174 // 2. Stuff that is required to upload a package. Things like "homepage | |
| 175 // must use a valid scheme". That should go elsewhere. pub upload should | |
| 176 // call it, and we should provide a separate command to show the user, | |
| 177 // and also expose it to the editor in some way. | |
| 178 | |
| 179 if (parsedPubspec.containsKey('homepage')) { | |
| 180 _validateFieldUrl(parsedPubspec['homepage'], 'homepage'); | |
| 181 } | |
| 182 if (parsedPubspec.containsKey('documentation')) { | |
| 183 _validateFieldUrl(parsedPubspec['documentation'], 'documentation'); | |
| 184 } | |
| 185 | |
| 186 if (parsedPubspec.containsKey('author') && | |
| 187 parsedPubspec['author'] is! String) { | |
| 188 throw new FormatException( | |
| 189 'The "author" field should be a string, but was ' | |
| 190 '${parsedPubspec["author"]}.'); | |
| 191 } | |
| 192 | |
| 193 if (parsedPubspec.containsKey('authors')) { | |
| 194 var authors = parsedPubspec['authors']; | |
| 195 if (authors is List) { | |
| 196 // All of the elements must be strings. | |
| 197 if (!authors.every((author) => author is String)) { | |
| 198 throw new FormatException('The "authors" field should be a string ' | |
| 199 'or a list of strings, but was "$authors".'); | |
| 200 } | |
| 201 } else if (authors is! String) { | |
| 202 throw new FormatException('The pubspec "authors" field should be a ' | |
| 203 'string or a list of strings, but was "$authors".'); | |
| 204 } | |
| 205 | |
| 206 if (parsedPubspec.containsKey('author')) { | |
| 207 throw new FormatException('A pubspec should not have both an "author" ' | |
| 208 'and an "authors" field.'); | |
| 209 } | |
| 210 } | |
| 211 | |
| 212 return new Pubspec(name, version, dependencies, devDependencies, | |
| 213 environment, parsedPubspec); | |
| 214 } | 102 } |
| 215 } | 103 } |
| 216 | 104 |
| 217 /// Evaluates whether the given [url] for [field] is valid. | 105 /// Evaluates whether the given [url] for [field] is valid. |
| 218 /// | 106 /// |
| 219 /// Throws [FormatException] on an invalid url. | 107 /// Throws [FormatException] on an invalid url. |
| 220 void _validateFieldUrl(url, String field) { | 108 void _validateFieldUrl(url, String field) { |
| 221 if (url is! String) { | 109 if (url is! String) { |
| 222 throw new FormatException( | 110 throw new FormatException( |
| 223 'The "$field" field should be a string, but was "$url".'); | 111 'The "$field" field should be a string, but was "$url".'); |
| 224 } | 112 } |
| 225 | 113 |
| 226 var goodScheme = new RegExp(r'^https?:'); | 114 var goodScheme = new RegExp(r'^https?:'); |
| 227 if (!goodScheme.hasMatch(url)) { | 115 if (!goodScheme.hasMatch(url)) { |
| 228 throw new FormatException( | 116 throw new FormatException( |
| 229 'The "$field" field should be an "http:" or "https:" URL, but ' | 117 'The "$field" field should be an "http:" or "https:" URL, but ' |
| 230 'was "$url".'); | 118 'was "$url".'); |
| 231 } | 119 } |
| 232 } | 120 } |
| 233 | 121 |
| 122 Pubspec _parseMap(String filePath, Map map, SourceRegistry sources) { |
| 123 var name = null; |
| 124 var version = Version.none; |
| 125 |
| 126 if (map.containsKey('name')) { |
| 127 name = map['name']; |
| 128 if (name is! String) { |
| 129 throw new FormatException( |
| 130 'The pubspec "name" field should be a string, but was "$name".'); |
| 131 } |
| 132 } |
| 133 |
| 134 if (map.containsKey('version')) { |
| 135 version = new Version.parse(map['version']); |
| 136 } |
| 137 |
| 138 var dependencies = _parseDependencies(filePath, sources, |
| 139 map['dependencies']); |
| 140 |
| 141 var devDependencies = _parseDependencies(filePath, sources, |
| 142 map['dev_dependencies']); |
| 143 |
| 144 // Make sure the same package doesn't appear as both a regular and dev |
| 145 // dependency. |
| 146 var dependencyNames = dependencies.map((dep) => dep.name).toSet(); |
| 147 var collisions = dependencyNames.intersection( |
| 148 devDependencies.map((dep) => dep.name).toSet()); |
| 149 |
| 150 if (!collisions.isEmpty) { |
| 151 var packageNames; |
| 152 if (collisions.length == 1) { |
| 153 packageNames = 'Package "${collisions.first}"'; |
| 154 } else { |
| 155 var names = collisions.toList(); |
| 156 names.sort(); |
| 157 var buffer = new StringBuffer(); |
| 158 buffer.write("Packages "); |
| 159 for (var i = 0; i < names.length; i++) { |
| 160 buffer.write('"'); |
| 161 buffer.write(names[i]); |
| 162 buffer.write('"'); |
| 163 if (i == names.length - 2) { |
| 164 buffer.write(", "); |
| 165 } else if (i == names.length - 1) { |
| 166 buffer.write(", and "); |
| 167 } |
| 168 } |
| 169 |
| 170 packageNames = buffer.toString(); |
| 171 } |
| 172 throw new FormatException( |
| 173 '$packageNames cannot appear in both "dependencies" and ' |
| 174 '"dev_dependencies".'); |
| 175 } |
| 176 |
| 177 var environmentYaml = map['environment']; |
| 178 var sdkConstraint = VersionConstraint.any; |
| 179 if (environmentYaml != null) { |
| 180 if (environmentYaml is! Map) { |
| 181 throw new FormatException( |
| 182 'The pubspec "environment" field should be a map, but was ' |
| 183 '"$environmentYaml".'); |
| 184 } |
| 185 |
| 186 var sdkYaml = environmentYaml['sdk']; |
| 187 if (sdkYaml is! String) { |
| 188 throw new FormatException( |
| 189 'The "sdk" field of "environment" should be a string, but was ' |
| 190 '"$sdkYaml".'); |
| 191 } |
| 192 |
| 193 sdkConstraint = new VersionConstraint.parse(sdkYaml); |
| 194 } |
| 195 var environment = new PubspecEnvironment(sdkConstraint); |
| 196 |
| 197 // Even though the pub app itself doesn't use these fields, we validate |
| 198 // them here so that users find errors early before they try to upload to |
| 199 // the server: |
| 200 // TODO(rnystrom): We should split this validation into separate layers: |
| 201 // 1. Stuff that is required in any pubspec to perform any command. Things |
| 202 // like "must have a name". That should go here. |
| 203 // 2. Stuff that is required to upload a package. Things like "homepage |
| 204 // must use a valid scheme". That should go elsewhere. pub upload should |
| 205 // call it, and we should provide a separate command to show the user, |
| 206 // and also expose it to the editor in some way. |
| 207 |
| 208 if (map.containsKey('homepage')) { |
| 209 _validateFieldUrl(map['homepage'], 'homepage'); |
| 210 } |
| 211 if (map.containsKey('documentation')) { |
| 212 _validateFieldUrl(map['documentation'], 'documentation'); |
| 213 } |
| 214 |
| 215 if (map.containsKey('author') && map['author'] is! String) { |
| 216 throw new FormatException( |
| 217 'The "author" field should be a string, but was ' |
| 218 '${map["author"]}.'); |
| 219 } |
| 220 |
| 221 if (map.containsKey('authors')) { |
| 222 var authors = map['authors']; |
| 223 if (authors is List) { |
| 224 // All of the elements must be strings. |
| 225 if (!authors.every((author) => author is String)) { |
| 226 throw new FormatException('The "authors" field should be a string ' |
| 227 'or a list of strings, but was "$authors".'); |
| 228 } |
| 229 } else if (authors is! String) { |
| 230 throw new FormatException('The pubspec "authors" field should be a ' |
| 231 'string or a list of strings, but was "$authors".'); |
| 232 } |
| 233 |
| 234 if (map.containsKey('author')) { |
| 235 throw new FormatException('A pubspec should not have both an "author" ' |
| 236 'and an "authors" field.'); |
| 237 } |
| 238 } |
| 239 |
| 240 return new Pubspec(name, version, dependencies, devDependencies, |
| 241 environment, map); |
| 242 } |
| 243 |
| 234 List<PackageDep> _parseDependencies(String pubspecPath, SourceRegistry sources, | 244 List<PackageDep> _parseDependencies(String pubspecPath, SourceRegistry sources, |
| 235 yaml) { | 245 yaml) { |
| 236 var dependencies = <PackageDep>[]; | 246 var dependencies = <PackageDep>[]; |
| 237 | 247 |
| 238 // Allow an empty dependencies key. | 248 // Allow an empty dependencies key. |
| 239 if (yaml == null) return dependencies; | 249 if (yaml == null) return dependencies; |
| 240 | 250 |
| 241 if (yaml is! Map || yaml.keys.any((e) => e is! String)) { | 251 if (yaml is! Map || yaml.keys.any((e) => e is! String)) { |
| 242 throw new FormatException( | 252 throw new FormatException( |
| 243 'The pubspec dependencies should be a map of package names, but ' | 253 'The pubspec dependencies should be a map of package names, but ' |
| (...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 296 /// The environment-related metadata in the pubspec. Corresponds to the data | 306 /// The environment-related metadata in the pubspec. Corresponds to the data |
| 297 /// under the "environment:" key in the pubspec. | 307 /// under the "environment:" key in the pubspec. |
| 298 class PubspecEnvironment { | 308 class PubspecEnvironment { |
| 299 /// The version constraint specifying which SDK versions this package works | 309 /// The version constraint specifying which SDK versions this package works |
| 300 /// with. | 310 /// with. |
| 301 final VersionConstraint sdkVersion; | 311 final VersionConstraint sdkVersion; |
| 302 | 312 |
| 303 PubspecEnvironment([VersionConstraint sdk]) | 313 PubspecEnvironment([VersionConstraint sdk]) |
| 304 : sdkVersion = sdk != null ? sdk : VersionConstraint.any; | 314 : sdkVersion = sdk != null ? sdk : VersionConstraint.any; |
| 305 } | 315 } |
| OLD | NEW |