Index: lib/src/runner/parse_metadata.dart |
diff --git a/lib/src/runner/parse_metadata.dart b/lib/src/runner/parse_metadata.dart |
index 21ed6e2cbd954fc71812c324522a6a34007b10c8..e4e4079608b8ee72a583eb3d7d9ad909a2aaf381 100644 |
--- a/lib/src/runner/parse_metadata.dart |
+++ b/lib/src/runner/parse_metadata.dart |
@@ -15,310 +15,331 @@ import '../backend/metadata.dart'; |
import '../frontend/timeout.dart'; |
import '../util/dart.dart'; |
-/// The valid argument names for [new Duration]. |
-const _durationArgs = const [ |
- "days", |
- "hours", |
- "minutes", |
- "seconds", |
- "milliseconds", |
- "microseconds" |
-]; |
- |
/// Parse the test metadata for the test file at [path]. |
/// |
/// Throws an [AnalysisError] if parsing fails or a [FormatException] if the |
/// test annotations are incorrect. |
-Metadata parseMetadata(String path) { |
- var timeout; |
- var testOn; |
- var skip; |
- |
- var contents = new File(path).readAsStringSync(); |
- var directives = parseDirectives(contents, name: path).directives; |
- var annotations = directives.isEmpty ? [] : directives.first.metadata; |
- |
- // We explicitly *don't* just look for "package:test" imports here, |
- // because it could be re-exported from another library. |
- var prefixes = directives.map((directive) { |
- if (directive is! ImportDirective) return null; |
- if (directive.prefix == null) return null; |
- return directive.prefix.name; |
- }).where((prefix) => prefix != null).toSet(); |
- |
- for (var annotation in annotations) { |
- // The annotation syntax is ambiguous between named constructors and |
- // prefixed annotations, so we need to resolve that ambiguity using the |
- // known prefixes. The analyzer parses "@x.y()" as prefix "x", annotation |
- // "y", and named constructor null. It parses "@x.y.z()" as prefix "x", |
- // annotation "y", and named constructor "z". |
- var name; |
- var constructorName; |
- var identifier = annotation.name; |
- if (identifier is PrefixedIdentifier && |
- !prefixes.contains(identifier.prefix.name) && |
- annotation.constructorName == null) { |
- name = identifier.prefix.name; |
- constructorName = identifier.identifier.name; |
- } else { |
- name = identifier is PrefixedIdentifier |
- ? identifier.identifier.name |
- : identifier.name; |
- if (annotation.constructorName != null) { |
- constructorName = annotation.constructorName.name; |
- } |
- } |
+Metadata parseMetadata(String path) => new _Parser(path).parse(); |
+ |
+/// A parser for test suite metadata. |
+class _Parser { |
+ /// The path to the test suite. |
+ final String _path; |
+ |
+ /// All annotations at the top of the file. |
+ List<Annotation> _annotations; |
+ |
+ /// All prefixes defined by imports in this file. |
+ Set<String> _prefixes; |
+ |
+ _Parser(String path) |
+ : _path = path { |
+ var contents = new File(path).readAsStringSync(); |
+ var directives = parseDirectives(contents, name: path).directives; |
+ _annotations = directives.isEmpty ? [] : directives.first.metadata; |
+ |
+ // We explicitly *don't* just look for "package:test" imports here, |
+ // because it could be re-exported from another library. |
+ _prefixes = directives.map((directive) { |
+ if (directive is! ImportDirective) return null; |
+ if (directive.prefix == null) return null; |
+ return directive.prefix.name; |
+ }).where((prefix) => prefix != null).toSet(); |
+ } |
- if (name == 'TestOn') { |
- if (testOn != null) { |
- throw new SourceSpanFormatException( |
- "Only a single TestOn annotation may be used for a given test file.", |
- _spanFor(annotation, path)); |
+ /// Parses the metadata. |
+ Metadata parse() { |
+ var timeout; |
+ var testOn; |
+ var skip; |
+ |
+ for (var annotation in _annotations) { |
+ // The annotation syntax is ambiguous between named constructors and |
+ // prefixed annotations, so we need to resolve that ambiguity using the |
+ // known prefixes. The analyzer parses "@x.y()" as prefix "x", annotation |
+ // "y", and named constructor null. It parses "@x.y.z()" as prefix "x", |
+ // annotation "y", and named constructor "z". |
+ var name; |
+ var constructorName; |
+ var identifier = annotation.name; |
+ if (identifier is PrefixedIdentifier && |
+ !_prefixes.contains(identifier.prefix.name) && |
+ annotation.constructorName == null) { |
+ name = identifier.prefix.name; |
+ constructorName = identifier.identifier.name; |
+ } else { |
+ name = identifier is PrefixedIdentifier |
+ ? identifier.identifier.name |
+ : identifier.name; |
+ if (annotation.constructorName != null) { |
+ constructorName = annotation.constructorName.name; |
+ } |
} |
- testOn = _parseTestOn(annotation, constructorName, path); |
- } else if (name == 'Timeout') { |
- if (timeout != null) { |
- throw new SourceSpanFormatException( |
- "Only a single Timeout annotation may be used for a given test file.", |
- _spanFor(annotation, path)); |
- } |
- timeout = _parseTimeout(annotation, constructorName, path); |
- } else if (name == 'Skip') { |
- if (skip != null) { |
- throw new SourceSpanFormatException( |
- "Only a single Skip annotation may be used for a given test file.", |
- _spanFor(annotation, path)); |
+ |
+ if (name == 'TestOn') { |
+ _assertSingleAnnotation(testOn, 'TestOn', annotation); |
+ testOn = _parseTestOn(annotation, constructorName); |
+ } else if (name == 'Timeout') { |
+ _assertSingleAnnotation(timeout, 'Timeout', annotation); |
+ timeout = _parseTimeout(annotation, constructorName); |
+ } else if (name == 'Skip') { |
+ _assertSingleAnnotation(skip, 'Skip', annotation); |
+ skip = _parseSkip(annotation, constructorName); |
} |
- skip = _parseSkip(annotation, constructorName, path); |
} |
- } |
- try { |
- return new Metadata.parse( |
- testOn: testOn == null ? null : testOn.stringValue, |
- timeout: timeout, |
- skip: skip); |
- } on SourceSpanFormatException catch (error) { |
- var file = new SourceFile(new File(path).readAsStringSync(), |
- url: p.toUri(path)); |
- var span = contextualizeSpan(error.span, testOn, file); |
- if (span == null) rethrow; |
- throw new SourceSpanFormatException(error.message, span); |
+ try { |
+ return new Metadata.parse( |
+ testOn: testOn == null ? null : testOn.stringValue, |
+ timeout: timeout, |
+ skip: skip); |
+ } on SourceSpanFormatException catch (error) { |
+ var file = new SourceFile(new File(_path).readAsStringSync(), |
+ url: p.toUri(_path)); |
+ var span = contextualizeSpan(error.span, testOn, file); |
+ if (span == null) rethrow; |
+ throw new SourceSpanFormatException(error.message, span); |
+ } |
} |
-} |
-/// Parses a `@TestOn` annotation. |
-/// |
-/// [annotation] is the annotation. [constructorName] is the name of the named |
-/// constructor for the annotation, if any. [path] is the path to the file from |
-/// which the annotation was parsed. |
-StringLiteral _parseTestOn(Annotation annotation, String constructorName, |
- String path) { |
- if (constructorName != null) { |
- throw new SourceSpanFormatException( |
- 'TestOn doesn\'t have a constructor named "$constructorName".', |
- _spanFor(annotation, path)); |
+ /// Parses a `@TestOn` annotation. |
+ /// |
+ /// [annotation] is the annotation. [constructorName] is the name of the named |
+ /// constructor for the annotation, if any. |
+ StringLiteral _parseTestOn(Annotation annotation, String constructorName) { |
+ _assertConstructorName(constructorName, 'TestOn', annotation); |
+ _assertArguments(annotation.arguments, 'TestOn', annotation, positional: 1); |
+ return _parseString(annotation.arguments.arguments.first); |
} |
- if (annotation.arguments == null) { |
- throw new SourceSpanFormatException( |
- 'TestOn takes one argument.', _spanFor(annotation, path)); |
- } |
+ /// Parses a `@Timeout` annotation. |
+ /// |
+ /// [annotation] is the annotation. [constructorName] is the name of the named |
+ /// constructor for the annotation, if any. |
+ Timeout _parseTimeout(Annotation annotation, String constructorName) { |
+ _assertConstructorName(constructorName, 'Timeout', annotation, |
+ validNames: [null, 'factor']); |
- var args = annotation.arguments.arguments; |
- if (args.isEmpty) { |
- throw new SourceSpanFormatException( |
- 'TestOn takes one argument.', _spanFor(annotation.arguments, path)); |
- } |
+ var description = 'Timeout'; |
+ if (constructorName != null) description += '.$constructorName'; |
- if (args.first is NamedExpression) { |
- throw new SourceSpanFormatException( |
- "TestOn doesn't take named parameters.", _spanFor(args.first, path)); |
- } |
+ _assertArguments(annotation.arguments, description, annotation, |
+ positional: 1); |
- if (args.length > 1) { |
- throw new SourceSpanFormatException( |
- "TestOn takes only one argument.", |
- _spanFor(annotation.arguments, path)); |
+ var args = annotation.arguments.arguments; |
+ if (constructorName == null) return new Timeout(_parseDuration(args.first)); |
+ return new Timeout.factor(_parseNum(args.first)); |
} |
- if (args.first is! StringLiteral) { |
- throw new SourceSpanFormatException( |
- "TestOn takes a String.", _spanFor(args.first, path)); |
+ /// Parses a `@Skip` annotation. |
+ /// |
+ /// [annotation] is the annotation. [constructorName] is the name of the named |
+ /// constructor for the annotation, if any. |
+ /// |
+ /// Returns either `true` or a reason string. |
+ _parseSkip(Annotation annotation, String constructorName) { |
+ _assertConstructorName(constructorName, 'Skip', annotation); |
+ _assertArguments(annotation.arguments, 'Skip', annotation, optional: 1); |
+ |
+ var args = annotation.arguments.arguments; |
+ return args.isEmpty ? true : _parseString(args.first).stringValue; |
} |
- return args.first; |
-} |
+ /// Parses a `const Duration` expression. |
+ Duration _parseDuration(Expression expression) { |
+ _parseConstructor(expression, 'Duration'); |
-/// Parses a `@Timeout` annotation. |
-/// |
-/// [annotation] is the annotation. [constructorName] is the name of the named |
-/// constructor for the annotation, if any. [path] is the path to the file from |
-/// which the annotation was parsed. |
-Timeout _parseTimeout(Annotation annotation, String constructorName, |
- String path) { |
- if (constructorName != null && constructorName != 'factor') { |
- throw new SourceSpanFormatException( |
- 'Timeout doesn\'t have a constructor named "$constructorName".', |
- _spanFor(annotation, path)); |
- } |
+ var constructor = expression as InstanceCreationExpression; |
+ var values = _assertArguments( |
+ constructor.argumentList, 'Duration', constructor, named: [ |
+ 'days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds' |
+ ]); |
- var description = 'Timeout'; |
- if (constructorName != null) description += '.$constructorName'; |
+ for (var key in values.keys.toList()) { |
+ if (values.containsKey(key)) values[key] = _parseInt(values[key]); |
+ } |
- if (annotation.arguments == null) { |
- throw new SourceSpanFormatException( |
- '$description takes one argument.', _spanFor(annotation, path)); |
+ return new Duration( |
+ days: values["days"] == null ? 0 : values["days"], |
+ hours: values["hours"] == null ? 0 : values["hours"], |
+ minutes: values["minutes"] == null ? 0 : values["minutes"], |
+ seconds: values["seconds"] == null ? 0 : values["seconds"], |
+ milliseconds: values["milliseconds"] == null ? 0 : values["milliseconds"], |
+ microseconds: |
+ values["microseconds"] == null ? 0 : values["microseconds"]); |
} |
- var args = annotation.arguments.arguments; |
- if (args.isEmpty) { |
+ /// Asserts that [existing] is null. |
+ /// |
+ /// [name] is the name of the annotation and [node] is its location, used for |
+ /// error reporting. |
+ void _assertSingleAnnotation(Object existing, String name, AstNode node) { |
+ if (existing == null) return; |
throw new SourceSpanFormatException( |
- '$description takes one argument.', |
- _spanFor(annotation.arguments, path)); |
+ "Only a single $name annotation may be used for a given test file.", |
+ _spanFor(node)); |
} |
- if (args.first is NamedExpression) { |
- throw new SourceSpanFormatException( |
- "$description doesn't take named parameters.", |
- _spanFor(args.first, path)); |
+ /// Asserts that [constructorName] is a valid constructor name for an AST |
+ /// node. |
+ /// |
+ /// [nodeName] is the name of the class being constructed, and [node] is the |
+ /// AST node for that class. [validNames], if passed, is the set of valid |
+ /// constructor names; if an unnamed constructor is valid, it should include |
+ /// `null`. By default, only an unnamed constructor is allowed. |
+ void _assertConstructorName(String constructorName, String nodeName, |
+ AstNode node, {Iterable<String> validNames}) { |
+ if (validNames == null) validNames = [null]; |
+ if (validNames.contains(constructorName)) return; |
+ |
+ if (constructorName == null) { |
+ throw new SourceSpanFormatException( |
+ "$nodeName doesn't have an unnamed constructor.", |
+ _spanFor(node)); |
+ } else { |
+ throw new SourceSpanFormatException( |
+ '$nodeName doesn\'t have a constructor named "$constructorName".', |
+ _spanFor(node)); |
+ } |
} |
- if (args.length > 1) { |
- throw new SourceSpanFormatException( |
- "$description takes only one argument.", |
- _spanFor(annotation.arguments, path)); |
- } |
+ /// Parses a constructor invocation for [className]. |
+ /// |
+ /// [validNames], if passed, is the set of valid constructor names; if an |
+ /// unnamed constructor is valid, it should include `null`. By default, only |
+ /// an unnamed constructor is allowed. |
+ /// |
+ /// Returns the name of the named constructor, if any. |
+ String _parseConstructor(Expression expression, String className, |
+ {Iterable<String> validNames}) { |
+ if (validNames == null) validNames = [null]; |
+ |
+ if (expression is! InstanceCreationExpression) { |
+ throw new SourceSpanFormatException( |
+ "Expected a $className.", _spanFor(expression)); |
+ } |
- if (constructorName == null) { |
- return new Timeout(_parseDuration(args.first, path)); |
- } else { |
- return new Timeout.factor(_parseNum(args.first, path)); |
- } |
-} |
+ var constructor = expression as InstanceCreationExpression; |
+ if (constructor.constructorName.type.name.name != className) { |
+ throw new SourceSpanFormatException( |
+ "Expected a $className.", _spanFor(constructor)); |
+ } |
-/// Parses a `@Skip` annotation. |
-/// |
-/// [annotation] is the annotation. [constructorName] is the name of the named |
-/// constructor for the annotation, if any. [path] is the path to the file from |
-/// which the annotation was parsed. |
-/// |
-/// Returns either `true` or a reason string. |
-_parseSkip(Annotation annotation, String constructorName, String path) { |
- if (constructorName != null) { |
- throw new SourceSpanFormatException( |
- 'Skip doesn\'t have a constructor named "$constructorName".', |
- _spanFor(annotation, path)); |
- } |
+ if (constructor.keyword.lexeme != "const") { |
+ throw new SourceSpanFormatException( |
+ "$className must use a const constructor.", _spanFor(constructor)); |
+ } |
- if (annotation.arguments == null) { |
- throw new SourceSpanFormatException( |
- 'Skip must have parentheses.', _spanFor(annotation, path)); |
+ var name = constructor.constructorName == null |
+ ? null |
+ : constructor.constructorName.name; |
+ _assertConstructorName(name, className, expression, |
+ validNames: validNames); |
+ return name; |
} |
- var args = annotation.arguments.arguments; |
- if (args.length > 1) { |
- throw new SourceSpanFormatException( |
- 'Skip takes zero arguments or one argument.', |
- _spanFor(annotation.arguments, path)); |
- } |
+ /// Assert that [arguments] is a valid argument list. |
+ /// |
+ /// [name] describes the function and [node] is its AST node. [positional] is |
+ /// the number of required positional arguments, [optional] the number of |
+ /// optional positional arguments, and [named] the set of valid argument |
+ /// names. |
+ /// |
+ /// The set of parsed named arguments is returned. |
+ Map<String, Expression> _assertArguments(ArgumentList arguments, String name, |
+ AstNode node, {int positional, int optional, Iterable<String> named}) { |
+ if (positional == null) positional = 0; |
+ if (optional == null) optional = 0; |
+ if (named == null) named = new Set(); |
+ |
+ if (arguments == null) { |
+ throw new SourceSpanFormatException( |
+ '$name takes arguments.', _spanFor(node)); |
+ } |
- if (args.isEmpty) return true; |
+ var actualNamed = arguments.arguments |
+ .where((arg) => arg is NamedExpression).toList(); |
+ if (!actualNamed.isEmpty && named.isEmpty) { |
+ throw new SourceSpanFormatException( |
+ "$name doesn't take named arguments.", _spanFor(actualNamed.first)); |
+ } |
- if (args.first is NamedExpression) { |
- throw new SourceSpanFormatException( |
- "Skip doesn't take named parameters.", _spanFor(args.first, path)); |
- } |
+ var namedValues = {}; |
+ for (var argument in actualNamed) { |
+ var argumentName = argument.name.label.name; |
+ if (!named.contains(argumentName)) { |
+ throw new SourceSpanFormatException( |
+ '$name doesn\'t take an argument named "$argumentName".', |
+ _spanFor(argument)); |
+ } else if (namedValues.containsKey(argumentName)) { |
+ throw new SourceSpanFormatException( |
+ 'An argument named "$argumentName" was already passed.', |
+ _spanFor(argument)); |
+ } else { |
+ namedValues[argumentName] = argument.expression; |
+ } |
+ } |
- if (args.first is! StringLiteral) { |
- throw new SourceSpanFormatException( |
- "Skip takes a String.", _spanFor(args.first, path)); |
- } |
+ var actualPositional = arguments.arguments.length - actualNamed.length; |
+ if (actualPositional < positional) { |
+ var buffer = new StringBuffer("$name takes "); |
+ if (optional != 0) buffer.write("at least "); |
+ buffer.write("$positional argument"); |
+ if (positional > 1) buffer.write("s"); |
+ buffer.write("."); |
+ throw new SourceSpanFormatException( |
+ buffer.toString(), _spanFor(arguments)); |
+ } |
- return args.first.stringValue; |
-} |
+ if (actualPositional > positional + optional) { |
+ if (optional + positional == 0) { |
+ var buffer = new StringBuffer("$name doesn't take "); |
+ if (!named.isEmpty) buffer.write("positional "); |
+ buffer.write("arguments."); |
+ throw new SourceSpanFormatException( |
+ buffer.toString(), _spanFor(arguments)); |
+ } |
-/// Parses a `const Duration` expression. |
-Duration _parseDuration(Expression expression, String path) { |
- if (expression is! InstanceCreationExpression) { |
- throw new SourceSpanFormatException( |
- "Expected a Duration.", |
- _spanFor(expression, path)); |
- } |
+ var buffer = new StringBuffer("$name takes "); |
+ if (optional != 0) buffer.write("at most "); |
+ buffer.write("${positional + optional} argument"); |
+ if (positional > 1) buffer.write("s"); |
+ buffer.write("."); |
+ throw new SourceSpanFormatException( |
+ buffer.toString(), _spanFor(arguments)); |
+ } |
- var constructor = expression as InstanceCreationExpression; |
- if (constructor.constructorName.type.name.name != 'Duration') { |
- throw new SourceSpanFormatException( |
- "Expected a Duration.", |
- _spanFor(constructor, path)); |
+ return namedValues; |
} |
- if (constructor.keyword.lexeme != "const") { |
+ /// Parses a constant number literal. |
+ num _parseNum(Expression expression) { |
+ if (expression is IntegerLiteral) return expression.value; |
+ if (expression is DoubleLiteral) return expression.value; |
throw new SourceSpanFormatException( |
- "Duration must use a const constructor.", |
- _spanFor(constructor, path)); |
+ "Expected a number.", _spanFor(expression)); |
} |
- if (constructor.constructorName.name != null) { |
+ /// Parses a constant int literal. |
+ int _parseInt(Expression expression) { |
+ if (expression is IntegerLiteral) return expression.value; |
throw new SourceSpanFormatException( |
- "Duration doesn't have a constructor named " |
- '"${constructor.constructorName}".', |
- _spanFor(constructor.constructorName, path)); |
+ "Expected an integer.", _spanFor(expression)); |
} |
- var values = {}; |
- var args = constructor.argumentList.arguments; |
- for (var argument in args) { |
- if (argument is! NamedExpression) { |
- throw new SourceSpanFormatException( |
- "Duration doesn't take positional arguments.", |
- _spanFor(argument, path)); |
- } |
- |
- var name = argument.name.label.name; |
- if (!_durationArgs.contains(name)) { |
- throw new SourceSpanFormatException( |
- 'Duration doesn\'t take an argument named "$name".', |
- _spanFor(argument, path)); |
- } |
- |
- if (values.containsKey(name)) { |
- throw new SourceSpanFormatException( |
- 'An argument named "$name" was already passed.', |
- _spanFor(argument, path)); |
- } |
- |
- values[name] = _parseInt(argument.expression, path); |
+ /// Parses a constant String literal. |
+ StringLiteral _parseString(Expression expression) { |
+ if (expression is StringLiteral) return expression; |
+ throw new SourceSpanFormatException( |
+ "Expected a String.", _spanFor(expression)); |
} |
- return new Duration( |
- days: values["days"] == null ? 0 : values["days"], |
- hours: values["hours"] == null ? 0 : values["hours"], |
- minutes: values["minutes"] == null ? 0 : values["minutes"], |
- seconds: values["seconds"] == null ? 0 : values["seconds"], |
- milliseconds: values["milliseconds"] == null ? 0 : values["milliseconds"], |
- microseconds: |
- values["microseconds"] == null ? 0 : values["microseconds"]); |
-} |
- |
-/// Parses a constant number literal. |
-num _parseNum(Expression expression, String path) { |
- if (expression is IntegerLiteral) return expression.value; |
- if (expression is DoubleLiteral) return expression.value; |
- throw new SourceSpanFormatException( |
- "Expected a number.", _spanFor(expression, path)); |
-} |
- |
-/// Parses a constant int literal. |
-int _parseInt(Expression expression, String path) { |
- if (expression is IntegerLiteral) return expression.value; |
- throw new SourceSpanFormatException( |
- "Expected an integer.", _spanFor(expression, path)); |
-} |
- |
-/// Creates a [SourceSpan] for [node]. |
-SourceSpan _spanFor(AstNode node, String path) => |
+ /// Creates a [SourceSpan] for [node]. |
+ SourceSpan _spanFor(AstNode node) { |
// Load a SourceFile from scratch here since we're only ever going to emit |
// one error per file anyway. |
- new SourceFile(new File(path).readAsStringSync(), url: p.toUri(path)) |
+ var contents = new File(_path).readAsStringSync(); |
+ return new SourceFile(contents, url: p.toUri(_path)) |
.span(node.offset, node.end); |
+ } |
+} |