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 /** | |
6 * Code transform for @observable. The core transformation is relatively | |
7 * straightforward, and essentially like an editor refactoring. | |
8 */ | |
9 library observe.transform; | |
10 | |
11 import 'dart:async'; | |
12 import 'package:path/path.dart' as path; | |
13 import 'package:analyzer_experimental/src/generated/ast.dart'; | |
14 import 'package:analyzer_experimental/src/generated/error.dart'; | |
15 import 'package:analyzer_experimental/src/generated/parser.dart'; | |
16 import 'package:analyzer_experimental/src/generated/scanner.dart'; | |
17 import 'package:barback/barback.dart'; | |
Jennifer Messerly
2013/08/09 05:44:03
sanity check: barback doesn't depend on "dart:io"
Bob Nystrom
2013/08/09 15:42:38
Yes, barback does. That means this library in obse
Siggi Cherem (dart-lang)
2013/08/09 22:09:09
For now yes, but I think eventually it would be ni
nweiz
2013/08/09 22:21:28
I don't think it makes sense either for barback no
Siggi Cherem (dart-lang)
2013/08/09 22:38:33
I think it would be nice to be able to run transfo
Jennifer Messerly
2013/08/09 22:46:50
+1 siggi
nweiz
2013/08/09 22:53:24
Transformer is a class that wraps code that can do
| |
18 import 'package:codegen/refactor.dart'; | |
19 import 'package:source_maps/span.dart' show SourceFile; | |
20 | |
21 /** | |
22 * A [Transformer] that replaces observables based on dirty-checking with an | |
23 * implementation based on change notifications. | |
24 * | |
25 * The transformation adds hooks for field setters and notifies the observation | |
26 * system of the change. | |
27 */ | |
28 class ObservableTransformer extends Transformer { | |
29 | |
30 Future<bool> isPrimary(Asset input) { | |
31 if (input.id.extension != '.dart') return new Future.value(false); | |
32 // Note: technically we should parse the file to find accurately the | |
Jennifer Messerly
2013/08/09 05:44:03
is there no way to save state between this check a
nweiz
2013/08/09 19:41:26
It's fine if [isPrimary] generates some false posi
Siggi Cherem (dart-lang)
2013/08/09 22:09:09
In terms of caching - I thought about doing that t
nweiz
2013/08/09 22:21:28
I don't think it's likely that we'll merge the two
| |
33 // observable annotation, but that seems expensive. It would require almost | |
34 // as much work as applying the transform. We rather have some false | |
35 // positives here, and then generate no outputs when we apply this | |
36 // transform. | |
37 return input.readAsString().then((c) => c.contains("@observable")); | |
Jennifer Messerly
2013/08/09 05:44:03
a parse should be pretty fast, right? I wonder if
Siggi Cherem (dart-lang)
2013/08/09 22:09:09
Not sure. I'm currently inclined to leave it as is
| |
38 } | |
39 | |
40 Future apply(Transform transform) { | |
41 return transform.primaryInput | |
Jennifer Messerly
2013/08/09 05:44:03
it's a small detail, but it seems goofy that a tra
Bob Nystrom
2013/08/09 15:42:38
We could probably make accessing the primary input
nweiz
2013/08/09 19:41:26
The trouble with making [primaryInput] sync is tha
Siggi Cherem (dart-lang)
2013/08/09 22:09:09
From yesterday's discussion I thought that you gua
nweiz
2013/08/09 22:21:28
We can't guarantee that; the asset can change on d
| |
42 .then((input) => input.readAsString().then((content) { | |
43 var id = transform.primaryId; | |
44 // TODO(sigmund): improve how we compute this url | |
45 var url = id.path.startsWith('lib/') | |
46 ? 'package:${id.package}/${id.path.substring(4)}' : id.path; | |
47 var sourceFile = new SourceFile.text(url, content); | |
48 var transaction = _transformCompilationUnit( | |
49 content, sourceFile, transform.logger); | |
50 if (!transaction.hasEdits) { | |
51 transform.addOutput(input); | |
52 return; | |
53 } | |
54 var printer = transaction.commit(); | |
55 // TODO(sigmund): emit source maps when backback supports it (see | |
Jennifer Messerly
2013/08/09 05:44:03
barback? :)
Siggi Cherem (dart-lang)
2013/08/09 22:09:09
=)
| |
56 // dartbug.com/12340) | |
Jennifer Messerly
2013/08/09 05:44:03
idea: add a dummy "addSourceMap" method so you can
Siggi Cherem (dart-lang)
2013/08/09 22:09:09
I posted a comment about this in the bug, for now
| |
57 printer.build(url); | |
58 transform.addOutput(new Asset.fromString(id, printer.text)); | |
59 })); | |
60 } | |
61 } | |
62 | |
63 TextEditTransaction _transformCompilationUnit( | |
64 String inputCode, SourceFile sourceFile, TransformLogger logger) { | |
65 var unit = _parseCompilationUnit(inputCode); | |
66 return transformObservables(unit, sourceFile, inputCode, logger); | |
67 } | |
68 | |
69 // TODO(sigmund): make this private. This is currently public so it can be used | |
70 // by the polymer.dart package which is not yet entirely migrated to use | |
71 // pub-serve and pub-deploy. | |
72 TextEditTransaction transformObservables(CompilationUnit unit, | |
73 SourceFile sourceFile, String content, TransformLogger logger) { | |
74 var code = new TextEditTransaction(content, sourceFile); | |
75 for (var directive in unit.directives) { | |
76 if (directive is LibraryDirective && _hasObservable(directive)) { | |
77 logger.warning('@observable on a library no longer has any effect. ' | |
78 'It should be placed on individual fields.', | |
79 _getSpan(sourceFile, directive)); | |
80 break; | |
81 } | |
82 } | |
83 | |
84 for (var declaration in unit.declarations) { | |
85 if (declaration is ClassDeclaration) { | |
86 _transformClass(declaration, code, sourceFile, logger); | |
87 } else if (declaration is TopLevelVariableDeclaration) { | |
88 if (_hasObservable(declaration)) { | |
89 logger.warning('Top-level fields can no longer be observable. ' | |
90 'Observable fields should be put in an observable objects.', | |
91 _getSpan(sourceFile, declaration)); | |
92 } | |
93 } | |
94 } | |
95 return code; | |
96 } | |
97 | |
98 /** Parse [code] using analyzer_experimental. */ | |
99 CompilationUnit _parseCompilationUnit(String code) { | |
100 var errorListener = new _ErrorCollector(); | |
101 var scanner = new StringScanner(null, code, errorListener); | |
102 var token = scanner.tokenize(); | |
103 var parser = new Parser(null, errorListener); | |
104 return parser.parseCompilationUnit(token); | |
105 } | |
106 | |
107 class _ErrorCollector extends AnalysisErrorListener { | |
108 final errors = <AnalysisError>[]; | |
109 onError(error) => errors.add(error); | |
110 } | |
111 | |
112 _getSpan(SourceFile file, ASTNode node) => file.span(node.offset, node.end); | |
113 | |
114 /** True if the node has the `@observable` annotation. */ | |
115 bool _hasObservable(AnnotatedNode node) => _hasAnnotation(node, 'observable'); | |
116 | |
117 bool _hasAnnotation(AnnotatedNode node, String name) { | |
118 // TODO(jmesserly): this isn't correct if the annotation has been imported | |
119 // with a prefix, or cases like that. We should technically be resolving, but | |
120 // that is expensive. | |
121 return node.metadata.any((m) => m.name.name == name && | |
122 m.constructorName == null && m.arguments == null); | |
123 } | |
124 | |
125 void _transformClass(ClassDeclaration cls, TextEditTransaction code, | |
126 SourceFile file, TransformLogger logger) { | |
127 | |
128 if (_hasObservable(cls)) { | |
129 logger.warning('@observable on a class no longer has any effect. ' | |
130 'It should be placed on individual fields.', | |
131 _getSpan(file, cls)); | |
132 } | |
133 | |
134 // We'd like to track whether observable was declared explicitly, otherwise | |
135 // report a warning later below. Because we don't have type analysis (only | |
136 // syntactic understanding of the code), we only report warnings that are | |
137 // known to be true. | |
138 var declaresObservable = false; | |
139 if (cls.extendsClause != null) { | |
140 var id = _getSimpleIdentifier(cls.extendsClause.superclass.name); | |
141 if (id.name == 'ObservableBase') { | |
142 code.edit(id.offset, id.end, 'ChangeNotifierBase'); | |
143 declaresObservable = true; | |
144 } else if (id.name == 'ChangeNotifierBase') { | |
145 declaresObservable = true; | |
146 } else if (id.name != 'PolymerElement' && id.name != 'CustomElement' | |
147 && id.name != 'Object') { | |
148 // TODO(sigmund): this is conservative, consider using type-resolution to | |
149 // improve this check. | |
150 declaresObservable = true; | |
151 } | |
152 } | |
153 | |
154 if (cls.withClause != null) { | |
155 for (var type in cls.withClause.mixinTypes) { | |
156 var id = _getSimpleIdentifier(type.name); | |
157 if (id.name == 'ObservableMixin') { | |
158 code.edit(id.offset, id.end, 'ChangeNotifierMixin'); | |
159 declaresObservable = true; | |
160 break; | |
161 } else if (id.name == 'ChangeNotifierMixin') { | |
162 declaresObservable = true; | |
163 break; | |
164 } else { | |
165 // TODO(sigmund): this is conservative, consider using type-resolution | |
166 // to improve this check. | |
167 declaresObservable = true; | |
168 } | |
169 } | |
170 } | |
171 | |
172 if (!declaresObservable && cls.implementsClause != null) { | |
173 // TODO(sigmund): consider adding type-resolution to give a more precise | |
174 // answer. | |
175 declaresObservable = true; | |
176 } | |
177 | |
178 // Track fields that were transformed. | |
179 var instanceFields = new Set<String>(); | |
180 var getters = new List<String>(); | |
181 var setters = new List<String>(); | |
182 | |
183 for (var member in cls.members) { | |
184 if (member is FieldDeclaration) { | |
185 bool isStatic = _hasKeyword(member.keyword, Keyword.STATIC); | |
186 if (isStatic) { | |
187 if (_hasObservable(member)){ | |
188 logger.warning('Static fields can no longer be observable. ' | |
189 'Observable fields should be put in an observable objects.', | |
190 _getSpan(file, member)); | |
191 } | |
192 continue; | |
193 } | |
194 if (_hasObservable(member)) { | |
195 if (!declaresObservable) { | |
196 logger.warning('Observable fields should be put in an observable' | |
197 ' objects. Please declare that this class extends from ' | |
198 'ObservableBase, includes ObservableMixin, or implements ' | |
199 'Observable.', | |
200 _getSpan(file, member)); | |
201 | |
202 } | |
203 _transformFields(member.fields, code, member.offset, member.end); | |
204 | |
205 var names = member.fields.variables.map((v) => v.name.name); | |
206 | |
207 getters.addAll(names); | |
208 if (!_isReadOnly(member.fields)) { | |
209 setters.addAll(names); | |
210 instanceFields.addAll(names); | |
211 } | |
212 } | |
213 } | |
214 // TODO(jmesserly): this is a temporary workaround until we can remove | |
215 // getValueWorkaround and setValueWorkaround. | |
216 if (member is MethodDeclaration) { | |
217 if (_hasKeyword(member.propertyKeyword, Keyword.GET)) { | |
218 getters.add(member.name.name); | |
219 } else if (_hasKeyword(member.propertyKeyword, Keyword.SET)) { | |
220 setters.add(member.name.name); | |
221 } | |
222 } | |
223 } | |
224 | |
225 // If nothing was @observable, bail. | |
226 if (instanceFields.length == 0) return; | |
227 | |
228 // Fix initializers, because they aren't allowed to call the setter. | |
229 for (var member in cls.members) { | |
230 if (member is ConstructorDeclaration) { | |
231 _fixConstructor(member, code, instanceFields); | |
232 } | |
233 } | |
234 } | |
235 | |
236 SimpleIdentifier _getSimpleIdentifier(Identifier id) => | |
237 id is PrefixedIdentifier ? (id as PrefixedIdentifier).identifier : id; | |
238 | |
239 | |
240 bool _hasKeyword(Token token, Keyword keyword) => | |
241 token is KeywordToken && (token as KeywordToken).keyword == keyword; | |
242 | |
243 String _getOriginalCode(TextEditTransaction code, ASTNode node) => | |
244 code.original.substring(node.offset, node.end); | |
245 | |
246 void _fixConstructor(ConstructorDeclaration ctor, TextEditTransaction code, | |
247 Set<String> changedFields) { | |
248 | |
249 // Fix normal initializers | |
250 for (var initializer in ctor.initializers) { | |
251 if (initializer is ConstructorFieldInitializer) { | |
252 var field = initializer.fieldName; | |
253 if (changedFields.contains(field.name)) { | |
254 code.edit(field.offset, field.end, '__\$${field.name}'); | |
255 } | |
256 } | |
257 } | |
258 | |
259 // Fix "this." initializer in parameter list. These are tricky: | |
260 // we need to preserve the name and add an initializer. | |
261 // Preserving the name is important for named args, and for dartdoc. | |
262 // BEFORE: Foo(this.bar, this.baz) { ... } | |
263 // AFTER: Foo(bar, baz) : __$bar = bar, __$baz = baz { ... } | |
264 | |
265 var thisInit = []; | |
266 for (var param in ctor.parameters.parameters) { | |
267 if (param is DefaultFormalParameter) { | |
268 param = param.parameter; | |
269 } | |
270 if (param is FieldFormalParameter) { | |
271 var name = param.identifier.name; | |
272 if (changedFields.contains(name)) { | |
273 thisInit.add(name); | |
274 // Remove "this." but keep everything else. | |
275 code.edit(param.thisToken.offset, param.period.end, ''); | |
276 } | |
277 } | |
278 } | |
279 | |
280 if (thisInit.length == 0) return; | |
281 | |
282 // TODO(jmesserly): smarter formatting with indent, etc. | |
283 var inserted = thisInit.map((i) => '__\$$i = $i').join(', '); | |
284 | |
285 int offset; | |
286 if (ctor.separator != null) { | |
287 offset = ctor.separator.end; | |
288 inserted = ' $inserted,'; | |
289 } else { | |
290 offset = ctor.parameters.end; | |
291 inserted = ' : $inserted'; | |
292 } | |
293 | |
294 code.edit(offset, offset, inserted); | |
295 } | |
296 | |
297 bool _isReadOnly(VariableDeclarationList fields) { | |
298 return _hasKeyword(fields.keyword, Keyword.CONST) || | |
299 _hasKeyword(fields.keyword, Keyword.FINAL); | |
300 } | |
301 | |
302 void _transformFields(VariableDeclarationList fields, TextEditTransaction code, | |
303 int begin, int end) { | |
304 | |
305 if (_isReadOnly(fields)) return; | |
306 | |
307 var indent = guessIndent(code.original, begin); | |
308 var replace = new StringBuffer(); | |
309 | |
310 // Unfortunately "var" doesn't work in all positions where type annotations | |
311 // are allowed, such as "var get name". So we use "dynamic" instead. | |
312 var type = 'dynamic'; | |
313 if (fields.type != null) { | |
314 type = _getOriginalCode(code, fields.type); | |
315 } | |
316 | |
317 for (var field in fields.variables) { | |
318 var initializer = ''; | |
319 if (field.initializer != null) { | |
320 initializer = ' = ${_getOriginalCode(code, field.initializer)}'; | |
321 } | |
322 | |
323 var name = field.name.name; | |
324 | |
325 // TODO(jmesserly): should we generate this one one line, so source maps | |
326 // don't break? | |
327 if (replace.length > 0) replace.write('\n\n$indent'); | |
328 replace.write(''' | |
329 $type __\$$name$initializer; | |
330 $type get $name => __\$$name; | |
331 set $name($type value) { | |
332 __\$$name = notifyPropertyChange(const Symbol('$name'), __\$$name, value); | |
333 } | |
334 '''.replaceAll('\n', '\n$indent')); | |
335 } | |
336 | |
337 code.edit(begin, end, '$replace'); | |
338 } | |
OLD | NEW |