Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2)

Unified Diff: lib/src/runner/parse_metadata.dart

Issue 1093313002: Refactor parseMetadata. (Closed) Base URL: git@github.com:dart-lang/test@master
Patch Set: Created 5 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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);
+ }
+}
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698