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. You can find the | |
8 * core implementation in [transformClass], which is ultimately called by | |
9 * [transformObservables], the entry point to this library. | |
10 */ | |
11 library observable_transform; | |
12 | |
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/scanner.dart'; | |
16 import 'package:source_maps/span.dart' show SourceFile; | |
17 import 'dart_parser.dart'; | |
18 import 'messages.dart'; | |
19 import 'refactor.dart'; | |
20 | |
21 /** | |
22 * Transform types in Dart [userCode] marked with `@observable` by hooking all | |
23 * field setters, and notifying the observation system of the change. If the | |
24 * code was changed this returns true, otherwise returns false. Modified code | |
25 * can be found in [userCode.code]. | |
26 * | |
27 * Note: there is no special checking for transitive immutability. It is up to | |
28 * the rest of the observation system to handle check for this condition and | |
29 * handle it appropriately. We do not want to violate reference equality of | |
30 * any fields that are set into the object. | |
31 */ | |
32 TextEditTransaction transformObservables(DartCodeInfo userCode, | |
33 Messages messages) { | |
34 | |
35 if (userCode == null || userCode.compilationUnit == null) return null; | |
36 var transaction = new TextEditTransaction(userCode.code, userCode.sourceFile); | |
37 transformCompilationUnit(userCode, transaction, messages); | |
38 return transaction; | |
39 } | |
40 | |
41 void transformCompilationUnit(DartCodeInfo userCode, TextEditTransaction code, | |
42 Messages messages) { | |
43 | |
44 var unit = userCode.compilationUnit; | |
45 for (var directive in unit.directives) { | |
46 if (directive is LibraryDirective && hasObservable(directive)) { | |
47 messages.warning('@observable on a library no longer has any effect. ' | |
48 'It should be placed on individual fields.', | |
49 _getSpan(userCode.sourceFile, directive)); | |
50 break; | |
51 } | |
52 } | |
53 | |
54 for (var declaration in unit.declarations) { | |
55 if (declaration is ClassDeclaration) { | |
56 transformClass(declaration, code, userCode.sourceFile, messages); | |
57 } else if (declaration is TopLevelVariableDeclaration) { | |
58 if (hasObservable(declaration)) { | |
59 messages.warning('Top-level fields can no longer be observable. ' | |
60 'Observable fields should be put in an observable objects.', | |
61 _getSpan(userCode.sourceFile, declaration)); | |
62 } | |
63 } | |
64 } | |
65 } | |
66 | |
67 _getSpan(SourceFile file, ASTNode node) => file.span(node.offset, node.end); | |
68 | |
69 /** True if the node has the `@observable` annotation. */ | |
70 bool hasObservable(AnnotatedNode node) => hasAnnotation(node, 'observable'); | |
71 | |
72 bool hasAnnotation(AnnotatedNode node, String name) { | |
73 // TODO(jmesserly): this isn't correct if the annotation has been imported | |
74 // with a prefix, or cases like that. We should technically be resolving, but | |
75 // that is expensive. | |
76 return node.metadata.any((m) => m.name.name == name && | |
77 m.constructorName == null && m.arguments == null); | |
78 } | |
79 | |
80 void transformClass(ClassDeclaration cls, TextEditTransaction code, | |
81 SourceFile file, Messages messages) { | |
82 | |
83 if (hasObservable(cls)) { | |
84 messages.warning('@observable on a class no longer has any effect. ' | |
85 'It should be placed on individual fields.', | |
86 _getSpan(file, cls)); | |
87 } | |
88 | |
89 // We'd like to track whether observable was declared explicitly, otherwise | |
90 // report a warning later below. Because we don't have type analysis (only | |
91 // syntactic understanding of the code), we only report warnings that are | |
92 // known to be true. | |
93 var declaresObservable = false; | |
94 if (cls.extendsClause != null) { | |
95 var id = _getSimpleIdentifier(cls.extendsClause.superclass.name); | |
96 if (id.name == 'ObservableBase') { | |
97 code.edit(id.offset, id.end, 'ChangeNotifierBase'); | |
98 declaresObservable = true; | |
99 } else if (id.name == 'ChangeNotifierBase') { | |
100 declaresObservable = true; | |
101 } else if (id.name != 'PolymerElement' && id.name != 'CustomElement' | |
102 && id.name != 'Object') { | |
103 // TODO(sigmund): this is conservative, consider using type-resolution to | |
104 // improve this check. | |
105 declaresObservable = true; | |
106 } | |
107 } | |
108 | |
109 if (cls.withClause != null) { | |
110 for (var type in cls.withClause.mixinTypes) { | |
111 var id = _getSimpleIdentifier(type.name); | |
112 if (id.name == 'ObservableMixin') { | |
113 code.edit(id.offset, id.end, 'ChangeNotifierMixin'); | |
114 declaresObservable = true; | |
115 break; | |
116 } else if (id.name == 'ChangeNotifierMixin') { | |
117 declaresObservable = true; | |
118 break; | |
119 } else { | |
120 // TODO(sigmund): this is conservative, consider using type-resolution | |
121 // to improve this check. | |
122 declaresObservable = true; | |
123 } | |
124 } | |
125 } | |
126 | |
127 if (!declaresObservable && cls.implementsClause != null) { | |
128 // TODO(sigmund): consider adding type-resolution to give a more precise | |
129 // answer. | |
130 declaresObservable = true; | |
131 } | |
132 | |
133 // Track fields that were transformed. | |
134 var instanceFields = new Set<String>(); | |
135 var getters = new List<String>(); | |
136 var setters = new List<String>(); | |
137 | |
138 for (var member in cls.members) { | |
139 if (member is FieldDeclaration) { | |
140 bool isStatic = hasKeyword(member.keyword, Keyword.STATIC); | |
141 if (isStatic) { | |
142 if (hasObservable(member)){ | |
143 messages.warning('Static fields can no longer be observable. ' | |
144 'Observable fields should be put in an observable objects.', | |
145 _getSpan(file, member)); | |
146 } | |
147 continue; | |
148 } | |
149 if (hasObservable(member)) { | |
150 if (!declaresObservable) { | |
151 messages.warning('Observable fields should be put in an observable' | |
152 ' objects. Please declare that this class extends from ' | |
153 'ObservableBase, includes ObservableMixin, or implements ' | |
154 'Observable.', | |
155 _getSpan(file, member)); | |
156 | |
157 } | |
158 transformFields(member.fields, code, member.offset, member.end); | |
159 | |
160 var names = member.fields.variables.map((v) => v.name.name); | |
161 | |
162 getters.addAll(names); | |
163 if (!_isReadOnly(member.fields)) { | |
164 setters.addAll(names); | |
165 instanceFields.addAll(names); | |
166 } | |
167 } | |
168 } | |
169 // TODO(jmesserly): this is a temporary workaround until we can remove | |
170 // getValueWorkaround and setValueWorkaround. | |
171 if (member is MethodDeclaration) { | |
172 if (hasKeyword(member.propertyKeyword, Keyword.GET)) { | |
173 getters.add(member.name.name); | |
174 } else if (hasKeyword(member.propertyKeyword, Keyword.SET)) { | |
175 setters.add(member.name.name); | |
176 } | |
177 } | |
178 } | |
179 | |
180 // If nothing was @observable, bail. | |
181 if (instanceFields.length == 0) return; | |
182 | |
183 if (getters.length > 0 || setters.length > 0) { | |
184 mirrorWorkaround(cls, code, getters, setters); | |
185 } | |
186 | |
187 // Fix initializers, because they aren't allowed to call the setter. | |
188 for (var member in cls.members) { | |
189 if (member is ConstructorDeclaration) { | |
190 fixConstructor(member, code, instanceFields); | |
191 } | |
192 } | |
193 } | |
194 | |
195 SimpleIdentifier _getSimpleIdentifier(Identifier id) => | |
196 id is PrefixedIdentifier ? id.identifier : id; | |
197 | |
198 | |
199 /** | |
200 * Generates `getValueWorkaround` and `setValueWorkaround`. These will go away | |
201 * shortly once dart2js supports mirrors. For the moment they provide something | |
202 * that the binding system can use. | |
203 */ | |
204 void mirrorWorkaround(ClassDeclaration cls, TextEditTransaction code, | |
205 List<String> getters, List<String> setters) { | |
206 | |
207 var sb = new StringBuffer('\ngetValueWorkaround(key) {\n'); | |
208 for (var name in getters) { | |
209 if (name.startsWith('_')) continue; | |
210 sb.write(" if (key == const Symbol('$name')) return this.$name;\n"); | |
211 } | |
212 sb.write(' return null;\n}\n'); | |
213 sb.write('\nsetValueWorkaround(key, value) {\n'); | |
214 for (var name in setters) { | |
215 if (name.startsWith('_')) continue; | |
216 sb.write(" if (key == const Symbol('$name')) " | |
217 "{ this.$name = value; return; }\n"); | |
218 } | |
219 sb.write('}\n'); | |
220 | |
221 int pos = cls.rightBracket.offset; | |
222 var indent = guessIndent(code.original, pos); | |
223 | |
224 code.edit(pos, pos, sb.toString().replaceAll('\n', '\n$indent ')); | |
225 } | |
226 | |
227 bool hasKeyword(Token token, Keyword keyword) => | |
228 token is KeywordToken && (token as KeywordToken).keyword == keyword; | |
229 | |
230 String getOriginalCode(TextEditTransaction code, ASTNode node) => | |
231 code.original.substring(node.offset, node.end); | |
232 | |
233 void fixConstructor(ConstructorDeclaration ctor, TextEditTransaction code, | |
234 Set<String> changedFields) { | |
235 | |
236 // Fix normal initializers | |
237 for (var initializer in ctor.initializers) { | |
238 if (initializer is ConstructorFieldInitializer) { | |
239 var field = initializer.fieldName; | |
240 if (changedFields.contains(field.name)) { | |
241 code.edit(field.offset, field.end, '__\$${field.name}'); | |
242 } | |
243 } | |
244 } | |
245 | |
246 // Fix "this." initializer in parameter list. These are tricky: | |
247 // we need to preserve the name and add an initializer. | |
248 // Preserving the name is important for named args, and for dartdoc. | |
249 // BEFORE: Foo(this.bar, this.baz) { ... } | |
250 // AFTER: Foo(bar, baz) : __$bar = bar, __$baz = baz { ... } | |
251 | |
252 var thisInit = []; | |
253 for (var param in ctor.parameters.parameters) { | |
254 if (param is DefaultFormalParameter) { | |
255 param = param.parameter; | |
256 } | |
257 if (param is FieldFormalParameter) { | |
258 var name = param.identifier.name; | |
259 if (changedFields.contains(name)) { | |
260 thisInit.add(name); | |
261 // Remove "this." but keep everything else. | |
262 code.edit(param.thisToken.offset, param.period.end, ''); | |
263 } | |
264 } | |
265 } | |
266 | |
267 if (thisInit.length == 0) return; | |
268 | |
269 // TODO(jmesserly): smarter formatting with indent, etc. | |
270 var inserted = thisInit.map((i) => '__\$$i = $i').join(', '); | |
271 | |
272 int offset; | |
273 if (ctor.separator != null) { | |
274 offset = ctor.separator.end; | |
275 inserted = ' $inserted,'; | |
276 } else { | |
277 offset = ctor.parameters.end; | |
278 inserted = ' : $inserted'; | |
279 } | |
280 | |
281 code.edit(offset, offset, inserted); | |
282 } | |
283 | |
284 bool _isReadOnly(VariableDeclarationList fields) { | |
285 return hasKeyword(fields.keyword, Keyword.CONST) || | |
286 hasKeyword(fields.keyword, Keyword.FINAL); | |
287 } | |
288 | |
289 void transformFields(VariableDeclarationList fields, TextEditTransaction code, | |
290 int begin, int end) { | |
291 | |
292 if (_isReadOnly(fields)) return; | |
293 | |
294 var indent = guessIndent(code.original, begin); | |
295 var replace = new StringBuffer(); | |
296 | |
297 // Unfortunately "var" doesn't work in all positions where type annotations | |
298 // are allowed, such as "var get name". So we use "dynamic" instead. | |
299 var type = 'dynamic'; | |
300 if (fields.type != null) { | |
301 type = getOriginalCode(code, fields.type); | |
302 } | |
303 | |
304 for (var field in fields.variables) { | |
305 var initializer = ''; | |
306 if (field.initializer != null) { | |
307 initializer = ' = ${getOriginalCode(code, field.initializer)}'; | |
308 } | |
309 | |
310 var name = field.name.name; | |
311 | |
312 // TODO(jmesserly): should we generate this one one line, so source maps | |
313 // don't break? | |
314 if (replace.length > 0) replace.write('\n\n$indent'); | |
315 replace.write(''' | |
316 $type __\$$name$initializer; | |
317 $type get $name => __\$$name; | |
318 set $name($type value) { | |
319 __\$$name = notifyPropertyChange(const Symbol('$name'), __\$$name, value); | |
320 } | |
321 '''.replaceAll('\n', '\n$indent')); | |
322 } | |
323 | |
324 code.edit(begin, end, '$replace'); | |
325 } | |
OLD | NEW |