OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, 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 dart2js.mirrors_used; | |
6 | |
7 import 'constants/expressions.dart'; | |
8 import 'constants/values.dart' show | |
9 ConstantValue, | |
10 ConstructedConstantValue, | |
11 ListConstantValue, | |
12 StringConstantValue, | |
13 TypeConstantValue; | |
14 | |
15 import 'dart_types.dart' show | |
16 DartType, | |
17 InterfaceType, | |
18 TypeKind; | |
19 | |
20 import 'dart2jslib.dart' show | |
21 Compiler, | |
22 CompilerTask, | |
23 ConstantCompiler, | |
24 MessageKind, | |
25 TreeElements, | |
26 invariant; | |
27 | |
28 import 'elements/elements.dart' show | |
29 ClassElement, | |
30 Element, | |
31 LibraryElement, | |
32 MetadataAnnotation, | |
33 ScopeContainerElement, | |
34 VariableElement; | |
35 | |
36 import 'tree/tree.dart' show | |
37 Import, | |
38 LibraryTag, | |
39 NamedArgument, | |
40 NewExpression, | |
41 Node; | |
42 | |
43 import 'util/util.dart' show | |
44 Link, | |
45 Spannable; | |
46 | |
47 /** | |
48 * Compiler task that analyzes MirrorsUsed annotations. | |
49 * | |
50 * When importing 'dart:mirrors', it is possible to annotate the import with | |
51 * MirrorsUsed annotation. This is a way to declare what elements will be | |
52 * reflected on at runtime. Such elements, even they would normally be | |
53 * discarded by the implicit tree-shaking algorithm must be preserved in the | |
54 * final output. | |
55 * | |
56 * Since some libraries cannot tell exactly what they will be reflecting on, it | |
57 * is possible for one library to specify a MirrorsUsed annotation that applies | |
58 * to another library. For example: | |
59 * | |
60 * Mirror utility library that cannot tell what it is reflecting on: | |
61 * library mirror_utils; | |
62 * import 'dart:mirrors'; | |
63 * ... | |
64 * | |
65 * The main app which knows how it use the mirror utility library: | |
66 * library main_app; | |
67 * @MirrorsUsed(override='mirror_utils') | |
68 * import 'dart:mirrors'; | |
69 * import 'mirror_utils.dart'; | |
70 * ... | |
71 * | |
72 * In this case, we say that @MirrorsUsed in main_app overrides @MirrorsUsed in | |
73 * mirror_utils. | |
74 * | |
75 * It is possible to override all libraries using override='*'. If multiple | |
76 * catch-all overrides like this, they are merged together. | |
77 * | |
78 * It is possible for library "a" to declare that it overrides library "b", and | |
79 * vice versa. In this case, both annotations will be discarded and the | |
80 * compiler will emit a hint (that is, a warning that is not specified by the | |
81 * language specification). | |
82 * | |
83 * After applying all the overrides, we can iterate over libraries that import | |
84 * 'dart:mirrors'. If a library does not have an associated MirrorsUsed | |
85 * annotation, then we have to discard all MirrorsUsed annotations and assume | |
86 * everything can be reflected on. | |
87 * | |
88 * On the other hand, if all libraries importing dart:mirrors have a | |
89 * MirrorsUsed annotation, these annotations are merged. | |
90 * | |
91 * MERGING MIRRORSUSED | |
92 * | |
93 * TBD. | |
94 */ | |
95 class MirrorUsageAnalyzerTask extends CompilerTask { | |
96 Set<LibraryElement> librariesWithUsage; | |
97 MirrorUsageAnalyzer analyzer; | |
98 | |
99 MirrorUsageAnalyzerTask(Compiler compiler) | |
100 : super(compiler) { | |
101 analyzer = new MirrorUsageAnalyzer(compiler, this); | |
102 } | |
103 | |
104 /// Collect @MirrorsUsed annotations in all libraries. Called by the | |
105 /// compiler after all libraries are loaded, but before resolution. | |
106 void analyzeUsage(LibraryElement mainApp) { | |
107 if (mainApp == null || compiler.mirrorsLibrary == null) return; | |
108 measure(analyzer.run); | |
109 List<String> symbols = analyzer.mergedMirrorUsage.symbols; | |
110 List<Element> targets = analyzer.mergedMirrorUsage.targets; | |
111 List<Element> metaTargets = analyzer.mergedMirrorUsage.metaTargets; | |
112 compiler.backend.registerMirrorUsage( | |
113 symbols == null ? null : new Set<String>.from(symbols), | |
114 targets == null ? null : new Set<Element>.from(targets), | |
115 metaTargets == null ? null : new Set<Element>.from(metaTargets)); | |
116 librariesWithUsage = analyzer.librariesWithUsage; | |
117 } | |
118 | |
119 /// Is there a @MirrorsUsed annotation in the library of [element]? Used by | |
120 /// the resolver to suppress hints about using new Symbol or | |
121 /// MirrorSystem.getName. | |
122 bool hasMirrorUsage(Element element) { | |
123 LibraryElement library = element.library; | |
124 // Internal libraries always have implicit mirror usage. | |
125 return library.isInternalLibrary | |
126 || (librariesWithUsage != null | |
127 && librariesWithUsage.contains(library)); | |
128 } | |
129 | |
130 /// Call-back from the resolver to analyze MirorsUsed annotations. The result | |
131 /// is stored in [analyzer] and later used to compute | |
132 /// [:analyzer.mergedMirrorUsage:]. | |
133 void validate(NewExpression node, TreeElements mapping) { | |
134 for (Node argument in node.send.arguments) { | |
135 NamedArgument named = argument.asNamedArgument(); | |
136 if (named == null) continue; | |
137 ConstantCompiler constantCompiler = compiler.resolver.constantCompiler; | |
138 ConstantValue value = | |
139 constantCompiler.compileNode(named.expression, mapping).value; | |
140 | |
141 MirrorUsageBuilder builder = | |
142 new MirrorUsageBuilder( | |
143 analyzer, mapping.analyzedElement.library, named.expression, | |
144 value, mapping); | |
145 | |
146 if (named.name.source == 'symbols') { | |
147 analyzer.cachedStrings[value] = | |
148 builder.convertConstantToUsageList(value, onlyStrings: true); | |
149 } else if (named.name.source == 'targets') { | |
150 analyzer.cachedElements[value] = | |
151 builder.resolveUsageList(builder.convertConstantToUsageList(value)); | |
152 } else if (named.name.source == 'metaTargets') { | |
153 analyzer.cachedElements[value] = | |
154 builder.resolveUsageList(builder.convertConstantToUsageList(value)); | |
155 } else if (named.name.source == 'override') { | |
156 analyzer.cachedElements[value] = | |
157 builder.resolveUsageList(builder.convertConstantToUsageList(value)); | |
158 } | |
159 } | |
160 } | |
161 } | |
162 | |
163 class MirrorUsageAnalyzer { | |
164 final Compiler compiler; | |
165 final MirrorUsageAnalyzerTask task; | |
166 List<LibraryElement> wildcard; | |
167 final Set<LibraryElement> librariesWithUsage; | |
168 final Map<ConstantValue, List<String>> cachedStrings; | |
169 final Map<ConstantValue, List<Element>> cachedElements; | |
170 MirrorUsage mergedMirrorUsage; | |
171 | |
172 MirrorUsageAnalyzer(Compiler compiler, this.task) | |
173 : compiler = compiler, | |
174 librariesWithUsage = new Set<LibraryElement>(), | |
175 cachedStrings = new Map<ConstantValue, List<String>>(), | |
176 cachedElements = new Map<ConstantValue, List<Element>>(); | |
177 | |
178 /// Collect and merge all @MirrorsUsed annotations. As a side-effect, also | |
179 /// compute which libraries have the annotation (which is used by | |
180 /// [MirrorUsageAnalyzerTask.hasMirrorUsage]). | |
181 void run() { | |
182 wildcard = compiler.libraryLoader.libraries.toList(); | |
183 Map<LibraryElement, List<MirrorUsage>> usageMap = | |
184 collectMirrorsUsedAnnotation(); | |
185 propagateOverrides(usageMap); | |
186 Set<LibraryElement> librariesWithoutUsage = new Set<LibraryElement>(); | |
187 usageMap.forEach((LibraryElement library, List<MirrorUsage> usage) { | |
188 if (usage.isEmpty) librariesWithoutUsage.add(library); | |
189 }); | |
190 if (librariesWithoutUsage.isEmpty) { | |
191 mergedMirrorUsage = mergeUsages(usageMap); | |
192 } else { | |
193 mergedMirrorUsage = new MirrorUsage(null, null, null, null); | |
194 } | |
195 } | |
196 | |
197 /// Collect all @MirrorsUsed from all libraries and represent them as | |
198 /// [MirrorUsage]. | |
199 Map<LibraryElement, List<MirrorUsage>> collectMirrorsUsedAnnotation() { | |
200 Map<LibraryElement, List<MirrorUsage>> result = | |
201 new Map<LibraryElement, List<MirrorUsage>>(); | |
202 for (LibraryElement library in compiler.libraryLoader.libraries) { | |
203 if (library.isInternalLibrary) continue; | |
204 for (LibraryTag tag in library.tags) { | |
205 Import importTag = tag.asImport(); | |
206 if (importTag == null) continue; | |
207 compiler.withCurrentElement(library, () { | |
208 List<MirrorUsage> usages = | |
209 mirrorsUsedOnLibraryTag(library, importTag); | |
210 if (usages != null) { | |
211 List<MirrorUsage> existing = result[library]; | |
212 if (existing != null) { | |
213 existing.addAll(usages); | |
214 } else { | |
215 result[library] = usages; | |
216 } | |
217 } | |
218 }); | |
219 } | |
220 } | |
221 return result; | |
222 } | |
223 | |
224 /// Apply [MirrorUsage] with 'override' to libraries they override. | |
225 void propagateOverrides(Map<LibraryElement, List<MirrorUsage>> usageMap) { | |
226 Map<LibraryElement, List<MirrorUsage>> propagatedOverrides = | |
227 new Map<LibraryElement, List<MirrorUsage>>(); | |
228 usageMap.forEach((LibraryElement library, List<MirrorUsage> usages) { | |
229 for (MirrorUsage usage in usages) { | |
230 List<Element> override = usage.override; | |
231 if (override == null) continue; | |
232 if (override == wildcard) { | |
233 for (LibraryElement overridden in wildcard) { | |
234 if (overridden != library) { | |
235 List<MirrorUsage> overriddenUsages = propagatedOverrides | |
236 .putIfAbsent(overridden, () => <MirrorUsage>[]); | |
237 overriddenUsages.add(usage); | |
238 } | |
239 } | |
240 } else { | |
241 for (Element overridden in override) { | |
242 List<MirrorUsage> overriddenUsages = propagatedOverrides | |
243 .putIfAbsent(overridden, () => <MirrorUsage>[]); | |
244 overriddenUsages.add(usage); | |
245 } | |
246 } | |
247 } | |
248 }); | |
249 propagatedOverrides.forEach((LibraryElement overridden, | |
250 List<MirrorUsage> overriddenUsages) { | |
251 List<MirrorUsage> usages = | |
252 usageMap.putIfAbsent(overridden, () => <MirrorUsage>[]); | |
253 usages.addAll(overriddenUsages); | |
254 }); | |
255 } | |
256 | |
257 /// Find @MirrorsUsed annotations on the given import [tag] in [library]. The | |
258 /// annotations are represented as [MirrorUsage]. | |
259 List<MirrorUsage> mirrorsUsedOnLibraryTag(LibraryElement library, | |
260 Import tag) { | |
261 LibraryElement importedLibrary = library.getLibraryFromTag(tag); | |
262 if (importedLibrary != compiler.mirrorsLibrary) { | |
263 return null; | |
264 } | |
265 List<MirrorUsage> result = <MirrorUsage>[]; | |
266 for (MetadataAnnotation metadata in tag.metadata) { | |
267 metadata.ensureResolved(compiler); | |
268 Element element = metadata.constant.value.computeType(compiler).element; | |
269 if (element == compiler.mirrorsUsedClass) { | |
270 result.add(buildUsage(metadata.constant.value)); | |
271 } | |
272 } | |
273 return result; | |
274 } | |
275 | |
276 /// Merge all [MirrorUsage] instances accross all libraries. | |
277 MirrorUsage mergeUsages(Map<LibraryElement, List<MirrorUsage>> usageMap) { | |
278 Set<MirrorUsage> usagesToMerge = new Set<MirrorUsage>(); | |
279 usageMap.forEach((LibraryElement library, List<MirrorUsage> usages) { | |
280 librariesWithUsage.add(library); | |
281 usagesToMerge.addAll(usages); | |
282 }); | |
283 if (usagesToMerge.isEmpty) { | |
284 return new MirrorUsage(null, wildcard, null, null); | |
285 } else { | |
286 MirrorUsage result = new MirrorUsage(null, null, null, null); | |
287 for (MirrorUsage usage in usagesToMerge) { | |
288 result = merge(result, usage); | |
289 } | |
290 return result; | |
291 } | |
292 } | |
293 | |
294 /// Merge [a] with [b]. The resulting [MirrorUsage] simply has the symbols, | |
295 /// targets, and metaTargets of [a] and [b] concatenated. 'override' is | |
296 /// ignored. | |
297 MirrorUsage merge(MirrorUsage a, MirrorUsage b) { | |
298 // TOOO(ahe): Should be an instance method on MirrorUsage. | |
299 if (a.symbols == null && a.targets == null && a.metaTargets == null) { | |
300 return b; | |
301 } else if ( | |
302 b.symbols == null && b.targets == null && b.metaTargets == null) { | |
303 return a; | |
304 } | |
305 // TODO(ahe): Test the following cases. | |
306 List<String> symbols = a.symbols; | |
307 if (symbols == null) { | |
308 symbols = b.symbols; | |
309 } else if (b.symbols != null) { | |
310 symbols.addAll(b.symbols); | |
311 } | |
312 List<Element> targets = a.targets; | |
313 if (targets == null) { | |
314 targets = b.targets; | |
315 } else if (targets != wildcard && b.targets != null) { | |
316 targets.addAll(b.targets); | |
317 } | |
318 List<Element> metaTargets = a.metaTargets; | |
319 if (metaTargets == null) { | |
320 metaTargets = b.metaTargets; | |
321 } else if (metaTargets != wildcard && b.metaTargets != null) { | |
322 metaTargets.addAll(b.metaTargets); | |
323 } | |
324 return new MirrorUsage(symbols, targets, metaTargets, null); | |
325 } | |
326 | |
327 /// Convert a [constant] to an instance of [MirrorUsage] using information | |
328 /// that was resolved during [MirrorUsageAnalyzerTask.validate]. | |
329 MirrorUsage buildUsage(ConstructedConstantValue constant) { | |
330 Map<Element, ConstantValue> fields = constant.fieldElements; | |
331 VariableElement symbolsField = compiler.mirrorsUsedClass.lookupLocalMember( | |
332 'symbols'); | |
333 VariableElement targetsField = compiler.mirrorsUsedClass.lookupLocalMember( | |
334 'targets'); | |
335 VariableElement metaTargetsField = | |
336 compiler.mirrorsUsedClass.lookupLocalMember( | |
337 'metaTargets'); | |
338 VariableElement overrideField = compiler.mirrorsUsedClass.lookupLocalMember( | |
339 'override'); | |
340 | |
341 return new MirrorUsage( | |
342 cachedStrings[fields[symbolsField]], | |
343 cachedElements[fields[targetsField]], | |
344 cachedElements[fields[metaTargetsField]], | |
345 cachedElements[fields[overrideField]]); | |
346 } | |
347 } | |
348 | |
349 /// Used to represent a resolved MirrorsUsed constant. | |
350 class MirrorUsage { | |
351 final List<String> symbols; | |
352 final List<Element> targets; | |
353 final List<Element> metaTargets; | |
354 final List<Element> override; | |
355 | |
356 MirrorUsage(this.symbols, this.targets, this.metaTargets, this.override); | |
357 | |
358 String toString() { | |
359 return | |
360 'MirrorUsage(' | |
361 'symbols = $symbols, ' | |
362 'targets = $targets, ' | |
363 'metaTargets = $metaTargets, ' | |
364 'override = $override' | |
365 ')'; | |
366 | |
367 } | |
368 } | |
369 | |
370 class MirrorUsageBuilder { | |
371 final MirrorUsageAnalyzer analyzer; | |
372 final LibraryElement enclosingLibrary; | |
373 final Spannable spannable; | |
374 final ConstantValue constant; | |
375 final TreeElements elements; | |
376 | |
377 MirrorUsageBuilder( | |
378 this.analyzer, | |
379 this.enclosingLibrary, | |
380 this.spannable, | |
381 this.constant, | |
382 this.elements); | |
383 | |
384 Compiler get compiler => analyzer.compiler; | |
385 | |
386 /// Convert a constant to a list of [String] and [Type] values. If the | |
387 /// constant is a single [String], it is assumed to be a comma-separated list | |
388 /// of qualified names. If the constant is a [Type] t, the result is [:[t]:]. | |
389 /// Otherwise, the constant is assumed to represent a list of strings (each a | |
390 /// qualified name) and types, and such a list is constructed. If | |
391 /// [onlyStrings] is true, the returned list is a [:List<String>:] and any | |
392 /// [Type] values are treated as an error (meaning that the value is ignored | |
393 /// and a hint is emitted). | |
394 List convertConstantToUsageList( | |
395 ConstantValue constant, { bool onlyStrings: false }) { | |
396 if (constant.isNull) { | |
397 return null; | |
398 } else if (constant.isList) { | |
399 ListConstantValue list = constant; | |
400 List result = onlyStrings ? <String> [] : []; | |
401 for (ConstantValue entry in list.entries) { | |
402 if (entry.isString) { | |
403 StringConstantValue string = entry; | |
404 result.add(string.primitiveValue.slowToString()); | |
405 } else if (!onlyStrings && entry.isType) { | |
406 TypeConstantValue type = entry; | |
407 result.add(type.representedType); | |
408 } else { | |
409 Spannable node = positionOf(entry); | |
410 MessageKind kind = onlyStrings | |
411 ? MessageKind.MIRRORS_EXPECTED_STRING | |
412 : MessageKind.MIRRORS_EXPECTED_STRING_OR_TYPE; | |
413 compiler.reportHint( | |
414 node, | |
415 kind, {'name': node, 'type': apiTypeOf(entry)}); | |
416 } | |
417 } | |
418 return result; | |
419 } else if (!onlyStrings && constant.isType) { | |
420 TypeConstantValue type = constant; | |
421 return [type.representedType]; | |
422 } else if (constant.isString) { | |
423 StringConstantValue string = constant; | |
424 var iterable = | |
425 string.primitiveValue.slowToString().split(',').map((e) => e.trim()); | |
426 return onlyStrings ? new List<String>.from(iterable) : iterable.toList(); | |
427 } else { | |
428 Spannable node = positionOf(constant); | |
429 MessageKind kind = onlyStrings | |
430 ? MessageKind.MIRRORS_EXPECTED_STRING_OR_LIST | |
431 : MessageKind.MIRRORS_EXPECTED_STRING_TYPE_OR_LIST; | |
432 compiler.reportHint( | |
433 node, | |
434 kind, {'name': node, 'type': apiTypeOf(constant)}); | |
435 return null; | |
436 } | |
437 } | |
438 | |
439 /// Find the first non-implementation interface of constant. | |
440 DartType apiTypeOf(ConstantValue constant) { | |
441 DartType type = constant.computeType(compiler); | |
442 LibraryElement library = type.element.library; | |
443 if (type.isInterfaceType && library.isInternalLibrary) { | |
444 InterfaceType interface = type; | |
445 ClassElement cls = type.element; | |
446 cls.ensureResolved(compiler); | |
447 for (DartType supertype in cls.allSupertypes) { | |
448 if (supertype.isInterfaceType | |
449 && !supertype.element.library.isInternalLibrary) { | |
450 return interface.asInstanceOf(supertype.element); | |
451 } | |
452 } | |
453 } | |
454 return type; | |
455 } | |
456 | |
457 /// Convert a list of strings and types to a list of elements. Types are | |
458 /// converted to their corresponding element, and strings are resolved as | |
459 /// follows: | |
460 /// | |
461 /// First find the longest library name that is a prefix of the string, if | |
462 /// there are none, resolve using [resolveExpression]. Otherwise, resolve the | |
463 /// rest of the string using [resolveLocalExpression]. | |
464 List<Element> resolveUsageList(List list) { | |
465 if (list == null) return null; | |
466 if (list.length == 1 && list[0] == '*') { | |
467 return analyzer.wildcard; | |
468 } | |
469 List<Element> result = <Element>[]; | |
470 for (var entry in list) { | |
471 if (entry is DartType) { | |
472 DartType type = entry; | |
473 result.add(type.element); | |
474 } else { | |
475 String string = entry; | |
476 LibraryElement libraryCandiate; | |
477 String libraryNameCandiate; | |
478 for (LibraryElement l in compiler.libraryLoader.libraries) { | |
479 if (l.hasLibraryName()) { | |
480 String libraryName = l.getLibraryOrScriptName(); | |
481 if (string == libraryName) { | |
482 // Found an exact match. | |
483 libraryCandiate = l; | |
484 libraryNameCandiate = libraryName; | |
485 break; | |
486 } else if (string.startsWith('$libraryName.')) { | |
487 if (libraryNameCandiate == null | |
488 || libraryNameCandiate.length < libraryName.length) { | |
489 // Found a better candiate | |
490 libraryCandiate = l; | |
491 libraryNameCandiate = libraryName; | |
492 } | |
493 } | |
494 } | |
495 } | |
496 Element e; | |
497 if (libraryNameCandiate == string) { | |
498 e = libraryCandiate; | |
499 } else if (libraryNameCandiate != null) { | |
500 e = resolveLocalExpression( | |
501 libraryCandiate, | |
502 string.substring(libraryNameCandiate.length + 1).split('.')); | |
503 } else { | |
504 e = resolveExpression(string); | |
505 } | |
506 if (e != null) result.add(e); | |
507 } | |
508 } | |
509 return result; | |
510 } | |
511 | |
512 /// Resolve [expression] in [enclosingLibrary]'s import scope. | |
513 Element resolveExpression(String expression) { | |
514 List<String> identifiers = expression.split('.'); | |
515 Element element = enclosingLibrary.find(identifiers[0]); | |
516 if (element == null) { | |
517 compiler.reportHint( | |
518 spannable, MessageKind.MIRRORS_CANNOT_RESOLVE_IN_CURRENT_LIBRARY, | |
519 {'name': expression}); | |
520 return null; | |
521 } else { | |
522 if (identifiers.length == 1) return element; | |
523 return resolveLocalExpression(element, identifiers.sublist(1)); | |
524 } | |
525 } | |
526 | |
527 /// Resolve [identifiers] in [element]'s local members. | |
528 Element resolveLocalExpression(Element element, List<String> identifiers) { | |
529 Element current = element; | |
530 for (String identifier in identifiers) { | |
531 Element e = findLocalMemberIn(current, identifier); | |
532 if (e == null) { | |
533 if (current.isLibrary) { | |
534 LibraryElement library = current; | |
535 compiler.reportHint( | |
536 spannable, MessageKind.MIRRORS_CANNOT_RESOLVE_IN_LIBRARY, | |
537 {'name': identifiers[0], | |
538 'library': library.getLibraryOrScriptName()}); | |
539 } else { | |
540 compiler.reportHint( | |
541 spannable, MessageKind.MIRRORS_CANNOT_FIND_IN_ELEMENT, | |
542 {'name': identifier, 'element': current.name}); | |
543 } | |
544 return current; | |
545 } | |
546 current = e; | |
547 } | |
548 return current; | |
549 } | |
550 | |
551 /// Helper method to lookup members in a [ScopeContainerElement]. If | |
552 /// [element] is not a ScopeContainerElement, return null. | |
553 Element findLocalMemberIn(Element element, String name) { | |
554 if (element is ScopeContainerElement) { | |
555 ScopeContainerElement scope = element; | |
556 if (element.isClass) { | |
557 ClassElement cls = element; | |
558 cls.ensureResolved(compiler); | |
559 } | |
560 return scope.localLookup(name); | |
561 } | |
562 return null; | |
563 } | |
564 | |
565 /// Attempt to find a [Spannable] corresponding to constant. | |
566 Spannable positionOf(ConstantValue constant) { | |
567 Node node; | |
568 elements.forEachConstantNode((Node n, ConstantExpression c) { | |
569 if (node == null && c.value == constant) { | |
570 node = n; | |
571 } | |
572 }); | |
573 if (node == null) { | |
574 // TODO(ahe): Returning [spannable] here leads to confusing error | |
575 // messages. For example, consider: | |
576 // @MirrorsUsed(targets: fisk) | |
577 // import 'dart:mirrors'; | |
578 // | |
579 // const fisk = const [main]; | |
580 // | |
581 // main() {} | |
582 // | |
583 // The message is: | |
584 // example.dart:1:23: Hint: Can't use 'fisk' here because ... | |
585 // Did you forget to add quotes? | |
586 // @MirrorsUsed(targets: fisk) | |
587 // ^^^^ | |
588 // | |
589 // Instead of saying 'fisk' should pretty print the problematic constant | |
590 // value. | |
591 return spannable; | |
592 } | |
593 return node; | |
594 } | |
595 } | |
OLD | NEW |