OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * Code transform for @observable. The core transformation is relatively | 6 * Code transform for @observable. This library will be removed soon, it simply |
7 * straightforward, and essentially like an editor refactoring. | 7 * reexports `observe.transformer`. |
8 */ | 8 */ |
| 9 // TODO(sigmund): deprecate and delete. |
9 library observe.transform; | 10 library observe.transform; |
10 | 11 |
11 import 'dart:async'; | 12 export 'transformer.dart'; |
12 | |
13 import 'package:analyzer/src/generated/java_core.dart' show CharSequence; | |
14 import 'package:analyzer/src/generated/ast.dart'; | |
15 import 'package:analyzer/src/generated/error.dart'; | |
16 import 'package:analyzer/src/generated/parser.dart'; | |
17 import 'package:analyzer/src/generated/scanner.dart'; | |
18 import 'package:barback/barback.dart'; | |
19 import 'package:source_maps/refactor.dart'; | |
20 import 'package:source_maps/span.dart' show SourceFile; | |
21 | |
22 /** | |
23 * A [Transformer] that replaces observables based on dirty-checking with an | |
24 * implementation based on change notifications. | |
25 * | |
26 * The transformation adds hooks for field setters and notifies the observation | |
27 * system of the change. | |
28 */ | |
29 class ObservableTransformer extends Transformer { | |
30 | |
31 Future<bool> isPrimary(Asset input) { | |
32 if (input.id.extension != '.dart') return new Future.value(false); | |
33 // Note: technically we should parse the file to find accurately the | |
34 // observable annotation, but that seems expensive. It would require almost | |
35 // as much work as applying the transform. We rather have some false | |
36 // positives here, and then generate no outputs when we apply this | |
37 // transform. | |
38 return input.readAsString().then( | |
39 (c) => c.contains("@observable") || c.contains("@published")); | |
40 } | |
41 | |
42 Future apply(Transform transform) { | |
43 return transform.primaryInput.readAsString().then((content) { | |
44 var id = transform.primaryInput.id; | |
45 // TODO(sigmund): improve how we compute this url | |
46 var url = id.path.startsWith('lib/') | |
47 ? 'package:${id.package}/${id.path.substring(4)}' : id.path; | |
48 var sourceFile = new SourceFile.text(url, content); | |
49 var transaction = _transformCompilationUnit( | |
50 content, sourceFile, transform.logger); | |
51 if (!transaction.hasEdits) { | |
52 transform.addOutput(transform.primaryInput); | |
53 return; | |
54 } | |
55 var printer = transaction.commit(); | |
56 // TODO(sigmund): emit source maps when barback supports it (see | |
57 // dartbug.com/12340) | |
58 printer.build(url); | |
59 transform.addOutput(new Asset.fromString(id, printer.text)); | |
60 }); | |
61 } | |
62 } | |
63 | |
64 TextEditTransaction _transformCompilationUnit( | |
65 String inputCode, SourceFile sourceFile, TransformLogger logger) { | |
66 var unit = _parseCompilationUnit(inputCode); | |
67 var code = new TextEditTransaction(inputCode, sourceFile); | |
68 for (var directive in unit.directives) { | |
69 if (directive is LibraryDirective && _hasObservable(directive)) { | |
70 logger.warning('@observable on a library no longer has any effect. ' | |
71 'It should be placed on individual fields.', | |
72 span: _getSpan(sourceFile, directive)); | |
73 break; | |
74 } | |
75 } | |
76 | |
77 for (var declaration in unit.declarations) { | |
78 if (declaration is ClassDeclaration) { | |
79 _transformClass(declaration, code, sourceFile, logger); | |
80 } else if (declaration is TopLevelVariableDeclaration) { | |
81 if (_hasObservable(declaration)) { | |
82 logger.warning('Top-level fields can no longer be observable. ' | |
83 'Observable fields should be put in an observable objects.', | |
84 span: _getSpan(sourceFile, declaration)); | |
85 } | |
86 } | |
87 } | |
88 return code; | |
89 } | |
90 | |
91 /** Parse [code] using analyzer. */ | |
92 CompilationUnit _parseCompilationUnit(String code) { | |
93 var errorListener = new _ErrorCollector(); | |
94 var reader = new CharSequenceReader(new CharSequence(code)); | |
95 var scanner = new Scanner(null, reader, errorListener); | |
96 var token = scanner.tokenize(); | |
97 var parser = new Parser(null, errorListener); | |
98 return parser.parseCompilationUnit(token); | |
99 } | |
100 | |
101 class _ErrorCollector extends AnalysisErrorListener { | |
102 final errors = <AnalysisError>[]; | |
103 onError(error) => errors.add(error); | |
104 } | |
105 | |
106 _getSpan(SourceFile file, ASTNode node) => file.span(node.offset, node.end); | |
107 | |
108 /** True if the node has the `@observable` or `@published` annotation. */ | |
109 // TODO(jmesserly): it is not good to be hard coding Polymer support here. | |
110 bool _hasObservable(AnnotatedNode node) => | |
111 node.metadata.any(_isObservableAnnotation); | |
112 | |
113 // TODO(jmesserly): this isn't correct if the annotation has been imported | |
114 // with a prefix, or cases like that. We should technically be resolving, but | |
115 // that is expensive in analyzer, so it isn't feasible yet. | |
116 bool _isObservableAnnotation(Annotation node) => | |
117 _isAnnotationContant(node, 'observable') || | |
118 _isAnnotationContant(node, 'published') || | |
119 _isAnnotationType(node, 'ObservableProperty') || | |
120 _isAnnotationType(node, 'PublishedProperty'); | |
121 | |
122 bool _isAnnotationContant(Annotation m, String name) => | |
123 m.name.name == name && m.constructorName == null && m.arguments == null; | |
124 | |
125 bool _isAnnotationType(Annotation m, String name) => m.name == name; | |
126 | |
127 void _transformClass(ClassDeclaration cls, TextEditTransaction code, | |
128 SourceFile file, TransformLogger logger) { | |
129 | |
130 if (_hasObservable(cls)) { | |
131 logger.warning('@observable on a class no longer has any effect. ' | |
132 'It should be placed on individual fields.', | |
133 span: _getSpan(file, cls)); | |
134 } | |
135 | |
136 // We'd like to track whether observable was declared explicitly, otherwise | |
137 // report a warning later below. Because we don't have type analysis (only | |
138 // syntactic understanding of the code), we only report warnings that are | |
139 // known to be true. | |
140 var explicitObservable = false; | |
141 var implicitObservable = false; | |
142 if (cls.extendsClause != null) { | |
143 var id = _getSimpleIdentifier(cls.extendsClause.superclass.name); | |
144 if (id.name == 'Observable') { | |
145 code.edit(id.offset, id.end, 'ChangeNotifier'); | |
146 explicitObservable = true; | |
147 } else if (id.name == 'ChangeNotifier') { | |
148 explicitObservable = true; | |
149 } else if (id.name != 'HtmlElement' && id.name != 'CustomElement' | |
150 && id.name != 'Object') { | |
151 // TODO(sigmund): this is conservative, consider using type-resolution to | |
152 // improve this check. | |
153 implicitObservable = true; | |
154 } | |
155 } | |
156 | |
157 if (cls.withClause != null) { | |
158 for (var type in cls.withClause.mixinTypes) { | |
159 var id = _getSimpleIdentifier(type.name); | |
160 if (id.name == 'Observable') { | |
161 code.edit(id.offset, id.end, 'ChangeNotifier'); | |
162 explicitObservable = true; | |
163 break; | |
164 } else if (id.name == 'ChangeNotifier') { | |
165 explicitObservable = true; | |
166 break; | |
167 } else { | |
168 // TODO(sigmund): this is conservative, consider using type-resolution | |
169 // to improve this check. | |
170 implicitObservable = true; | |
171 } | |
172 } | |
173 } | |
174 | |
175 if (cls.implementsClause != null) { | |
176 // TODO(sigmund): consider adding type-resolution to give a more precise | |
177 // answer. | |
178 implicitObservable = true; | |
179 } | |
180 | |
181 var declaresObservable = explicitObservable || implicitObservable; | |
182 | |
183 // Track fields that were transformed. | |
184 var instanceFields = new Set<String>(); | |
185 var getters = new List<String>(); | |
186 var setters = new List<String>(); | |
187 | |
188 for (var member in cls.members) { | |
189 if (member is FieldDeclaration) { | |
190 if (member.isStatic) { | |
191 if (_hasObservable(member)){ | |
192 logger.warning('Static fields can no longer be observable. ' | |
193 'Observable fields should be put in an observable objects.', | |
194 span: _getSpan(file, member)); | |
195 } | |
196 continue; | |
197 } | |
198 if (_hasObservable(member)) { | |
199 if (!declaresObservable) { | |
200 logger.warning('Observable fields should be put in an observable ' | |
201 'objects. Please declare that this class extends from ' | |
202 'Observable, includes Observable, or implements ' | |
203 'Observable.', | |
204 span: _getSpan(file, member)); | |
205 } | |
206 _transformFields(file, member, code, logger); | |
207 | |
208 var names = member.fields.variables.map((v) => v.name.name); | |
209 | |
210 getters.addAll(names); | |
211 if (!_isReadOnly(member.fields)) { | |
212 setters.addAll(names); | |
213 instanceFields.addAll(names); | |
214 } | |
215 } | |
216 } | |
217 // TODO(jmesserly): this is a temporary workaround until we can remove | |
218 // getValueWorkaround and setValueWorkaround. | |
219 if (member is MethodDeclaration) { | |
220 if (_hasKeyword(member.propertyKeyword, Keyword.GET)) { | |
221 getters.add(member.name.name); | |
222 } else if (_hasKeyword(member.propertyKeyword, Keyword.SET)) { | |
223 setters.add(member.name.name); | |
224 } | |
225 } | |
226 } | |
227 | |
228 // If nothing was @observable, bail. | |
229 if (instanceFields.length == 0) return; | |
230 | |
231 if (!explicitObservable) _mixinObservable(cls, code); | |
232 | |
233 // Fix initializers, because they aren't allowed to call the setter. | |
234 for (var member in cls.members) { | |
235 if (member is ConstructorDeclaration) { | |
236 _fixConstructor(member, code, instanceFields); | |
237 } | |
238 } | |
239 } | |
240 | |
241 /** Adds "with ChangeNotifier" and associated implementation. */ | |
242 void _mixinObservable(ClassDeclaration cls, TextEditTransaction code) { | |
243 // Note: we need to be careful to put the with clause after extends, but | |
244 // before implements clause. | |
245 if (cls.withClause != null) { | |
246 var pos = cls.withClause.end; | |
247 code.edit(pos, pos, ', ChangeNotifier'); | |
248 } else if (cls.extendsClause != null) { | |
249 var pos = cls.extendsClause.end; | |
250 code.edit(pos, pos, ' with ChangeNotifier '); | |
251 } else { | |
252 var params = cls.typeParameters; | |
253 var pos = params != null ? params.end : cls.name.end; | |
254 code.edit(pos, pos, ' extends ChangeNotifier '); | |
255 } | |
256 } | |
257 | |
258 SimpleIdentifier _getSimpleIdentifier(Identifier id) => | |
259 id is PrefixedIdentifier ? id.identifier : id; | |
260 | |
261 | |
262 bool _hasKeyword(Token token, Keyword keyword) => | |
263 token is KeywordToken && token.keyword == keyword; | |
264 | |
265 String _getOriginalCode(TextEditTransaction code, ASTNode node) => | |
266 code.original.substring(node.offset, node.end); | |
267 | |
268 void _fixConstructor(ConstructorDeclaration ctor, TextEditTransaction code, | |
269 Set<String> changedFields) { | |
270 | |
271 // Fix normal initializers | |
272 for (var initializer in ctor.initializers) { | |
273 if (initializer is ConstructorFieldInitializer) { | |
274 var field = initializer.fieldName; | |
275 if (changedFields.contains(field.name)) { | |
276 code.edit(field.offset, field.end, '__\$${field.name}'); | |
277 } | |
278 } | |
279 } | |
280 | |
281 // Fix "this." initializer in parameter list. These are tricky: | |
282 // we need to preserve the name and add an initializer. | |
283 // Preserving the name is important for named args, and for dartdoc. | |
284 // BEFORE: Foo(this.bar, this.baz) { ... } | |
285 // AFTER: Foo(bar, baz) : __$bar = bar, __$baz = baz { ... } | |
286 | |
287 var thisInit = []; | |
288 for (var param in ctor.parameters.parameters) { | |
289 if (param is DefaultFormalParameter) { | |
290 param = param.parameter; | |
291 } | |
292 if (param is FieldFormalParameter) { | |
293 var name = param.identifier.name; | |
294 if (changedFields.contains(name)) { | |
295 thisInit.add(name); | |
296 // Remove "this." but keep everything else. | |
297 code.edit(param.thisToken.offset, param.period.end, ''); | |
298 } | |
299 } | |
300 } | |
301 | |
302 if (thisInit.length == 0) return; | |
303 | |
304 // TODO(jmesserly): smarter formatting with indent, etc. | |
305 var inserted = thisInit.map((i) => '__\$$i = $i').join(', '); | |
306 | |
307 int offset; | |
308 if (ctor.separator != null) { | |
309 offset = ctor.separator.end; | |
310 inserted = ' $inserted,'; | |
311 } else { | |
312 offset = ctor.parameters.end; | |
313 inserted = ' : $inserted'; | |
314 } | |
315 | |
316 code.edit(offset, offset, inserted); | |
317 } | |
318 | |
319 bool _isReadOnly(VariableDeclarationList fields) { | |
320 return _hasKeyword(fields.keyword, Keyword.CONST) || | |
321 _hasKeyword(fields.keyword, Keyword.FINAL); | |
322 } | |
323 | |
324 void _transformFields(SourceFile file, FieldDeclaration member, | |
325 TextEditTransaction code, TransformLogger logger) { | |
326 | |
327 final fields = member.fields; | |
328 if (_isReadOnly(fields)) return; | |
329 | |
330 // Private fields aren't supported: | |
331 for (var field in fields.variables) { | |
332 final name = field.name.name; | |
333 if (Identifier.isPrivateName(name)) { | |
334 logger.warning('Cannot make private field $name observable.', | |
335 span: _getSpan(file, field)); | |
336 return; | |
337 } | |
338 } | |
339 | |
340 // Unfortunately "var" doesn't work in all positions where type annotations | |
341 // are allowed, such as "var get name". So we use "dynamic" instead. | |
342 var type = 'dynamic'; | |
343 if (fields.type != null) { | |
344 type = _getOriginalCode(code, fields.type); | |
345 } else if (_hasKeyword(fields.keyword, Keyword.VAR)) { | |
346 // Replace 'var' with 'dynamic' | |
347 code.edit(fields.keyword.offset, fields.keyword.end, type); | |
348 } | |
349 | |
350 // Note: the replacements here are a bit subtle. It needs to support multiple | |
351 // fields declared via the same @observable, as well as preserving newlines. | |
352 // (Preserving newlines is important because it allows the generated code to | |
353 // be debugged without needing a source map.) | |
354 // | |
355 // For example: | |
356 // | |
357 // @observable | |
358 // @otherMetaData | |
359 // Foo | |
360 // foo = 1, bar = 2, | |
361 // baz; | |
362 // | |
363 // Will be transformed into something like: | |
364 // | |
365 // @reflectable @observable | |
366 // @OtherMetaData() | |
367 // Foo | |
368 // get foo => __foo; Foo __foo = 1; @reflectable set foo ...; ... | |
369 // @observable @OtherMetaData() Foo get baz => __baz; Foo baz; ... | |
370 // | |
371 // Metadata is moved to the getter. | |
372 | |
373 String metadata = ''; | |
374 if (fields.variables.length > 1) { | |
375 metadata = member.metadata | |
376 .map((m) => _getOriginalCode(code, m)) | |
377 .join(' '); | |
378 metadata = '@reflectable $metadata'; | |
379 } | |
380 | |
381 for (int i = 0; i < fields.variables.length; i++) { | |
382 final field = fields.variables[i]; | |
383 final name = field.name.name; | |
384 | |
385 var beforeInit = 'get $name => __\$$name; $type __\$$name'; | |
386 | |
387 // The first field is expanded differently from subsequent fields, because | |
388 // we can reuse the metadata and type annotation. | |
389 if (i == 0) { | |
390 final begin = member.metadata.first.offset; | |
391 code.edit(begin, begin, '@reflectable '); | |
392 } else { | |
393 beforeInit = '$metadata $type $beforeInit'; | |
394 } | |
395 | |
396 code.edit(field.name.offset, field.name.end, beforeInit); | |
397 | |
398 // Replace comma with semicolon | |
399 final end = _findFieldSeperator(field.endToken.next); | |
400 if (end.type == TokenType.COMMA) code.edit(end.offset, end.end, ';'); | |
401 | |
402 code.edit(end.end, end.end, ' @reflectable set $name($type value) { ' | |
403 '__\$$name = notifyPropertyChange(#$name, __\$$name, value); }'); | |
404 } | |
405 } | |
406 | |
407 Token _findFieldSeperator(Token token) { | |
408 while (token != null) { | |
409 if (token.type == TokenType.COMMA || token.type == TokenType.SEMICOLON) { | |
410 break; | |
411 } | |
412 token = token.next; | |
413 } | |
414 return token; | |
415 } | |
OLD | NEW |