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

Side by Side Diff: pkg/observe/lib/transform.dart

Issue 63173009: Ensure we run the observable transform on PolymerElement (fixes issue 14942) (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 1 month 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 | Annotate | Revision Log
« no previous file with comments | « no previous file | pkg/observe/lib/transformer.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 }
OLDNEW
« no previous file with comments | « no previous file | pkg/observe/lib/transformer.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698