Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(394)

Side by Side Diff: lib/src/observable_transform.dart

Issue 22962005: Merge pull request #581 from kevmoo/polymer (Closed) Base URL: https://github.com/dart-lang/web-ui.git@polymer
Patch Set: Created 7 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « lib/src/messages.dart ('k') | lib/src/refactor.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « lib/src/messages.dart ('k') | lib/src/refactor.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698