OLD | NEW |
| (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 library initialize.build.initializer_plugin; | |
5 | |
6 import 'package:analyzer/src/generated/ast.dart'; | |
7 import 'package:analyzer/src/generated/element.dart'; | |
8 import 'package:barback/barback.dart'; | |
9 import 'package:code_transformers/resolver.dart'; | |
10 import 'package:initialize/transformer.dart'; | |
11 import 'package:path/path.dart' as path; | |
12 | |
13 /// A plug which allows an initializer to write out an [InitEntry] given some | |
14 /// [InitializerData] from an annotation that was found. | |
15 abstract class InitializerPlugin { | |
16 /// Whether or not this plugin should be applied to an [Initializer] given | |
17 /// some [InitializerData]. If [true] is returned then this plugin will take | |
18 /// ownership of this [InitializerData] and no subsequent plugins will have | |
19 /// an opportunity to access it. | |
20 bool shouldApply(InitializerPluginData data); | |
21 | |
22 /// Returns a [String] or [null]. The [String] should represent dart code | |
23 /// which creates a new [InitEntry] and that entry is added to the static | |
24 /// initializers list. If [null] is returned then no entry is added at all for | |
25 /// this [InitializerData]. | |
26 String apply(InitializerPluginData data); | |
27 } | |
28 | |
29 /// A class which wraps all the default data passed to an [InitializerPlugin] | |
30 /// for each annotation. | |
31 class InitializerPluginData { | |
32 final InitializerData initializer; | |
33 final AssetId bootstrapId; | |
34 final Map<LibraryElement, String> libraryPrefixes; | |
35 final TransformLogger logger; | |
36 final Resolver resolver; | |
37 InitializerPluginData(this.initializer, this.bootstrapId, | |
38 this.libraryPrefixes, this.resolver, this.logger); | |
39 } | |
40 | |
41 /// The basic [InitializerPlugin]. This generates a new [InitEntry] to be added | |
42 /// to the static initializers list, and applies to every item it sees. | |
43 class DefaultInitializerPlugin implements InitializerPlugin { | |
44 const DefaultInitializerPlugin(); | |
45 | |
46 /// Applies to everything. Put other plugins before this one to override this | |
47 /// behaviour. | |
48 bool shouldApply(InitializerPluginData data) => true; | |
49 | |
50 /// Creates a normal [InitEntry] string. | |
51 String apply(InitializerPluginData pluginData) { | |
52 var target = buildTarget(pluginData); | |
53 var meta = buildMeta(pluginData); | |
54 return 'new InitEntry($meta, $target)'; | |
55 } | |
56 | |
57 /// Builds a [String] representing the meta of an [InitEntry] given an | |
58 /// [ElementAnnotation] that was found. | |
59 String buildMeta(InitializerPluginData pluginData) { | |
60 var logger = pluginData.logger; | |
61 var element = pluginData.initializer.targetElement; | |
62 var elementAnnotation = pluginData.initializer.annotationElement; | |
63 var elementAnnotationElement = elementAnnotation.element; | |
64 var libraryPrefixes = pluginData.libraryPrefixes; | |
65 if (elementAnnotationElement is ConstructorElement) { | |
66 return buildConstructorMeta(elementAnnotation, pluginData); | |
67 } else if (elementAnnotationElement is PropertyAccessorElement) { | |
68 return buildPropertyMeta(elementAnnotation, pluginData); | |
69 } else { | |
70 logger.error('Unsupported annotation type. Only constructors and ' | |
71 'properties are supported as initializers.'); | |
72 } | |
73 return null; | |
74 } | |
75 | |
76 /// Builds a [String] representing the meta of an [InitEntry] given an | |
77 /// [ElementAnnotation] whose element was a [ConstructorElement]. | |
78 String buildConstructorMeta( | |
79 ElementAnnotation elementAnnotation, InitializerPluginData pluginData) { | |
80 var logger = pluginData.logger; | |
81 var node = pluginData.initializer.targetNode; | |
82 var metaPrefix = | |
83 pluginData.libraryPrefixes[elementAnnotation.element.library]; | |
84 | |
85 var annotation = pluginData.initializer.annotationNode; | |
86 if (annotation == null) { | |
87 logger.error( | |
88 'Initializer annotations are only supported on libraries, classes, ' | |
89 'and top level methods. Found $node.'); | |
90 } | |
91 var clazz = annotation.name; | |
92 var constructor = annotation.constructorName == null | |
93 ? '' | |
94 : '.${annotation.constructorName}'; | |
95 var args = buildArgumentList(annotation.arguments, pluginData); | |
96 return 'const $metaPrefix.${clazz}$constructor$args'; | |
97 } | |
98 | |
99 /// Builds a [String] representing the meta of an [InitEntry] given an | |
100 /// [ElementAnnotation] whose element was a [PropertyAccessorElement]. | |
101 String buildPropertyMeta( | |
102 ElementAnnotation annotation, InitializerPluginData pluginData) { | |
103 var metaPrefix = pluginData.libraryPrefixes[annotation.element.library]; | |
104 return '$metaPrefix.${annotation.element.name}'; | |
105 } | |
106 | |
107 /// Builds a [String] for the target of an [InitEntry] given an [Element] that | |
108 /// was annotated. | |
109 String buildTarget(InitializerPluginData pluginData) { | |
110 var element = pluginData.initializer.targetElement; | |
111 var logger = pluginData.logger; | |
112 if (element is LibraryElement) { | |
113 return buildLibraryTarget(element, pluginData); | |
114 } else if (element is ClassElement) { | |
115 return buildClassTarget(element, pluginData); | |
116 } else if (element is FunctionElement) { | |
117 return buildFunctionTarget(element, pluginData); | |
118 } else { | |
119 logger.error('Initializers can only be applied to top level functions, ' | |
120 'libraries, and classes.'); | |
121 } | |
122 return null; | |
123 } | |
124 | |
125 /// Builds a [String] for the target of an [InitEntry] given [element] which | |
126 /// is an annotated class. | |
127 String buildClassTarget( | |
128 ClassElement element, InitializerPluginData pluginData) => | |
129 buildSimpleTarget(element, pluginData); | |
130 | |
131 /// Builds a [String] for the target of an [InitEntry] given [element] which | |
132 /// is an annotated function. | |
133 String buildFunctionTarget( | |
134 FunctionElement element, InitializerPluginData pluginData) => | |
135 buildSimpleTarget(element, pluginData); | |
136 | |
137 /// Builds a [String] for the target of an [InitEntry] for a simple [Element]. | |
138 /// This is just the library prefix followed by the element name. | |
139 String buildSimpleTarget(Element element, InitializerPluginData pluginData) => | |
140 '${pluginData.libraryPrefixes[element.library]}.${element.name}'; | |
141 | |
142 /// Builds a [String] for the target of an [InitEntry] given [element] which | |
143 /// is an annotated library. | |
144 String buildLibraryTarget( | |
145 LibraryElement element, InitializerPluginData pluginData) { | |
146 var bootstrapId = pluginData.bootstrapId; | |
147 var logger = pluginData.logger; | |
148 var segments = element.source.uri.pathSegments; | |
149 var package = segments[0]; | |
150 var libraryPath; | |
151 var packageString; | |
152 if (bootstrapId.package == package && | |
153 bootstrapId.path.startsWith('${segments[1]}/')) { | |
154 // reset `package` to null, we will do a relative path in this case. | |
155 packageString = 'null'; | |
156 libraryPath = path.url.relative( | |
157 path.url.joinAll(segments.getRange(1, segments.length)), | |
158 from: path.url.dirname(path.url.join(bootstrapId.path))); | |
159 } else if (segments[1] == 'lib') { | |
160 packageString = "'$package'"; | |
161 libraryPath = path.url.joinAll(segments.getRange(2, segments.length)); | |
162 } else { | |
163 logger.error('Unable to import `${element.source.uri.path}` from ' | |
164 '${bootstrapId.path}.'); | |
165 } | |
166 | |
167 return "const LibraryIdentifier" | |
168 "(#${element.name}, $packageString, '$libraryPath')"; | |
169 } | |
170 | |
171 /// Builds a [String] representing an [ArgumentList] taking into account the | |
172 /// [libraryPrefixes] from [pluginData]. | |
173 String buildArgumentList( | |
174 ArgumentList args, InitializerPluginData pluginData) { | |
175 var buffer = new StringBuffer(); | |
176 buffer.write('('); | |
177 var first = true; | |
178 for (var arg in args.arguments) { | |
179 if (!first) buffer.write(', '); | |
180 first = false; | |
181 | |
182 Expression expression; | |
183 if (arg is NamedExpression) { | |
184 buffer.write('${arg.name.label.name}: '); | |
185 expression = arg.expression; | |
186 } else { | |
187 expression = arg; | |
188 } | |
189 | |
190 buffer.write(buildExpression(expression, pluginData)); | |
191 } | |
192 buffer.write(')'); | |
193 return buffer.toString(); | |
194 } | |
195 | |
196 /// Builds a [String] representing [expression] taking into account the | |
197 /// [libraryPrefixes] from [pluginData]. | |
198 String buildExpression( | |
199 Expression expression, InitializerPluginData pluginData) { | |
200 var logger = pluginData.logger; | |
201 var libraryPrefixes = pluginData.libraryPrefixes; | |
202 var buffer = new StringBuffer(); | |
203 if (expression is StringLiteral && expression.stringValue != null) { | |
204 buffer.write(_stringValue(expression.stringValue)); | |
205 } else if (expression is BooleanLiteral || | |
206 expression is DoubleLiteral || | |
207 expression is IntegerLiteral || | |
208 expression is NullLiteral) { | |
209 buffer.write('${expression}'); | |
210 } else if (expression is ListLiteral) { | |
211 buffer.write('const ['); | |
212 var first = true; | |
213 for (Expression listExpression in expression.elements) { | |
214 if (!first) buffer.write(', '); | |
215 first = false; | |
216 buffer.write(buildExpression(listExpression, pluginData)); | |
217 } | |
218 buffer.write(']'); | |
219 } else if (expression is MapLiteral) { | |
220 buffer.write('const {'); | |
221 var first = true; | |
222 for (MapLiteralEntry entry in expression.entries) { | |
223 if (!first) buffer.write(', '); | |
224 first = false; | |
225 buffer.write(buildExpression(entry.key, pluginData)); | |
226 buffer.write(': '); | |
227 buffer.write(buildExpression(entry.value, pluginData)); | |
228 } | |
229 buffer.write('}'); | |
230 } else if (expression is Identifier) { | |
231 var element = expression.bestElement; | |
232 if (element == null) { | |
233 logger.error('Unable to get `bestElement` for expression: $expression'); | |
234 } else if (!element.isPublic) { | |
235 // Inline the evaluated value of private identifiers. | |
236 buffer.write(_evaluateExpression(expression, pluginData)); | |
237 } else { | |
238 libraryPrefixes.putIfAbsent( | |
239 element.library, () => 'i${libraryPrefixes.length}'); | |
240 | |
241 buffer.write('${libraryPrefixes[element.library]}.'); | |
242 if (element is ClassElement) { | |
243 buffer.write(element.name); | |
244 } else if (element is PropertyAccessorElement) { | |
245 var variable = element.variable; | |
246 if (variable is FieldElement) { | |
247 buffer.write('${variable.enclosingElement.name}.'); | |
248 } | |
249 buffer.write('${variable.name}'); | |
250 } else { | |
251 logger.error('Unsupported argument to initializer constructor.'); | |
252 } | |
253 } | |
254 } else if (expression is PropertyAccess) { | |
255 buffer.write(buildExpression(expression.target, pluginData)); | |
256 buffer.write('.${expression.propertyName}'); | |
257 } else if (expression is InstanceCreationExpression) { | |
258 logger.error('Unsupported expression in initializer, found $expression. ' | |
259 'Instance creation expressions are not supported (yet). Instead, ' | |
260 'please assign it to a const variable and use that instead.'); | |
261 } else { | |
262 buffer.write(_evaluateExpression(expression, pluginData)); | |
263 } | |
264 return buffer.toString(); | |
265 } | |
266 | |
267 _evaluateExpression(Expression expression, InitializerPluginData pluginData) { | |
268 var logger = pluginData.logger; | |
269 var result = pluginData.resolver.evaluateConstant( | |
270 pluginData.initializer.targetElement.library, expression); | |
271 if (!result.isValid) { | |
272 logger.error('Invalid expression in initializer, found $expression. ' | |
273 'And got the following errors: ${result.errors}.'); | |
274 return null; | |
275 } | |
276 var value = result.value.value; | |
277 if (value == null) { | |
278 logger.error('Unsupported expression in initializer, found ' | |
279 '$expression. Please file a bug at ' | |
280 'https://github.com/dart-lang/initialize/issues'); | |
281 return null; | |
282 } | |
283 | |
284 if (value is String) value = _stringValue(value); | |
285 return value; | |
286 } | |
287 | |
288 // Returns an expression for a string value. Wraps it in single quotes and | |
289 // escapes existing single quotes and escapes. | |
290 _stringValue(String value) { | |
291 value = value.replaceAll(r'\', r'\\').replaceAll(r"'", r"\'"); | |
292 return "'$value'"; | |
293 } | |
294 } | |
OLD | NEW |