| // 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. |
| |
| // ignore: deprecated_member_use |
| import 'package:analyzer/analyzer.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:source_span/source_span.dart'; |
| |
| import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/frontend/timeout.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/utils.dart'; // ignore: implementation_imports |
| import '../util/dart.dart'; |
| |
| /// Parse the test metadata for the test file at [path] with [contents]. |
| /// |
| /// 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, String contents, Set<String> platformVariables) => |
| _Parser(path, contents, 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; |
| |
| /// The actual contents of the file. |
| final String _contents; |
| |
| _Parser(this._path, this._contents, this._platformVariables) { |
| // ignore: deprecated_member_use |
| 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 directive.prefix?.name; |
| } else { |
| return null; |
| } |
| }) |
| .where((prefix) => prefix != null) |
| .toSet(); |
| } |
| |
| /// Parses the metadata. |
| Metadata parse() { |
| Timeout timeout; |
| PlatformSelector testOn; |
| dynamic /*String|bool*/ 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 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) => |
| _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, |
| () => 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) => |
| _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) { |
| if (constructorName == 'none') { |
| return Timeout.none; |
| } |
| |
| var args = annotation.arguments.arguments; |
| if (constructorName == null) return Timeout(_parseDuration(args.first)); |
| return Timeout.factor(_parseNum(args.first)); |
| } |
| |
| /// Parses a `Timeout` constructor. |
| Timeout _parseTimeoutConstructor(Expression constructor) { |
| var name = _findConstructorName(constructor, 'Timeout'); |
| var arguments = _parseArguments(constructor); |
| if (name == null) return Timeout(_parseDuration(arguments.first)); |
| if (name == 'factor') return Timeout.factor(_parseNum(arguments.first)); |
| throw SourceSpanFormatException('Invalid timeout', _spanFor(constructor)); |
| } |
| |
| /// 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. |
| dynamic _parseSkip(Annotation annotation, String constructorName) { |
| var args = annotation.arguments.arguments; |
| return args.isEmpty ? true : _parseString(args.first).stringValue; |
| } |
| |
| /// Parses a `Skip` constructor. |
| /// |
| /// Returns either `true` or a reason string. |
| dynamic _parseSkipConstructor(Expression constructor) { |
| _findConstructorName(constructor, 'Skip'); |
| var arguments = _parseArguments(constructor); |
| return arguments.isEmpty ? true : _parseString(arguments.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) { |
| return _parseList(annotation.arguments.arguments.first) |
| .map((tagExpression) { |
| var name = _parseString(tagExpression).stringValue; |
| if (name.contains(anchoredHyphenatedIdentifier)) return name; |
| |
| throw 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) { |
| return _parseMap(annotation.arguments.arguments.first, key: (key) { |
| return _parsePlatformSelector(key); |
| }, value: (value) { |
| var expressions = <AstNode>[]; |
| if (value is ListLiteral) { |
| expressions = _parseList(value); |
| } else if (value is InstanceCreationExpression || |
| value is PrefixedIdentifier || |
| value is MethodInvocation) { |
| expressions = [value]; |
| } else { |
| throw SourceSpanFormatException( |
| 'Expected a Timeout, Skip, or List of those.', _spanFor(value)); |
| } |
| |
| Timeout timeout; |
| dynamic 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 SourceSpanFormatException( |
| 'Undefined value.', _spanFor(expression)); |
| } |
| |
| _assertSingle(timeout, 'Timeout', expression); |
| timeout = Timeout.none; |
| continue; |
| } else if (expression is MethodInvocation) { |
| var className = |
| _typeNameFromMethodInvocation(expression, ['Timeout', 'Skip']); |
| if (className == 'Timeout') { |
| _assertSingle(timeout, 'Timeout', expression); |
| timeout = _parseTimeoutConstructor(expression); |
| continue; |
| } else if (className == 'Skip') { |
| _assertSingle(skip, 'Skip', expression); |
| skip = _parseSkipConstructor(expression); |
| continue; |
| } |
| } |
| |
| throw SourceSpanFormatException( |
| 'Expected a Timeout or Skip.', _spanFor(expression)); |
| } |
| |
| return Metadata.parse(timeout: timeout, skip: skip); |
| }); |
| } |
| |
| /// Parses a `const Duration` expression. |
| Duration _parseDuration(Expression expression) { |
| _findConstructorName(expression, 'Duration'); |
| |
| var arguments = _parseArguments(expression); |
| var values = _parseNamedArguments(arguments) |
| .map((key, value) => MapEntry(key, _parseInt(value))); |
| |
| return Duration( |
| days: values['days'] ?? 0, |
| hours: values['hours'] ?? 0, |
| minutes: values['minutes'] ?? 0, |
| seconds: values['seconds'] ?? 0, |
| milliseconds: values['milliseconds'] ?? 0, |
| microseconds: values['microseconds'] ?? 0); |
| } |
| |
| Map<String, Expression> _parseNamedArguments( |
| NodeList<Expression> arguments) => |
| { |
| for (var a in arguments.whereType<NamedExpression>()) |
| a.name.label.name: a.expression |
| }; |
| |
| /// 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 SourceSpanFormatException( |
| 'Only a single $name may be used.', _spanFor(node)); |
| } |
| |
| NodeList<Expression> _parseArguments(Expression expression) { |
| if (expression is InstanceCreationExpression) { |
| return expression.argumentList.arguments; |
| } |
| if (expression is MethodInvocation) { |
| return expression.argumentList.arguments; |
| } |
| throw SourceSpanFormatException( |
| 'Expected an instantiation', _spanFor(expression)); |
| } |
| |
| /// 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 Pair(className, namedConstructor); |
| } |
| |
| /// Parses a constructor invocation for [className]. |
| /// |
| /// Returns the name of the named constructor used, or null if the default |
| /// constructor is used. |
| /// If [expression] is not an instantiation of a [className] throws. |
| String _findConstructorName(Expression expression, String className) { |
| if (expression is InstanceCreationExpression) { |
| return _findConstructornameFromInstantiation(expression, className); |
| } |
| if (expression is MethodInvocation) { |
| return _findConstructorNameFromMethod(expression, className); |
| } |
| throw SourceSpanFormatException( |
| 'Expected a $className.', _spanFor(expression)); |
| } |
| |
| String _findConstructornameFromInstantiation( |
| InstanceCreationExpression constructor, String className) { |
| var pair = _resolveConstructor(constructor.constructorName.type.name, |
| constructor.constructorName.name); |
| var actualClassName = pair.first; |
| var constructorName = pair.last; |
| |
| if (actualClassName != className) { |
| throw SourceSpanFormatException( |
| 'Expected a $className.', _spanFor(constructor)); |
| } |
| |
| return constructorName; |
| } |
| |
| String _findConstructorNameFromMethod( |
| MethodInvocation constructor, String className) { |
| var target = constructor.target; |
| if (target != null) { |
| // target could be an import prefix or a different class. Assume that |
| // named constructor on a different class won't match the class name we |
| // are looking for. |
| // Example: `test.Timeout()` |
| if (constructor.methodName.name == className) return null; |
| // target is an optionally prefixed class, method is named constructor |
| // Examples: `Timeout.factor(2)`, `test.Timeout.factor(2)` |
| String parsedName; |
| if (target is SimpleIdentifier) parsedName = target.name; |
| if (target is PrefixedIdentifier) parsedName = target.identifier.name; |
| if (parsedName != className) { |
| throw SourceSpanFormatException( |
| 'Expected a $className.', _spanFor(constructor)); |
| } |
| return constructor.methodName.name; |
| } |
| // No target, must be an unnamed constructor |
| // Example `Timeout()` |
| if (constructor.methodName.name != className) { |
| throw SourceSpanFormatException( |
| 'Expected a $className.', _spanFor(constructor)); |
| } |
| return null; |
| } |
| |
| /// Returns a type from [candidates] that _may_ be a type instantiated by |
| /// [constructor]. |
| /// |
| /// This can be fooled - for instance the invocation `foo.Bar()` may look like |
| /// a prefixed instantiation of a `Bar` even though it is a named constructor |
| /// instantiation of a `foo`, or a method infocation on a variable `foo`, or |
| /// ... |
| /// |
| /// Similarly `Baz.another` may look like the named constructor invocation of |
| /// a `Baz`even though it is a prefixed instantiation of an `another`, or a |
| /// method invocation on a variable `Baz`, or ... |
| String _typeNameFromMethodInvocation( |
| MethodInvocation constructor, List<String> candidates) { |
| var methodName = constructor.methodName.name; |
| // Examples: `Timeout()`, `test.Timeout()` |
| if (candidates.contains(methodName)) return methodName; |
| var target = constructor.target; |
| // Example: `SomeOtherClass()` |
| if (target == null) return null; |
| if (target is SimpleIdentifier) { |
| // Example: `Timeout.factor()` |
| if (candidates.contains(target.name)) return target.name; |
| } |
| if (target is PrefixedIdentifier) { |
| // Looks like `some_prefix.SomeTarget.someMethod` - "SomeTarget" is the |
| // only potential type name. |
| // Example: `test.Timeout.factor()` |
| if (candidates.contains(target.identifier.name)) { |
| return target.identifier.name; |
| } |
| } |
| return null; |
| } |
| |
| /// 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 Function(Expression) key, V Function(Expression) value}) { |
| key ??= (expression) => expression as K; |
| value ??= (expression) => expression as V; |
| |
| if (expression is! SetOrMapLiteral) { |
| throw SourceSpanFormatException('Expected a Map.', _spanFor(expression)); |
| } |
| |
| var map = <K, V>{}; |
| for (var element in (expression as SetOrMapLiteral).elements) { |
| if (element is MapLiteralEntry) { |
| map[key(element.key)] = value(element.value); |
| } else { |
| throw SourceSpanFormatException( |
| 'Expected a map entry.', _spanFor(element)); |
| } |
| } |
| return map; |
| } |
| |
| /// Parses a List literal. |
| List<Expression> _parseList(Expression expression) { |
| if (expression is! ListLiteral) { |
| throw SourceSpanFormatException('Expected a List.', _spanFor(expression)); |
| } |
| |
| var list = expression as ListLiteral; |
| |
| return list.elements.map((e) { |
| if (e is! Expression) { |
| throw SourceSpanFormatException( |
| 'Expected only literal elements.', _spanFor(e)); |
| } |
| return e as Expression; |
| }).toList(); |
| } |
| |
| /// Parses a constant number literal. |
| num _parseNum(Expression expression) { |
| if (expression is IntegerLiteral) return expression.value; |
| if (expression is DoubleLiteral) return expression.value; |
| throw SourceSpanFormatException('Expected a number.', _spanFor(expression)); |
| } |
| |
| /// Parses a constant int literal. |
| int _parseInt(Expression expression) { |
| if (expression is IntegerLiteral) return expression.value; |
| throw SourceSpanFormatException( |
| 'Expected an integer.', _spanFor(expression)); |
| } |
| |
| /// Parses a constant String literal. |
| StringLiteral _parseString(Expression expression) { |
| if (expression is StringLiteral && expression.stringValue != null) { |
| return expression; |
| } |
| throw SourceSpanFormatException( |
| 'Expected a String literal.', _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. |
| return 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]. |
| T _contextualize<T>(StringLiteral literal, T Function() fn) { |
| try { |
| return fn(); |
| } on SourceSpanFormatException catch (error) { |
| var file = SourceFile.fromString(_contents, url: p.toUri(_path)); |
| var span = contextualizeSpan(error.span, literal, file); |
| if (span == null) rethrow; |
| throw SourceSpanFormatException(error.message, span); |
| } |
| } |
| } |