OLD | NEW |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2015, 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 test.runner.parse_metadata; | 5 library test.runner.parse_metadata; |
6 | 6 |
7 import 'dart:io'; | 7 import 'dart:io'; |
8 | 8 |
9 import 'package:analyzer/analyzer.dart'; | 9 import 'package:analyzer/analyzer.dart'; |
10 import 'package:analyzer/src/generated/ast.dart'; | 10 import 'package:analyzer/src/generated/ast.dart'; |
11 import 'package:path/path.dart' as p; | 11 import 'package:path/path.dart' as p; |
12 import 'package:source_span/source_span.dart'; | 12 import 'package:source_span/source_span.dart'; |
13 | 13 |
14 import '../backend/metadata.dart'; | 14 import '../backend/metadata.dart'; |
15 import '../frontend/timeout.dart'; | 15 import '../frontend/timeout.dart'; |
16 import '../util/dart.dart'; | 16 import '../util/dart.dart'; |
17 | 17 |
18 /// The valid argument names for [new Duration]. | |
19 const _durationArgs = const [ | |
20 "days", | |
21 "hours", | |
22 "minutes", | |
23 "seconds", | |
24 "milliseconds", | |
25 "microseconds" | |
26 ]; | |
27 | |
28 /// Parse the test metadata for the test file at [path]. | 18 /// Parse the test metadata for the test file at [path]. |
29 /// | 19 /// |
30 /// Throws an [AnalysisError] if parsing fails or a [FormatException] if the | 20 /// Throws an [AnalysisError] if parsing fails or a [FormatException] if the |
31 /// test annotations are incorrect. | 21 /// test annotations are incorrect. |
32 Metadata parseMetadata(String path) { | 22 Metadata parseMetadata(String path) => new _Parser(path).parse(); |
33 var timeout; | 23 |
34 var testOn; | 24 /// A parser for test suite metadata. |
35 var skip; | 25 class _Parser { |
36 | 26 /// The path to the test suite. |
37 var contents = new File(path).readAsStringSync(); | 27 final String _path; |
38 var directives = parseDirectives(contents, name: path).directives; | 28 |
39 var annotations = directives.isEmpty ? [] : directives.first.metadata; | 29 /// All annotations at the top of the file. |
40 | 30 List<Annotation> _annotations; |
41 // We explicitly *don't* just look for "package:test" imports here, | 31 |
42 // because it could be re-exported from another library. | 32 /// All prefixes defined by imports in this file. |
43 var prefixes = directives.map((directive) { | 33 Set<String> _prefixes; |
44 if (directive is! ImportDirective) return null; | 34 |
45 if (directive.prefix == null) return null; | 35 _Parser(String path) |
46 return directive.prefix.name; | 36 : _path = path { |
47 }).where((prefix) => prefix != null).toSet(); | 37 var contents = new File(path).readAsStringSync(); |
48 | 38 var directives = parseDirectives(contents, name: path).directives; |
49 for (var annotation in annotations) { | 39 _annotations = directives.isEmpty ? [] : directives.first.metadata; |
50 // The annotation syntax is ambiguous between named constructors and | 40 |
51 // prefixed annotations, so we need to resolve that ambiguity using the | 41 // We explicitly *don't* just look for "package:test" imports here, |
52 // known prefixes. The analyzer parses "@x.y()" as prefix "x", annotation | 42 // because it could be re-exported from another library. |
53 // "y", and named constructor null. It parses "@x.y.z()" as prefix "x", | 43 _prefixes = directives.map((directive) { |
54 // annotation "y", and named constructor "z". | 44 if (directive is! ImportDirective) return null; |
55 var name; | 45 if (directive.prefix == null) return null; |
56 var constructorName; | 46 return directive.prefix.name; |
57 var identifier = annotation.name; | 47 }).where((prefix) => prefix != null).toSet(); |
58 if (identifier is PrefixedIdentifier && | 48 } |
59 !prefixes.contains(identifier.prefix.name) && | 49 |
60 annotation.constructorName == null) { | 50 /// Parses the metadata. |
61 name = identifier.prefix.name; | 51 Metadata parse() { |
62 constructorName = identifier.identifier.name; | 52 var timeout; |
| 53 var testOn; |
| 54 var skip; |
| 55 |
| 56 for (var annotation in _annotations) { |
| 57 // The annotation syntax is ambiguous between named constructors and |
| 58 // prefixed annotations, so we need to resolve that ambiguity using the |
| 59 // known prefixes. The analyzer parses "@x.y()" as prefix "x", annotation |
| 60 // "y", and named constructor null. It parses "@x.y.z()" as prefix "x", |
| 61 // annotation "y", and named constructor "z". |
| 62 var name; |
| 63 var constructorName; |
| 64 var identifier = annotation.name; |
| 65 if (identifier is PrefixedIdentifier && |
| 66 !_prefixes.contains(identifier.prefix.name) && |
| 67 annotation.constructorName == null) { |
| 68 name = identifier.prefix.name; |
| 69 constructorName = identifier.identifier.name; |
| 70 } else { |
| 71 name = identifier is PrefixedIdentifier |
| 72 ? identifier.identifier.name |
| 73 : identifier.name; |
| 74 if (annotation.constructorName != null) { |
| 75 constructorName = annotation.constructorName.name; |
| 76 } |
| 77 } |
| 78 |
| 79 if (name == 'TestOn') { |
| 80 _assertSingleAnnotation(testOn, 'TestOn', annotation); |
| 81 testOn = _parseTestOn(annotation, constructorName); |
| 82 } else if (name == 'Timeout') { |
| 83 _assertSingleAnnotation(timeout, 'Timeout', annotation); |
| 84 timeout = _parseTimeout(annotation, constructorName); |
| 85 } else if (name == 'Skip') { |
| 86 _assertSingleAnnotation(skip, 'Skip', annotation); |
| 87 skip = _parseSkip(annotation, constructorName); |
| 88 } |
| 89 } |
| 90 |
| 91 try { |
| 92 return new Metadata.parse( |
| 93 testOn: testOn == null ? null : testOn.stringValue, |
| 94 timeout: timeout, |
| 95 skip: skip); |
| 96 } on SourceSpanFormatException catch (error) { |
| 97 var file = new SourceFile(new File(_path).readAsStringSync(), |
| 98 url: p.toUri(_path)); |
| 99 var span = contextualizeSpan(error.span, testOn, file); |
| 100 if (span == null) rethrow; |
| 101 throw new SourceSpanFormatException(error.message, span); |
| 102 } |
| 103 } |
| 104 |
| 105 /// Parses a `@TestOn` annotation. |
| 106 /// |
| 107 /// [annotation] is the annotation. [constructorName] is the name of the named |
| 108 /// constructor for the annotation, if any. |
| 109 StringLiteral _parseTestOn(Annotation annotation, String constructorName) { |
| 110 _assertConstructorName(constructorName, 'TestOn', annotation); |
| 111 _assertArguments(annotation.arguments, 'TestOn', annotation, positional: 1); |
| 112 return _parseString(annotation.arguments.arguments.first); |
| 113 } |
| 114 |
| 115 /// Parses a `@Timeout` annotation. |
| 116 /// |
| 117 /// [annotation] is the annotation. [constructorName] is the name of the named |
| 118 /// constructor for the annotation, if any. |
| 119 Timeout _parseTimeout(Annotation annotation, String constructorName) { |
| 120 _assertConstructorName(constructorName, 'Timeout', annotation, |
| 121 validNames: [null, 'factor']); |
| 122 |
| 123 var description = 'Timeout'; |
| 124 if (constructorName != null) description += '.$constructorName'; |
| 125 |
| 126 _assertArguments(annotation.arguments, description, annotation, |
| 127 positional: 1); |
| 128 |
| 129 var args = annotation.arguments.arguments; |
| 130 if (constructorName == null) return new Timeout(_parseDuration(args.first)); |
| 131 return new Timeout.factor(_parseNum(args.first)); |
| 132 } |
| 133 |
| 134 /// Parses a `@Skip` annotation. |
| 135 /// |
| 136 /// [annotation] is the annotation. [constructorName] is the name of the named |
| 137 /// constructor for the annotation, if any. |
| 138 /// |
| 139 /// Returns either `true` or a reason string. |
| 140 _parseSkip(Annotation annotation, String constructorName) { |
| 141 _assertConstructorName(constructorName, 'Skip', annotation); |
| 142 _assertArguments(annotation.arguments, 'Skip', annotation, optional: 1); |
| 143 |
| 144 var args = annotation.arguments.arguments; |
| 145 return args.isEmpty ? true : _parseString(args.first).stringValue; |
| 146 } |
| 147 |
| 148 /// Parses a `const Duration` expression. |
| 149 Duration _parseDuration(Expression expression) { |
| 150 _parseConstructor(expression, 'Duration'); |
| 151 |
| 152 var constructor = expression as InstanceCreationExpression; |
| 153 var values = _assertArguments( |
| 154 constructor.argumentList, 'Duration', constructor, named: [ |
| 155 'days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds' |
| 156 ]); |
| 157 |
| 158 for (var key in values.keys.toList()) { |
| 159 if (values.containsKey(key)) values[key] = _parseInt(values[key]); |
| 160 } |
| 161 |
| 162 return new Duration( |
| 163 days: values["days"] == null ? 0 : values["days"], |
| 164 hours: values["hours"] == null ? 0 : values["hours"], |
| 165 minutes: values["minutes"] == null ? 0 : values["minutes"], |
| 166 seconds: values["seconds"] == null ? 0 : values["seconds"], |
| 167 milliseconds: values["milliseconds"] == null ? 0 : values["milliseconds"
], |
| 168 microseconds: |
| 169 values["microseconds"] == null ? 0 : values["microseconds"]); |
| 170 } |
| 171 |
| 172 /// Asserts that [existing] is null. |
| 173 /// |
| 174 /// [name] is the name of the annotation and [node] is its location, used for |
| 175 /// error reporting. |
| 176 void _assertSingleAnnotation(Object existing, String name, AstNode node) { |
| 177 if (existing == null) return; |
| 178 throw new SourceSpanFormatException( |
| 179 "Only a single $name annotation may be used for a given test file.", |
| 180 _spanFor(node)); |
| 181 } |
| 182 |
| 183 /// Asserts that [constructorName] is a valid constructor name for an AST |
| 184 /// node. |
| 185 /// |
| 186 /// [nodeName] is the name of the class being constructed, and [node] is the |
| 187 /// AST node for that class. [validNames], if passed, is the set of valid |
| 188 /// constructor names; if an unnamed constructor is valid, it should include |
| 189 /// `null`. By default, only an unnamed constructor is allowed. |
| 190 void _assertConstructorName(String constructorName, String nodeName, |
| 191 AstNode node, {Iterable<String> validNames}) { |
| 192 if (validNames == null) validNames = [null]; |
| 193 if (validNames.contains(constructorName)) return; |
| 194 |
| 195 if (constructorName == null) { |
| 196 throw new SourceSpanFormatException( |
| 197 "$nodeName doesn't have an unnamed constructor.", |
| 198 _spanFor(node)); |
63 } else { | 199 } else { |
64 name = identifier is PrefixedIdentifier | 200 throw new SourceSpanFormatException( |
65 ? identifier.identifier.name | 201 '$nodeName doesn\'t have a constructor named "$constructorName".', |
66 : identifier.name; | 202 _spanFor(node)); |
67 if (annotation.constructorName != null) { | 203 } |
68 constructorName = annotation.constructorName.name; | 204 } |
| 205 |
| 206 /// Parses a constructor invocation for [className]. |
| 207 /// |
| 208 /// [validNames], if passed, is the set of valid constructor names; if an |
| 209 /// unnamed constructor is valid, it should include `null`. By default, only |
| 210 /// an unnamed constructor is allowed. |
| 211 /// |
| 212 /// Returns the name of the named constructor, if any. |
| 213 String _parseConstructor(Expression expression, String className, |
| 214 {Iterable<String> validNames}) { |
| 215 if (validNames == null) validNames = [null]; |
| 216 |
| 217 if (expression is! InstanceCreationExpression) { |
| 218 throw new SourceSpanFormatException( |
| 219 "Expected a $className.", _spanFor(expression)); |
| 220 } |
| 221 |
| 222 var constructor = expression as InstanceCreationExpression; |
| 223 if (constructor.constructorName.type.name.name != className) { |
| 224 throw new SourceSpanFormatException( |
| 225 "Expected a $className.", _spanFor(constructor)); |
| 226 } |
| 227 |
| 228 if (constructor.keyword.lexeme != "const") { |
| 229 throw new SourceSpanFormatException( |
| 230 "$className must use a const constructor.", _spanFor(constructor)); |
| 231 } |
| 232 |
| 233 var name = constructor.constructorName == null |
| 234 ? null |
| 235 : constructor.constructorName.name; |
| 236 _assertConstructorName(name, className, expression, |
| 237 validNames: validNames); |
| 238 return name; |
| 239 } |
| 240 |
| 241 /// Assert that [arguments] is a valid argument list. |
| 242 /// |
| 243 /// [name] describes the function and [node] is its AST node. [positional] is |
| 244 /// the number of required positional arguments, [optional] the number of |
| 245 /// optional positional arguments, and [named] the set of valid argument |
| 246 /// names. |
| 247 /// |
| 248 /// The set of parsed named arguments is returned. |
| 249 Map<String, Expression> _assertArguments(ArgumentList arguments, String name, |
| 250 AstNode node, {int positional, int optional, Iterable<String> named}) { |
| 251 if (positional == null) positional = 0; |
| 252 if (optional == null) optional = 0; |
| 253 if (named == null) named = new Set(); |
| 254 |
| 255 if (arguments == null) { |
| 256 throw new SourceSpanFormatException( |
| 257 '$name takes arguments.', _spanFor(node)); |
| 258 } |
| 259 |
| 260 var actualNamed = arguments.arguments |
| 261 .where((arg) => arg is NamedExpression).toList(); |
| 262 if (!actualNamed.isEmpty && named.isEmpty) { |
| 263 throw new SourceSpanFormatException( |
| 264 "$name doesn't take named arguments.", _spanFor(actualNamed.first)); |
| 265 } |
| 266 |
| 267 var namedValues = {}; |
| 268 for (var argument in actualNamed) { |
| 269 var argumentName = argument.name.label.name; |
| 270 if (!named.contains(argumentName)) { |
| 271 throw new SourceSpanFormatException( |
| 272 '$name doesn\'t take an argument named "$argumentName".', |
| 273 _spanFor(argument)); |
| 274 } else if (namedValues.containsKey(argumentName)) { |
| 275 throw new SourceSpanFormatException( |
| 276 'An argument named "$argumentName" was already passed.', |
| 277 _spanFor(argument)); |
| 278 } else { |
| 279 namedValues[argumentName] = argument.expression; |
69 } | 280 } |
70 } | 281 } |
71 | 282 |
72 if (name == 'TestOn') { | 283 var actualPositional = arguments.arguments.length - actualNamed.length; |
73 if (testOn != null) { | 284 if (actualPositional < positional) { |
| 285 var buffer = new StringBuffer("$name takes "); |
| 286 if (optional != 0) buffer.write("at least "); |
| 287 buffer.write("$positional argument"); |
| 288 if (positional > 1) buffer.write("s"); |
| 289 buffer.write("."); |
| 290 throw new SourceSpanFormatException( |
| 291 buffer.toString(), _spanFor(arguments)); |
| 292 } |
| 293 |
| 294 if (actualPositional > positional + optional) { |
| 295 if (optional + positional == 0) { |
| 296 var buffer = new StringBuffer("$name doesn't take "); |
| 297 if (!named.isEmpty) buffer.write("positional "); |
| 298 buffer.write("arguments."); |
74 throw new SourceSpanFormatException( | 299 throw new SourceSpanFormatException( |
75 "Only a single TestOn annotation may be used for a given test file."
, | 300 buffer.toString(), _spanFor(arguments)); |
76 _spanFor(annotation, path)); | |
77 } | 301 } |
78 testOn = _parseTestOn(annotation, constructorName, path); | 302 |
79 } else if (name == 'Timeout') { | 303 var buffer = new StringBuffer("$name takes "); |
80 if (timeout != null) { | 304 if (optional != 0) buffer.write("at most "); |
81 throw new SourceSpanFormatException( | 305 buffer.write("${positional + optional} argument"); |
82 "Only a single Timeout annotation may be used for a given test file.
", | 306 if (positional > 1) buffer.write("s"); |
83 _spanFor(annotation, path)); | 307 buffer.write("."); |
84 } | 308 throw new SourceSpanFormatException( |
85 timeout = _parseTimeout(annotation, constructorName, path); | 309 buffer.toString(), _spanFor(arguments)); |
86 } else if (name == 'Skip') { | 310 } |
87 if (skip != null) { | 311 |
88 throw new SourceSpanFormatException( | 312 return namedValues; |
89 "Only a single Skip annotation may be used for a given test file.", | 313 } |
90 _spanFor(annotation, path)); | 314 |
91 } | 315 /// Parses a constant number literal. |
92 skip = _parseSkip(annotation, constructorName, path); | 316 num _parseNum(Expression expression) { |
93 } | 317 if (expression is IntegerLiteral) return expression.value; |
94 } | 318 if (expression is DoubleLiteral) return expression.value; |
95 | |
96 try { | |
97 return new Metadata.parse( | |
98 testOn: testOn == null ? null : testOn.stringValue, | |
99 timeout: timeout, | |
100 skip: skip); | |
101 } on SourceSpanFormatException catch (error) { | |
102 var file = new SourceFile(new File(path).readAsStringSync(), | |
103 url: p.toUri(path)); | |
104 var span = contextualizeSpan(error.span, testOn, file); | |
105 if (span == null) rethrow; | |
106 throw new SourceSpanFormatException(error.message, span); | |
107 } | |
108 } | |
109 | |
110 /// Parses a `@TestOn` annotation. | |
111 /// | |
112 /// [annotation] is the annotation. [constructorName] is the name of the named | |
113 /// constructor for the annotation, if any. [path] is the path to the file from | |
114 /// which the annotation was parsed. | |
115 StringLiteral _parseTestOn(Annotation annotation, String constructorName, | |
116 String path) { | |
117 if (constructorName != null) { | |
118 throw new SourceSpanFormatException( | 319 throw new SourceSpanFormatException( |
119 'TestOn doesn\'t have a constructor named "$constructorName".', | 320 "Expected a number.", _spanFor(expression)); |
120 _spanFor(annotation, path)); | 321 } |
121 } | 322 |
122 | 323 /// Parses a constant int literal. |
123 if (annotation.arguments == null) { | 324 int _parseInt(Expression expression) { |
| 325 if (expression is IntegerLiteral) return expression.value; |
124 throw new SourceSpanFormatException( | 326 throw new SourceSpanFormatException( |
125 'TestOn takes one argument.', _spanFor(annotation, path)); | 327 "Expected an integer.", _spanFor(expression)); |
126 } | 328 } |
127 | 329 |
128 var args = annotation.arguments.arguments; | 330 /// Parses a constant String literal. |
129 if (args.isEmpty) { | 331 StringLiteral _parseString(Expression expression) { |
| 332 if (expression is StringLiteral) return expression; |
130 throw new SourceSpanFormatException( | 333 throw new SourceSpanFormatException( |
131 'TestOn takes one argument.', _spanFor(annotation.arguments, path)); | 334 "Expected a String.", _spanFor(expression)); |
132 } | 335 } |
133 | 336 |
134 if (args.first is NamedExpression) { | 337 /// Creates a [SourceSpan] for [node]. |
135 throw new SourceSpanFormatException( | 338 SourceSpan _spanFor(AstNode node) { |
136 "TestOn doesn't take named parameters.", _spanFor(args.first, path)); | |
137 } | |
138 | |
139 if (args.length > 1) { | |
140 throw new SourceSpanFormatException( | |
141 "TestOn takes only one argument.", | |
142 _spanFor(annotation.arguments, path)); | |
143 } | |
144 | |
145 if (args.first is! StringLiteral) { | |
146 throw new SourceSpanFormatException( | |
147 "TestOn takes a String.", _spanFor(args.first, path)); | |
148 } | |
149 | |
150 return args.first; | |
151 } | |
152 | |
153 /// Parses a `@Timeout` annotation. | |
154 /// | |
155 /// [annotation] is the annotation. [constructorName] is the name of the named | |
156 /// constructor for the annotation, if any. [path] is the path to the file from | |
157 /// which the annotation was parsed. | |
158 Timeout _parseTimeout(Annotation annotation, String constructorName, | |
159 String path) { | |
160 if (constructorName != null && constructorName != 'factor') { | |
161 throw new SourceSpanFormatException( | |
162 'Timeout doesn\'t have a constructor named "$constructorName".', | |
163 _spanFor(annotation, path)); | |
164 } | |
165 | |
166 var description = 'Timeout'; | |
167 if (constructorName != null) description += '.$constructorName'; | |
168 | |
169 if (annotation.arguments == null) { | |
170 throw new SourceSpanFormatException( | |
171 '$description takes one argument.', _spanFor(annotation, path)); | |
172 } | |
173 | |
174 var args = annotation.arguments.arguments; | |
175 if (args.isEmpty) { | |
176 throw new SourceSpanFormatException( | |
177 '$description takes one argument.', | |
178 _spanFor(annotation.arguments, path)); | |
179 } | |
180 | |
181 if (args.first is NamedExpression) { | |
182 throw new SourceSpanFormatException( | |
183 "$description doesn't take named parameters.", | |
184 _spanFor(args.first, path)); | |
185 } | |
186 | |
187 if (args.length > 1) { | |
188 throw new SourceSpanFormatException( | |
189 "$description takes only one argument.", | |
190 _spanFor(annotation.arguments, path)); | |
191 } | |
192 | |
193 if (constructorName == null) { | |
194 return new Timeout(_parseDuration(args.first, path)); | |
195 } else { | |
196 return new Timeout.factor(_parseNum(args.first, path)); | |
197 } | |
198 } | |
199 | |
200 /// Parses a `@Skip` annotation. | |
201 /// | |
202 /// [annotation] is the annotation. [constructorName] is the name of the named | |
203 /// constructor for the annotation, if any. [path] is the path to the file from | |
204 /// which the annotation was parsed. | |
205 /// | |
206 /// Returns either `true` or a reason string. | |
207 _parseSkip(Annotation annotation, String constructorName, String path) { | |
208 if (constructorName != null) { | |
209 throw new SourceSpanFormatException( | |
210 'Skip doesn\'t have a constructor named "$constructorName".', | |
211 _spanFor(annotation, path)); | |
212 } | |
213 | |
214 if (annotation.arguments == null) { | |
215 throw new SourceSpanFormatException( | |
216 'Skip must have parentheses.', _spanFor(annotation, path)); | |
217 } | |
218 | |
219 var args = annotation.arguments.arguments; | |
220 if (args.length > 1) { | |
221 throw new SourceSpanFormatException( | |
222 'Skip takes zero arguments or one argument.', | |
223 _spanFor(annotation.arguments, path)); | |
224 } | |
225 | |
226 if (args.isEmpty) return true; | |
227 | |
228 if (args.first is NamedExpression) { | |
229 throw new SourceSpanFormatException( | |
230 "Skip doesn't take named parameters.", _spanFor(args.first, path)); | |
231 } | |
232 | |
233 if (args.first is! StringLiteral) { | |
234 throw new SourceSpanFormatException( | |
235 "Skip takes a String.", _spanFor(args.first, path)); | |
236 } | |
237 | |
238 return args.first.stringValue; | |
239 } | |
240 | |
241 /// Parses a `const Duration` expression. | |
242 Duration _parseDuration(Expression expression, String path) { | |
243 if (expression is! InstanceCreationExpression) { | |
244 throw new SourceSpanFormatException( | |
245 "Expected a Duration.", | |
246 _spanFor(expression, path)); | |
247 } | |
248 | |
249 var constructor = expression as InstanceCreationExpression; | |
250 if (constructor.constructorName.type.name.name != 'Duration') { | |
251 throw new SourceSpanFormatException( | |
252 "Expected a Duration.", | |
253 _spanFor(constructor, path)); | |
254 } | |
255 | |
256 if (constructor.keyword.lexeme != "const") { | |
257 throw new SourceSpanFormatException( | |
258 "Duration must use a const constructor.", | |
259 _spanFor(constructor, path)); | |
260 } | |
261 | |
262 if (constructor.constructorName.name != null) { | |
263 throw new SourceSpanFormatException( | |
264 "Duration doesn't have a constructor named " | |
265 '"${constructor.constructorName}".', | |
266 _spanFor(constructor.constructorName, path)); | |
267 } | |
268 | |
269 var values = {}; | |
270 var args = constructor.argumentList.arguments; | |
271 for (var argument in args) { | |
272 if (argument is! NamedExpression) { | |
273 throw new SourceSpanFormatException( | |
274 "Duration doesn't take positional arguments.", | |
275 _spanFor(argument, path)); | |
276 } | |
277 | |
278 var name = argument.name.label.name; | |
279 if (!_durationArgs.contains(name)) { | |
280 throw new SourceSpanFormatException( | |
281 'Duration doesn\'t take an argument named "$name".', | |
282 _spanFor(argument, path)); | |
283 } | |
284 | |
285 if (values.containsKey(name)) { | |
286 throw new SourceSpanFormatException( | |
287 'An argument named "$name" was already passed.', | |
288 _spanFor(argument, path)); | |
289 } | |
290 | |
291 values[name] = _parseInt(argument.expression, path); | |
292 } | |
293 | |
294 return new Duration( | |
295 days: values["days"] == null ? 0 : values["days"], | |
296 hours: values["hours"] == null ? 0 : values["hours"], | |
297 minutes: values["minutes"] == null ? 0 : values["minutes"], | |
298 seconds: values["seconds"] == null ? 0 : values["seconds"], | |
299 milliseconds: values["milliseconds"] == null ? 0 : values["milliseconds"], | |
300 microseconds: | |
301 values["microseconds"] == null ? 0 : values["microseconds"]); | |
302 } | |
303 | |
304 /// Parses a constant number literal. | |
305 num _parseNum(Expression expression, String path) { | |
306 if (expression is IntegerLiteral) return expression.value; | |
307 if (expression is DoubleLiteral) return expression.value; | |
308 throw new SourceSpanFormatException( | |
309 "Expected a number.", _spanFor(expression, path)); | |
310 } | |
311 | |
312 /// Parses a constant int literal. | |
313 int _parseInt(Expression expression, String path) { | |
314 if (expression is IntegerLiteral) return expression.value; | |
315 throw new SourceSpanFormatException( | |
316 "Expected an integer.", _spanFor(expression, path)); | |
317 } | |
318 | |
319 /// Creates a [SourceSpan] for [node]. | |
320 SourceSpan _spanFor(AstNode node, String path) => | |
321 // Load a SourceFile from scratch here since we're only ever going to emit | 339 // Load a SourceFile from scratch here since we're only ever going to emit |
322 // one error per file anyway. | 340 // one error per file anyway. |
323 new SourceFile(new File(path).readAsStringSync(), url: p.toUri(path)) | 341 var contents = new File(_path).readAsStringSync(); |
| 342 return new SourceFile(contents, url: p.toUri(_path)) |
324 .span(node.offset, node.end); | 343 .span(node.offset, node.end); |
| 344 } |
| 345 } |
OLD | NEW |