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

Side by Side Diff: mojo/public/dart/third_party/test/lib/src/runner/parse_metadata.dart

Issue 1346773002: Stop running pub get at gclient sync time and fix build bugs (Closed) Base URL: git@github.com:domokit/mojo.git@master
Patch Set: Created 5 years, 3 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 unified diff | Download patch
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698