OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2012, 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 'dart_parser.dart'; | |
17 import 'messages.dart'; | |
18 import 'refactor.dart'; | |
19 | |
20 /** | |
21 * Transform types in Dart [userCode] marked with `@observable` by hooking all | |
22 * field setters, and notifying the observation system of the change. If the | |
23 * code was changed this returns true, otherwise returns false. Modified code | |
24 * can be found in [userCode.code]. | |
25 * | |
26 * Note: there is no special checking for transitive immutability. It is up to | |
27 * the rest of the observation system to handle check for this condition and | |
28 * handle it appropriately. We do not want to violate reference equality of | |
29 * any fields that are set into the object. | |
30 */ | |
31 bool transformObservables(DartCodeInfo userCode, Messages messages) { | |
32 if (userCode == null || userCode.compilationUnit == null) { | |
33 return false; | |
Siggi Cherem (dart-lang)
2013/02/13 19:28:54
would this fit in the prev line?
Jennifer Messerly
2013/02/14 00:38:09
Done.
| |
34 } | |
35 var oldCode = userCode.code; | |
36 var transaction = new TextEditTransaction(oldCode); | |
37 transformCompilationUnit(userCode.compilationUnit, transaction); | |
38 | |
39 var newCode = transaction.commit(); | |
40 if (identical(oldCode, newCode)) { | |
41 return false; | |
42 } else { | |
Siggi Cherem (dart-lang)
2013/02/13 19:28:54
nit: remove else...
if (identical(...)) return fa
Jennifer Messerly
2013/02/14 00:38:09
Done.
| |
43 // Replace the code | |
44 userCode.code = newCode; | |
45 return true; | |
46 } | |
47 } | |
48 | |
49 void transformCompilationUnit(CompilationUnit unit, TextEditTransaction code) { | |
50 bool observeAll = unit.directives.any( | |
51 (d) => d is LibraryDirective && hasObservable(d)); | |
52 | |
53 for (var declaration in unit.declarations) { | |
54 if (declaration is ClassDeclaration) { | |
55 transformClass(declaration, code, observeAll); | |
56 } else if (declaration is TopLevelVariableDeclaration) { | |
57 if (observeAll || hasObservable(declaration)) { | |
58 transformTopLevelField(declaration, code); | |
59 } | |
60 } | |
61 } | |
62 } | |
63 | |
64 /** True if the code has the `@observable` annotation. */ | |
65 bool hasObservable(AnnotatedNode node) { | |
66 // TODO(jmesserly): this isn't correct if observable has been imported | |
67 // with a prefix, or cases like that. We should technically be resolving, but | |
68 // that is expensive. | |
69 return node.metadata.any((m) => m.name.name == 'observable' && | |
70 m.constructorName == null && m.arguments == null); | |
71 } | |
72 | |
73 void transformClass(ClassDeclaration cls, TextEditTransaction code, | |
74 bool observeAll) { | |
75 | |
76 observeAll = observeAll || hasObservable(cls); | |
77 | |
78 var changedFields = new Set<String>(); | |
79 for (var member in cls.members) { | |
80 if (member is FieldDeclaration) { | |
81 if (observeAll || hasObservable(member)) { | |
82 transformClassFields(member, code, changedFields); | |
83 } | |
84 } | |
85 } | |
86 | |
87 if (changedFields.length == 0) return; | |
88 | |
89 // Fix initializers, because they aren't allowed to call the setter. | |
90 for (var member in cls.members) { | |
91 if (member is ConstructorDeclaration) { | |
92 fixConstructor(member, code, changedFields); | |
93 } | |
94 } | |
95 } | |
96 | |
97 bool hasKeyword(Token token, Keyword keyword) => | |
98 token is KeywordToken && token.keyword == keyword; | |
99 | |
100 String getOriginalCode(TextEditTransaction code, ASTNode node) => | |
101 code.original.substring(node.offset, node.end); | |
102 | |
103 void transformTopLevelField(TopLevelVariableDeclaration field, | |
104 TextEditTransaction code) { | |
105 transformFields(field.variables, code, field.offset, field.end); | |
106 } | |
107 | |
108 void transformClassFields(FieldDeclaration member, TextEditTransaction code, | |
109 Set<String> changedFields) { | |
110 | |
111 transformFields(member.fields, code, member.offset, member.end, | |
112 isStatic: hasKeyword(member.keyword, Keyword.STATIC), | |
113 changedFields: changedFields); | |
114 } | |
115 | |
116 | |
117 void fixConstructor(ConstructorDeclaration ctor, TextEditTransaction code, | |
118 Set<String> changedFields) { | |
119 | |
120 // Fix normal initializers | |
121 for (var initializer in ctor.initializers) { | |
122 if (initializer is ConstructorFieldInitializer) { | |
123 var field = initializer.fieldName; | |
124 if (changedFields.contains(field.name)) { | |
125 code.edit(field.offset, field.end, '__\$${field.name}'); | |
126 } | |
127 } | |
128 } | |
129 | |
130 // Fix "this." initializer in parameter list. These are tricky: | |
131 // we need to preserve the name and add an initializer. | |
132 // BEFORE: Foo(this.bar, this.baz) { ... } | |
133 // AFTER: Foo(bar, baz) : __$bar = bar, __$baz = baz { ... } | |
Siggi Cherem (dart-lang)
2013/02/13 19:28:54
why can't AFTER be:
Foo(this.__$bar, this.__$baz)
Jennifer Messerly
2013/02/13 20:21:20
It's mainly to handle named arguments. However par
| |
134 | |
135 var thisInit = []; | |
136 for (var param in ctor.parameters.parameters) { | |
137 if (param is FieldFormalParameter) { | |
138 var name = param.identifier.name; | |
139 if (changedFields.contains(name)) { | |
140 thisInit.add(name); | |
141 // Remove "this." but keep everything else. | |
142 code.edit(param.thisToken.offset, param.period.end, ''); | |
143 } | |
144 } | |
145 } | |
146 | |
147 if (thisInit.length == 0) return; | |
148 | |
149 // TODO(jmesserly): smarter formatting with indent, etc. | |
150 var inserted = thisInit.map((i) => '__\$$i = $i').join(', '); | |
151 | |
152 int offset; | |
153 if (ctor.separator != null) { | |
154 offset = ctor.separator.end; | |
155 inserted = ' $inserted,'; | |
156 } else { | |
157 offset = ctor.parameters.end; | |
158 inserted = ' : $inserted'; | |
159 } | |
160 | |
161 code.edit(offset, offset, inserted); | |
162 } | |
163 | |
164 void transformFields(VariableDeclarationList fields, TextEditTransaction code, | |
165 int begin, int end, {bool isStatic: false, Set<String> changedFields}) { | |
166 | |
167 if (hasKeyword(fields.keyword, Keyword.CONST) || | |
168 hasKeyword(fields.keyword, Keyword.FINAL)) { | |
169 return; | |
170 } | |
171 | |
172 var indent = guessIndent(code.original, begin); | |
173 var replace = new StringBuffer(); | |
174 | |
175 var type = 'dynamic'; | |
Siggi Cherem (dart-lang)
2013/02/13 19:28:54
do you prefer 'dynamic' over 'var'?
Jennifer Messerly
2013/02/13 20:21:20
I had to change it because "var get fieldName" doe
Siggi Cherem (dart-lang)
2013/02/13 20:41:13
makes sense, thanks
Jennifer Messerly
2013/02/14 00:38:09
Done.
| |
176 if (fields.type != null) { | |
177 type = getOriginalCode(code, fields.type); | |
178 } | |
Siggi Cherem (dart-lang)
2013/02/13 19:28:54
totally optional nit, consider using ternary expre
Jennifer Messerly
2013/02/13 20:21:20
Here's my thinking: I like ternary expressions. Bu
Siggi Cherem (dart-lang)
2013/02/13 20:41:13
np - I figured it had something to do with long li
Jennifer Messerly
2013/02/14 00:38:09
gotcha :)
| |
179 | |
180 var mod = isStatic ? 'static ' : ''; | |
181 | |
182 for (var field in fields.variables) { | |
183 var initializer = ''; | |
184 if (field.initializer != null) { | |
185 initializer = ' = ${getOriginalCode(code, field.initializer)}'; | |
186 } | |
187 | |
188 var name = field.name.name; | |
189 | |
190 if (replace.length > 0) replace.add('\n\n$indent'); | |
191 replace.add(''' | |
192 ${mod}$type __\$$name$initializer; | |
193 ${mod}Object __obs\$$name; | |
194 ${mod}$type get $name { | |
195 if (autogenerated.observeReads) { | |
196 __obs\$$name = autogenerated.notifyRead(__obs\$$name); | |
197 } | |
198 return __\$$name; | |
199 } | |
200 ${mod}set $name($type value) { | |
201 if (__obs\$$name != null && __\$$name != value) { | |
202 __obs\$$name = autogenerated.notifyWrite(__obs\$$name); | |
203 } | |
204 __\$$name = value; | |
205 }'''.replaceAll('\n', '\n$indent')); | |
206 | |
207 if (changedFields != null) changedFields.add(name); | |
208 } | |
209 | |
210 code.edit(begin, end, '$replace'); | |
211 } | |
OLD | NEW |