OLD | NEW |
(Empty) | |
| 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 |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library test.runner.parse_metadata; |
| 6 |
| 7 import 'dart:io'; |
| 8 |
| 9 import 'package:analyzer/analyzer.dart'; |
| 10 import 'package:analyzer/src/generated/ast.dart'; |
| 11 import 'package:path/path.dart' as p; |
| 12 import 'package:source_span/source_span.dart'; |
| 13 |
| 14 import '../backend/metadata.dart'; |
| 15 import '../backend/platform_selector.dart'; |
| 16 import '../frontend/timeout.dart'; |
| 17 import '../util/dart.dart'; |
| 18 import '../utils.dart'; |
| 19 |
| 20 /// Parse the test metadata for the test file at [path]. |
| 21 /// |
| 22 /// Throws an [AnalysisError] if parsing fails or a [FormatException] if the |
| 23 /// test annotations are incorrect. |
| 24 Metadata parseMetadata(String path) => new _Parser(path).parse(); |
| 25 |
| 26 /// A parser for test suite metadata. |
| 27 class _Parser { |
| 28 /// The path to the test suite. |
| 29 final String _path; |
| 30 |
| 31 /// All annotations at the top of the file. |
| 32 List<Annotation> _annotations; |
| 33 |
| 34 /// All prefixes defined by imports in this file. |
| 35 Set<String> _prefixes; |
| 36 |
| 37 _Parser(String path) |
| 38 : _path = path { |
| 39 var contents = new File(path).readAsStringSync(); |
| 40 var directives = parseDirectives(contents, name: path).directives; |
| 41 _annotations = directives.isEmpty ? [] : directives.first.metadata; |
| 42 |
| 43 // We explicitly *don't* just look for "package:test" imports here, |
| 44 // because it could be re-exported from another library. |
| 45 _prefixes = directives.map((directive) { |
| 46 if (directive is! ImportDirective) return null; |
| 47 if (directive.prefix == null) return null; |
| 48 return directive.prefix.name; |
| 49 }).where((prefix) => prefix != null).toSet(); |
| 50 } |
| 51 |
| 52 /// Parses the metadata. |
| 53 Metadata parse() { |
| 54 var timeout; |
| 55 var testOn; |
| 56 var skip; |
| 57 var onPlatform; |
| 58 |
| 59 for (var annotation in _annotations) { |
| 60 var pair = _resolveConstructor( |
| 61 annotation.name, annotation.constructorName); |
| 62 var name = pair.first; |
| 63 var constructorName = pair.last; |
| 64 |
| 65 if (name == 'TestOn') { |
| 66 _assertSingle(testOn, 'TestOn', annotation); |
| 67 testOn = _parseTestOn(annotation, constructorName); |
| 68 } else if (name == 'Timeout') { |
| 69 _assertSingle(timeout, 'Timeout', annotation); |
| 70 timeout = _parseTimeout(annotation, constructorName); |
| 71 } else if (name == 'Skip') { |
| 72 _assertSingle(skip, 'Skip', annotation); |
| 73 skip = _parseSkip(annotation, constructorName); |
| 74 } else if (name == 'OnPlatform') { |
| 75 _assertSingle(onPlatform, 'OnPlatform', annotation); |
| 76 onPlatform = _parseOnPlatform(annotation, constructorName); |
| 77 } |
| 78 } |
| 79 |
| 80 return new Metadata( |
| 81 testOn: testOn, |
| 82 timeout: timeout, |
| 83 skip: skip != null, |
| 84 skipReason: skip is String ? skip : null, |
| 85 onPlatform: onPlatform); |
| 86 } |
| 87 |
| 88 /// Parses a `@TestOn` annotation. |
| 89 /// |
| 90 /// [annotation] is the annotation. [constructorName] is the name of the named |
| 91 /// constructor for the annotation, if any. |
| 92 PlatformSelector _parseTestOn(Annotation annotation, String constructorName) { |
| 93 _assertConstructorName(constructorName, 'TestOn', annotation); |
| 94 _assertArguments(annotation.arguments, 'TestOn', annotation, positional: 1); |
| 95 var literal = _parseString(annotation.arguments.arguments.first); |
| 96 return _contextualize(literal, |
| 97 () => new PlatformSelector.parse(literal.stringValue)); |
| 98 } |
| 99 |
| 100 /// Parses a `@Timeout` annotation. |
| 101 /// |
| 102 /// [annotation] is the annotation. [constructorName] is the name of the named |
| 103 /// constructor for the annotation, if any. |
| 104 Timeout _parseTimeout(Annotation annotation, String constructorName) { |
| 105 _assertConstructorName(constructorName, 'Timeout', annotation, |
| 106 validNames: [null, 'factor', 'none']); |
| 107 |
| 108 var description = 'Timeout'; |
| 109 if (constructorName != null) description += '.$constructorName'; |
| 110 |
| 111 if (constructorName == 'none') { |
| 112 _assertNoArguments(annotation, description); |
| 113 return Timeout.none; |
| 114 } |
| 115 |
| 116 _assertArguments(annotation.arguments, description, annotation, |
| 117 positional: 1); |
| 118 |
| 119 var args = annotation.arguments.arguments; |
| 120 if (constructorName == null) return new Timeout(_parseDuration(args.first)); |
| 121 return new Timeout.factor(_parseNum(args.first)); |
| 122 } |
| 123 |
| 124 /// Parses a `Timeout` constructor. |
| 125 Timeout _parseTimeoutConstructor(InstanceCreationExpression constructor) { |
| 126 var name = _parseConstructor(constructor, 'Timeout', |
| 127 validNames: [null, 'factor']); |
| 128 |
| 129 var description = 'Timeout'; |
| 130 if (name != null) description += '.$name'; |
| 131 |
| 132 _assertArguments(constructor.argumentList, description, constructor, |
| 133 positional: 1); |
| 134 |
| 135 var args = constructor.argumentList.arguments; |
| 136 if (name == null) return new Timeout(_parseDuration(args.first)); |
| 137 return new Timeout.factor(_parseNum(args.first)); |
| 138 } |
| 139 |
| 140 /// Parses a `@Skip` annotation. |
| 141 /// |
| 142 /// [annotation] is the annotation. [constructorName] is the name of the named |
| 143 /// constructor for the annotation, if any. |
| 144 /// |
| 145 /// Returns either `true` or a reason string. |
| 146 _parseSkip(Annotation annotation, String constructorName) { |
| 147 _assertConstructorName(constructorName, 'Skip', annotation); |
| 148 _assertArguments(annotation.arguments, 'Skip', annotation, optional: 1); |
| 149 |
| 150 var args = annotation.arguments.arguments; |
| 151 return args.isEmpty ? true : _parseString(args.first).stringValue; |
| 152 } |
| 153 |
| 154 /// Parses a `Skip` constructor. |
| 155 /// |
| 156 /// Returns either `true` or a reason string. |
| 157 _parseSkipConstructor(InstanceCreationExpression constructor) { |
| 158 _parseConstructor(constructor, 'Skip'); |
| 159 _assertArguments(constructor.argumentList, 'Skip', constructor, |
| 160 optional: 1); |
| 161 |
| 162 var args = constructor.argumentList.arguments; |
| 163 return args.isEmpty ? true : _parseString(args.first).stringValue; |
| 164 } |
| 165 |
| 166 /// Parses an `@OnPlatform` annotation. |
| 167 /// |
| 168 /// [annotation] is the annotation. [constructorName] is the name of the named |
| 169 /// constructor for the annotation, if any. |
| 170 Map<PlatformSelector, Metadata> _parseOnPlatform(Annotation annotation, |
| 171 String constructorName) { |
| 172 _assertConstructorName(constructorName, 'OnPlatform', annotation); |
| 173 _assertArguments(annotation.arguments, 'OnPlatform', annotation, |
| 174 positional: 1); |
| 175 |
| 176 return _parseMap(annotation.arguments.arguments.first, key: (key) { |
| 177 var selector = _parseString(key); |
| 178 return _contextualize(selector, |
| 179 () => new PlatformSelector.parse(selector.stringValue)); |
| 180 }, value: (value) { |
| 181 var expressions = []; |
| 182 if (value is ListLiteral) { |
| 183 expressions = _parseList(value); |
| 184 } else if (value is InstanceCreationExpression || |
| 185 value is PrefixedIdentifier) { |
| 186 expressions = [value]; |
| 187 } else { |
| 188 throw new SourceSpanFormatException( |
| 189 'Expected a Timeout, Skip, or List of those.', |
| 190 _spanFor(value)); |
| 191 } |
| 192 |
| 193 var timeout; |
| 194 var skip; |
| 195 for (var expression in expressions) { |
| 196 if (expression is InstanceCreationExpression) { |
| 197 var className = _resolveConstructor( |
| 198 expression.constructorName.type.name, |
| 199 expression.constructorName.name).first; |
| 200 |
| 201 if (className == 'Timeout') { |
| 202 _assertSingle(timeout, 'Timeout', expression); |
| 203 timeout = _parseTimeoutConstructor(expression); |
| 204 continue; |
| 205 } else if (className == 'Skip') { |
| 206 _assertSingle(skip, 'Skip', expression); |
| 207 skip = _parseSkipConstructor(expression); |
| 208 continue; |
| 209 } |
| 210 } else if (expression is PrefixedIdentifier && |
| 211 expression.prefix.name == 'Timeout') { |
| 212 if (expression.identifier.name != 'none') { |
| 213 throw new SourceSpanFormatException( |
| 214 'Undefined value.', _spanFor(expression)); |
| 215 } |
| 216 |
| 217 _assertSingle(timeout, 'Timeout', expression); |
| 218 timeout = Timeout.none; |
| 219 continue; |
| 220 } |
| 221 |
| 222 throw new SourceSpanFormatException( |
| 223 'Expected a Timeout or Skip.', |
| 224 _spanFor(expression)); |
| 225 } |
| 226 |
| 227 return new Metadata.parse(timeout: timeout, skip: skip); |
| 228 }); |
| 229 } |
| 230 |
| 231 /// Parses a `const Duration` expression. |
| 232 Duration _parseDuration(Expression expression) { |
| 233 _parseConstructor(expression, 'Duration'); |
| 234 |
| 235 var constructor = expression as InstanceCreationExpression; |
| 236 var values = _assertArguments( |
| 237 constructor.argumentList, 'Duration', constructor, named: [ |
| 238 'days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds' |
| 239 ]); |
| 240 |
| 241 for (var key in values.keys.toList()) { |
| 242 if (values.containsKey(key)) values[key] = _parseInt(values[key]); |
| 243 } |
| 244 |
| 245 return new Duration( |
| 246 days: values["days"] == null ? 0 : values["days"], |
| 247 hours: values["hours"] == null ? 0 : values["hours"], |
| 248 minutes: values["minutes"] == null ? 0 : values["minutes"], |
| 249 seconds: values["seconds"] == null ? 0 : values["seconds"], |
| 250 milliseconds: values["milliseconds"] == null ? 0 : values["milliseconds"
], |
| 251 microseconds: |
| 252 values["microseconds"] == null ? 0 : values["microseconds"]); |
| 253 } |
| 254 |
| 255 /// Asserts that [existing] is null. |
| 256 /// |
| 257 /// [name] is the name of the annotation and [node] is its location, used for |
| 258 /// error reporting. |
| 259 void _assertSingle(Object existing, String name, AstNode node) { |
| 260 if (existing == null) return; |
| 261 throw new SourceSpanFormatException( |
| 262 "Only a single $name may be used.", _spanFor(node)); |
| 263 } |
| 264 |
| 265 /// Resolves a constructor name from its type [identifier] and its |
| 266 /// [constructorName]. |
| 267 /// |
| 268 /// Since the parsed file isn't fully resolved, this is necessary to |
| 269 /// disambiguate between prefixed names and named constructors. |
| 270 Pair<String, String> _resolveConstructor(Identifier identifier, |
| 271 SimpleIdentifier constructorName) { |
| 272 // The syntax is ambiguous between named constructors and prefixed |
| 273 // annotations, so we need to resolve that ambiguity using the known |
| 274 // prefixes. The analyzer parses "new x.y()" as prefix "x", annotation "y", |
| 275 // and named constructor null. It parses "new x.y.z()" as prefix "x", |
| 276 // annotation "y", and named constructor "z". |
| 277 var className; |
| 278 var namedConstructor; |
| 279 if (identifier is PrefixedIdentifier && |
| 280 !_prefixes.contains(identifier.prefix.name) && |
| 281 constructorName == null) { |
| 282 className = identifier.prefix.name; |
| 283 namedConstructor = identifier.identifier.name; |
| 284 } else { |
| 285 className = identifier is PrefixedIdentifier |
| 286 ? identifier.identifier.name |
| 287 : identifier.name; |
| 288 if (constructorName != null) namedConstructor = constructorName.name; |
| 289 } |
| 290 return new Pair(className, namedConstructor); |
| 291 } |
| 292 |
| 293 /// Asserts that [constructorName] is a valid constructor name for an AST |
| 294 /// node. |
| 295 /// |
| 296 /// [nodeName] is the name of the class being constructed, and [node] is the |
| 297 /// AST node for that class. [validNames], if passed, is the set of valid |
| 298 /// constructor names; if an unnamed constructor is valid, it should include |
| 299 /// `null`. By default, only an unnamed constructor is allowed. |
| 300 void _assertConstructorName(String constructorName, String nodeName, |
| 301 AstNode node, {Iterable<String> validNames}) { |
| 302 if (validNames == null) validNames = [null]; |
| 303 if (validNames.contains(constructorName)) return; |
| 304 |
| 305 if (constructorName == null) { |
| 306 throw new SourceSpanFormatException( |
| 307 "$nodeName doesn't have an unnamed constructor.", |
| 308 _spanFor(node)); |
| 309 } else { |
| 310 throw new SourceSpanFormatException( |
| 311 '$nodeName doesn\'t have a constructor named "$constructorName".', |
| 312 _spanFor(node)); |
| 313 } |
| 314 } |
| 315 |
| 316 /// Parses a constructor invocation for [className]. |
| 317 /// |
| 318 /// [validNames], if passed, is the set of valid constructor names; if an |
| 319 /// unnamed constructor is valid, it should include `null`. By default, only |
| 320 /// an unnamed constructor is allowed. |
| 321 /// |
| 322 /// Returns the name of the named constructor, if any. |
| 323 String _parseConstructor(Expression expression, String className, |
| 324 {Iterable<String> validNames}) { |
| 325 if (validNames == null) validNames = [null]; |
| 326 |
| 327 if (expression is! InstanceCreationExpression) { |
| 328 throw new SourceSpanFormatException( |
| 329 "Expected a $className.", _spanFor(expression)); |
| 330 } |
| 331 |
| 332 var constructor = expression as InstanceCreationExpression; |
| 333 var pair = _resolveConstructor( |
| 334 constructor.constructorName.type.name, |
| 335 constructor.constructorName.name); |
| 336 var actualClassName = pair.first; |
| 337 var constructorName = pair.last; |
| 338 |
| 339 if (actualClassName != className) { |
| 340 throw new SourceSpanFormatException( |
| 341 "Expected a $className.", _spanFor(constructor)); |
| 342 } |
| 343 |
| 344 if (constructor.keyword.lexeme != "const") { |
| 345 throw new SourceSpanFormatException( |
| 346 "$className must use a const constructor.", _spanFor(constructor)); |
| 347 } |
| 348 |
| 349 _assertConstructorName(constructorName, className, expression, |
| 350 validNames: validNames); |
| 351 return constructorName; |
| 352 } |
| 353 |
| 354 /// Assert that [arguments] is a valid argument list. |
| 355 /// |
| 356 /// [name] describes the function and [node] is its AST node. [positional] is |
| 357 /// the number of required positional arguments, [optional] the number of |
| 358 /// optional positional arguments, and [named] the set of valid argument |
| 359 /// names. |
| 360 /// |
| 361 /// The set of parsed named arguments is returned. |
| 362 Map<String, Expression> _assertArguments(ArgumentList arguments, String name, |
| 363 AstNode node, {int positional, int optional, Iterable<String> named}) { |
| 364 if (positional == null) positional = 0; |
| 365 if (optional == null) optional = 0; |
| 366 if (named == null) named = new Set(); |
| 367 |
| 368 if (arguments == null) { |
| 369 throw new SourceSpanFormatException( |
| 370 '$name takes arguments.', _spanFor(node)); |
| 371 } |
| 372 |
| 373 var actualNamed = arguments.arguments |
| 374 .where((arg) => arg is NamedExpression).toList(); |
| 375 if (!actualNamed.isEmpty && named.isEmpty) { |
| 376 throw new SourceSpanFormatException( |
| 377 "$name doesn't take named arguments.", _spanFor(actualNamed.first)); |
| 378 } |
| 379 |
| 380 var namedValues = {}; |
| 381 for (var argument in actualNamed) { |
| 382 var argumentName = argument.name.label.name; |
| 383 if (!named.contains(argumentName)) { |
| 384 throw new SourceSpanFormatException( |
| 385 '$name doesn\'t take an argument named "$argumentName".', |
| 386 _spanFor(argument)); |
| 387 } else if (namedValues.containsKey(argumentName)) { |
| 388 throw new SourceSpanFormatException( |
| 389 'An argument named "$argumentName" was already passed.', |
| 390 _spanFor(argument)); |
| 391 } else { |
| 392 namedValues[argumentName] = argument.expression; |
| 393 } |
| 394 } |
| 395 |
| 396 var actualPositional = arguments.arguments.length - actualNamed.length; |
| 397 if (actualPositional < positional) { |
| 398 var buffer = new StringBuffer("$name takes "); |
| 399 if (optional != 0) buffer.write("at least "); |
| 400 buffer.write("$positional argument"); |
| 401 if (positional > 1) buffer.write("s"); |
| 402 buffer.write("."); |
| 403 throw new SourceSpanFormatException( |
| 404 buffer.toString(), _spanFor(arguments)); |
| 405 } |
| 406 |
| 407 if (actualPositional > positional + optional) { |
| 408 if (optional + positional == 0) { |
| 409 var buffer = new StringBuffer("$name doesn't take "); |
| 410 if (!named.isEmpty) buffer.write("positional "); |
| 411 buffer.write("arguments."); |
| 412 throw new SourceSpanFormatException( |
| 413 buffer.toString(), _spanFor(arguments)); |
| 414 } |
| 415 |
| 416 var buffer = new StringBuffer("$name takes "); |
| 417 if (optional != 0) buffer.write("at most "); |
| 418 buffer.write("${positional + optional} argument"); |
| 419 if (positional > 1) buffer.write("s"); |
| 420 buffer.write("."); |
| 421 throw new SourceSpanFormatException( |
| 422 buffer.toString(), _spanFor(arguments)); |
| 423 } |
| 424 |
| 425 return namedValues; |
| 426 } |
| 427 |
| 428 /// Assert that [annotation] (described by [name]) has no argument list. |
| 429 void _assertNoArguments(Annotation annotation, String name) { |
| 430 if (annotation.arguments == null) return; |
| 431 throw new SourceSpanFormatException( |
| 432 "$name doesn't take arguments.", _spanFor(annotation)); |
| 433 } |
| 434 |
| 435 /// Parses a Map literal. |
| 436 /// |
| 437 /// By default, returns [Expression] keys and values. These can be overridden |
| 438 /// with the [key] and [value] parameters. |
| 439 Map _parseMap(Expression expression, {key(Expression expression), |
| 440 value(Expression expression)}) { |
| 441 if (key == null) key = (expression) => expression; |
| 442 if (value == null) value = (expression) => expression; |
| 443 |
| 444 if (expression is! MapLiteral) { |
| 445 throw new SourceSpanFormatException( |
| 446 "Expected a Map.", _spanFor(expression)); |
| 447 } |
| 448 |
| 449 var map = expression as MapLiteral; |
| 450 if (map.constKeyword == null) { |
| 451 throw new SourceSpanFormatException( |
| 452 "Map literals must be const.", _spanFor(map)); |
| 453 } |
| 454 |
| 455 return new Map.fromIterable(map.entries, |
| 456 key: (entry) => key(entry.key), |
| 457 value: (entry) => value(entry.value)); |
| 458 } |
| 459 |
| 460 /// Parses a List literal. |
| 461 List<Expression> _parseList(Expression expression) { |
| 462 if (expression is! ListLiteral) { |
| 463 throw new SourceSpanFormatException( |
| 464 "Expected a List.", _spanFor(expression)); |
| 465 } |
| 466 |
| 467 var list = expression as ListLiteral; |
| 468 if (list.constKeyword == null) { |
| 469 throw new SourceSpanFormatException( |
| 470 "List literals must be const.", _spanFor(list)); |
| 471 } |
| 472 |
| 473 return list.elements; |
| 474 } |
| 475 |
| 476 /// Parses a constant number literal. |
| 477 num _parseNum(Expression expression) { |
| 478 if (expression is IntegerLiteral) return expression.value; |
| 479 if (expression is DoubleLiteral) return expression.value; |
| 480 throw new SourceSpanFormatException( |
| 481 "Expected a number.", _spanFor(expression)); |
| 482 } |
| 483 |
| 484 /// Parses a constant int literal. |
| 485 int _parseInt(Expression expression) { |
| 486 if (expression is IntegerLiteral) return expression.value; |
| 487 throw new SourceSpanFormatException( |
| 488 "Expected an integer.", _spanFor(expression)); |
| 489 } |
| 490 |
| 491 /// Parses a constant String literal. |
| 492 StringLiteral _parseString(Expression expression) { |
| 493 if (expression is StringLiteral) return expression; |
| 494 throw new SourceSpanFormatException( |
| 495 "Expected a String.", _spanFor(expression)); |
| 496 } |
| 497 |
| 498 /// Creates a [SourceSpan] for [node]. |
| 499 SourceSpan _spanFor(AstNode node) { |
| 500 // Load a SourceFile from scratch here since we're only ever going to emit |
| 501 // one error per file anyway. |
| 502 var contents = new File(_path).readAsStringSync(); |
| 503 return new SourceFile(contents, url: p.toUri(_path)) |
| 504 .span(node.offset, node.end); |
| 505 } |
| 506 |
| 507 /// Runs [fn] and contextualizes any [SourceSpanFormatException]s that occur |
| 508 /// in it relative to [literal]. |
| 509 _contextualize(StringLiteral literal, fn()) { |
| 510 try { |
| 511 return fn(); |
| 512 } on SourceSpanFormatException catch (error) { |
| 513 var file = new SourceFile(new File(_path).readAsStringSync(), |
| 514 url: p.toUri(_path)); |
| 515 var span = contextualizeSpan(error.span, literal, file); |
| 516 if (span == null) rethrow; |
| 517 throw new SourceSpanFormatException(error.message, span); |
| 518 } |
| 519 } |
| 520 } |
OLD | NEW |