blob: e8aa1ae22417770e204531464816423cf91252b6 [file] [log] [blame]
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io';
import 'package:analyzer/analyzer.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:collection/collection.dart';
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';
import '../backend/metadata.dart';
import '../backend/platform_selector.dart';
import '../frontend/timeout.dart';
import '../util/dart.dart';
import '../utils.dart';
/// Parse the test metadata for the test file at [path].
///
/// The [platformVariables] are the set of variables that are valid for platform
/// selectors in suite metadata, in addition to the built-in variables that are
/// allowed everywhere.
///
/// Throws an [AnalysisError] if parsing fails or a [FormatException] if the
/// test annotations are incorrect.
Metadata parseMetadata(String path, Set<String> platformVariables) =>
new _Parser(path, platformVariables).parse();
/// A parser for test suite metadata.
class _Parser {
/// The path to the test suite.
final String _path;
/// The set of variables that are valid for platform selectors, in addition to
/// the built-in variables that are allowed everywhere.
final Set<String> _platformVariables;
/// 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, this._platformVariables) : _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) {
if (directive.prefix == null) return null;
return directive.prefix.name;
} else {
return null;
}
})
.where((prefix) => prefix != null)
.toSet();
}
/// Parses the metadata.
Metadata parse() {
Timeout timeout;
PlatformSelector testOn;
var skip;
Map<PlatformSelector, Metadata> onPlatform;
Set<String> tags;
int retry;
for (var annotation in _annotations) {
var pair =
_resolveConstructor(annotation.name, annotation.constructorName);
var name = pair.first;
var constructorName = pair.last;
if (name == 'TestOn') {
_assertSingle(testOn, 'TestOn', annotation);
testOn = _parseTestOn(annotation, constructorName);
} else if (name == 'Timeout') {
_assertSingle(timeout, 'Timeout', annotation);
timeout = _parseTimeout(annotation, constructorName);
} else if (name == 'Skip') {
_assertSingle(skip, 'Skip', annotation);
skip = _parseSkip(annotation, constructorName);
} else if (name == 'OnPlatform') {
_assertSingle(onPlatform, 'OnPlatform', annotation);
onPlatform = _parseOnPlatform(annotation, constructorName);
} else if (name == 'Tags') {
_assertSingle(tags, 'Tags', annotation);
tags = _parseTags(annotation, constructorName);
} else if (name == 'Retry') {
retry = _parseRetry(annotation, constructorName);
}
}
return new Metadata(
testOn: testOn,
timeout: timeout,
skip: skip == null ? null : true,
skipReason: skip is String ? skip : null,
onPlatform: onPlatform,
tags: tags,
retry: retry);
}
/// Parses a `@TestOn` annotation.
///
/// [annotation] is the annotation. [constructorName] is the name of the named
/// constructor for the annotation, if any.
PlatformSelector _parseTestOn(Annotation annotation, String constructorName) {
_assertConstructorName(constructorName, 'TestOn', annotation);
_assertArguments(annotation.arguments, 'TestOn', annotation, positional: 1);
return _parsePlatformSelector(annotation.arguments.arguments.first);
}
/// Parses an [expression] that should contain a string representing a
/// [PlatformSelector].
PlatformSelector _parsePlatformSelector(Expression expression) {
var literal = _parseString(expression);
return _contextualize(
literal,
() => new PlatformSelector.parse(literal.stringValue)
..validate(_platformVariables));
}
/// Parses a `@Retry` annotation.
///
/// [annotation] is the annotation. [constructorName] is the name of the named
/// constructor for the annotation, if any.
int _parseRetry(Annotation annotation, String constructorName) {
_assertConstructorName(constructorName, 'Retry', annotation);
_assertArguments(annotation.arguments, 'Retry', annotation, positional: 1);
return _parseInt(annotation.arguments.arguments.first);
}
/// 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', 'none']);
var description = 'Timeout';
if (constructorName != null) description += '.$constructorName';
if (constructorName == 'none') {
_assertNoArguments(annotation, description);
return Timeout.none;
}
_assertArguments(annotation.arguments, description, annotation,
positional: 1);
var args = annotation.arguments.arguments;
if (constructorName == null) return new Timeout(_parseDuration(args.first));
return new Timeout.factor(_parseNum(args.first));
}
/// Parses a `Timeout` constructor.
Timeout _parseTimeoutConstructor(InstanceCreationExpression constructor) {
var name =
_parseConstructor(constructor, 'Timeout', validNames: [null, 'factor']);
var description = 'Timeout';
if (name != null) description += '.$name';
_assertArguments(constructor.argumentList, description, constructor,
positional: 1);
var args = constructor.argumentList.arguments;
if (name == null) return new Timeout(_parseDuration(args.first));
return new Timeout.factor(_parseNum(args.first));
}
/// 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;
}
/// Parses a `Skip` constructor.
///
/// Returns either `true` or a reason string.
_parseSkipConstructor(InstanceCreationExpression constructor) {
_parseConstructor(constructor, 'Skip');
_assertArguments(constructor.argumentList, 'Skip', constructor,
optional: 1);
var args = constructor.argumentList.arguments;
return args.isEmpty ? true : _parseString(args.first).stringValue;
}
/// Parses a `@Tags` annotation.
///
/// [annotation] is the annotation. [constructorName] is the name of the named
/// constructor for the annotation, if any.
Set<String> _parseTags(Annotation annotation, String constructorName) {
_assertConstructorName(constructorName, 'Tags', annotation);
_assertArguments(annotation.arguments, 'Tags', annotation, positional: 1);
return _parseList(annotation.arguments.arguments.first)
.map((tagExpression) {
var name = _parseString(tagExpression).stringValue;
if (name.contains(anchoredHyphenatedIdentifier)) return name;
throw new SourceSpanFormatException(
"Invalid tag name. Tags must be (optionally hyphenated) Dart "
"identifiers.",
_spanFor(tagExpression));
}).toSet();
}
/// Parses an `@OnPlatform` annotation.
///
/// [annotation] is the annotation. [constructorName] is the name of the named
/// constructor for the annotation, if any.
Map<PlatformSelector, Metadata> _parseOnPlatform(
Annotation annotation, String constructorName) {
_assertConstructorName(constructorName, 'OnPlatform', annotation);
_assertArguments(annotation.arguments, 'OnPlatform', annotation,
positional: 1);
return _parseMap(annotation.arguments.arguments.first, key: (key) {
return _parsePlatformSelector(key);
}, value: (value) {
var expressions = [];
if (value is ListLiteral) {
expressions = _parseList(value);
} else if (value is InstanceCreationExpression ||
value is PrefixedIdentifier) {
expressions = [value];
} else {
throw new SourceSpanFormatException(
'Expected a Timeout, Skip, or List of those.', _spanFor(value));
}
var timeout;
var skip;
for (var expression in expressions) {
if (expression is InstanceCreationExpression) {
var className = _resolveConstructor(
expression.constructorName.type.name,
expression.constructorName.name)
.first;
if (className == 'Timeout') {
_assertSingle(timeout, 'Timeout', expression);
timeout = _parseTimeoutConstructor(expression);
continue;
} else if (className == 'Skip') {
_assertSingle(skip, 'Skip', expression);
skip = _parseSkipConstructor(expression);
continue;
}
} else if (expression is PrefixedIdentifier &&
expression.prefix.name == 'Timeout') {
if (expression.identifier.name != 'none') {
throw new SourceSpanFormatException(
'Undefined value.', _spanFor(expression));
}
_assertSingle(timeout, 'Timeout', expression);
timeout = Timeout.none;
continue;
}
throw new SourceSpanFormatException(
'Expected a Timeout or Skip.', _spanFor(expression));
}
return new Metadata.parse(timeout: timeout, skip: skip);
});
}
/// Parses a `const Duration` expression.
Duration _parseDuration(Expression expression) {
_parseConstructor(expression, 'Duration');
var constructor = expression as InstanceCreationExpression;
var valueExpressions = _assertArguments(
constructor.argumentList, 'Duration', constructor, named: [
'days',
'hours',
'minutes',
'seconds',
'milliseconds',
'microseconds'
]);
var values =
mapMap(valueExpressions, value: (_, value) => _parseInt(value));
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"]);
}
/// Asserts that [existing] is null.
///
/// [name] is the name of the annotation and [node] is its location, used for
/// error reporting.
void _assertSingle(Object existing, String name, AstNode node) {
if (existing == null) return;
throw new SourceSpanFormatException(
"Only a single $name may be used.", _spanFor(node));
}
/// Resolves a constructor name from its type [identifier] and its
/// [constructorName].
///
/// Since the parsed file isn't fully resolved, this is necessary to
/// disambiguate between prefixed names and named constructors.
Pair<String, String> _resolveConstructor(
Identifier identifier, SimpleIdentifier constructorName) {
// The syntax is ambiguous between named constructors and prefixed
// annotations, so we need to resolve that ambiguity using the known
// prefixes. The analyzer parses "new x.y()" as prefix "x", annotation "y",
// and named constructor null. It parses "new x.y.z()" as prefix "x",
// annotation "y", and named constructor "z".
String className;
String namedConstructor;
if (identifier is PrefixedIdentifier &&
!_prefixes.contains(identifier.prefix.name) &&
constructorName == null) {
className = identifier.prefix.name;
namedConstructor = identifier.identifier.name;
} else {
className = identifier is PrefixedIdentifier
? identifier.identifier.name
: identifier.name;
if (constructorName != null) namedConstructor = constructorName.name;
}
return new Pair(className, namedConstructor);
}
/// 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));
}
}
/// 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));
}
var constructor = expression as InstanceCreationExpression;
var pair = _resolveConstructor(constructor.constructorName.type.name,
constructor.constructorName.name);
var actualClassName = pair.first;
var constructorName = pair.last;
if (actualClassName != className) {
throw new SourceSpanFormatException(
"Expected a $className.", _spanFor(constructor));
}
if (constructor.keyword.lexeme != "const") {
throw new SourceSpanFormatException(
"$className must use a const constructor.", _spanFor(constructor));
}
_assertConstructorName(constructorName, className, expression,
validNames: validNames);
return constructorName;
}
/// 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));
}
var actualNamed = arguments.arguments
.where((arg) => arg is NamedExpression)
.map((arg) => arg as NamedExpression)
.toList();
if (actualNamed.isNotEmpty && named.isEmpty) {
throw new SourceSpanFormatException(
"$name doesn't take named arguments.", _spanFor(actualNamed.first));
}
var namedValues = <String, Expression>{};
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;
}
}
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));
}
if (actualPositional > positional + optional) {
if (optional + positional == 0) {
var buffer = new StringBuffer("$name doesn't take ");
if (named.isNotEmpty) buffer.write("positional ");
buffer.write("arguments.");
throw new SourceSpanFormatException(
buffer.toString(), _spanFor(arguments));
}
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));
}
return namedValues;
}
/// Assert that [annotation] (described by [name]) has no argument list.
void _assertNoArguments(Annotation annotation, String name) {
if (annotation.arguments == null) return;
throw new SourceSpanFormatException(
"$name doesn't take arguments.", _spanFor(annotation));
}
/// Parses a Map literal.
///
/// By default, returns [Expression] keys and values. These can be overridden
/// with the [key] and [value] parameters.
Map<K, V> _parseMap<K, V>(Expression expression,
{K key(Expression expression), V value(Expression expression)}) {
if (key == null) key = (expression) => expression as K;
if (value == null) value = (expression) => expression as V;
if (expression is! MapLiteral) {
throw new SourceSpanFormatException(
"Expected a Map.", _spanFor(expression));
}
var map = expression as MapLiteral;
if (map.constKeyword == null) {
throw new SourceSpanFormatException(
"Map literals must be const.", _spanFor(map));
}
return new Map.fromIterable(map.entries,
key: (entry) => key(entry.key), value: (entry) => value(entry.value));
}
/// Parses a List literal.
List<Expression> _parseList(Expression expression) {
if (expression is! ListLiteral) {
throw new SourceSpanFormatException(
"Expected a List.", _spanFor(expression));
}
var list = expression as ListLiteral;
if (list.constKeyword == null) {
throw new SourceSpanFormatException(
"List literals must be const.", _spanFor(list));
}
return list.elements;
}
/// 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(
"Expected a number.", _spanFor(expression));
}
/// Parses a constant int literal.
int _parseInt(Expression expression) {
if (expression is IntegerLiteral) return expression.value;
throw new SourceSpanFormatException(
"Expected an integer.", _spanFor(expression));
}
/// Parses a constant String literal.
StringLiteral _parseString(Expression expression) {
if (expression is StringLiteral) return expression;
throw new SourceSpanFormatException(
"Expected a String.", _spanFor(expression));
}
/// 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.
var contents = new File(_path).readAsStringSync();
return new SourceFile.fromString(contents, url: p.toUri(_path))
.span(node.offset, node.end);
}
/// Runs [fn] and contextualizes any [SourceSpanFormatException]s that occur
/// in it relative to [literal].
_contextualize(StringLiteral literal, fn()) {
try {
return fn();
} on SourceSpanFormatException catch (error) {
var file = new SourceFile.fromString(new File(_path).readAsStringSync(),
url: p.toUri(_path));
var span = contextualizeSpan(error.span, literal, file);
if (span == null) rethrow;
throw new SourceSpanFormatException(error.message, span);
}
}
}