| 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 |