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

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

Issue 22396004: Make observable transform a barback transform. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: changes alone (patchset 1 has the existing code, in the new location) 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 | Annotate | Revision Log
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.
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';
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
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.
Bob Nystrom 2013/08/08 22:47:09 You could always do this fast check first and then
Siggi Cherem (dart-lang) 2013/08/08 23:14:02 true, but I feel is ok in that case to do the work
nweiz 2013/08/08 23:34:57 There's no harm in running the transform on a file
Siggi Cherem (dart-lang) 2013/08/09 00:29:54 correct. Or in my case not spit anything =)
37 return input.readAsString().then((c) => c.contains("@observable"));
38 }
39
40 Future apply(Transform transform) {
41 return transform.primaryInput
42 .then((input) => input.readAsString()).then((content) {
43 var id = transform.primaryId;
44 var sourceFile = new SourceFile.text(id.toString(), content);
nweiz 2013/08/08 23:34:57 Right now, [id.toString] will return a string of t
Siggi Cherem (dart-lang) 2013/08/09 00:29:54 I see. since the path doesn't have the package con
nweiz 2013/08/09 00:53:48 Not in general. Intermediate assets may have no on
45 var transaction = _transformCompilationUnit(
46 content, sourceFile, transform.log);
47 if (transaction.hasEdits) {
Bob Nystrom 2013/08/08 22:47:09 I would do: if (!transaction.hasEdits) return; T
Siggi Cherem (dart-lang) 2013/08/08 23:10:41 Done. Yeah, I like doing that too.
nweiz 2013/08/08 23:34:57 If this returns without any outputs, that's not th
Siggi Cherem (dart-lang) 2013/08/09 00:29:54 I'm not sure I see what's the difference, so I'll
Siggi Cherem (dart-lang) 2013/08/09 00:40:24 Fixed. So it turns out that if we dont emit it, it
48 var printer = transaction.commit();
49 var filename = path.basename(id.path);
50 printer.add('\n//# sourceMappingURL=$filename.map');
51 printer.build(id.path);
52 transform.addOutput(new Asset.fromString(id, printer.text));
53 transform.addOutput(
54 new Asset.fromString(id.addExtension('.map'), printer.map));
nweiz 2013/08/08 23:34:57 This sourcemap is not going to consistently point
Siggi Cherem (dart-lang) 2013/08/09 00:29:54 Oh my! those are good points. I don't know yet wha
nweiz 2013/08/09 00:53:48 I'd just get rid of the sourcemaps here for now un
Siggi Cherem (dart-lang) 2013/08/09 01:17:30 ok, no prob. I removed it for now. Our current use
55 }
56 });
Bob Nystrom 2013/08/08 22:47:09 Nice!
Siggi Cherem (dart-lang) 2013/08/08 23:10:41 :)
57 }
58 }
59
60 void _transformCompilationUnit(
Bob Nystrom 2013/08/08 22:47:09 Should this be void?
Siggi Cherem (dart-lang) 2013/08/08 23:10:41 oops, good catch, thanks.
61 String inputCode, SourceFile sourceFile, Log log) {
62 var unit = _parseCompilationUnit(inputCode);
63 return transformObservables(unit, sourceFile, inputCode, log);
64 }
65
66 // TODO(sigmund): make this private. This is currently public so it can be used
67 // by the polymer.dart package which is not yet entirely migrated to use
68 // pub-serve and pub-deploy.
69 TextEditTransaction transformObservables(CompilationUnit unit,
70 SourceFile sourceFile, String content, Log log) {
71 var code = new TextEditTransaction(content, sourceFile);
72 for (var directive in unit.directives) {
73 if (directive is LibraryDirective && _hasObservable(directive)) {
74 log.warning('@observable on a library no longer has any effect. '
75 'It should be placed on individual fields.',
76 _getSpan(sourceFile, directive));
77 break;
78 }
79 }
80
81 for (var declaration in unit.declarations) {
82 if (declaration is ClassDeclaration) {
83 _transformClass(declaration, code, sourceFile, log);
84 } else if (declaration is TopLevelVariableDeclaration) {
85 if (_hasObservable(declaration)) {
86 log.warning('Top-level fields can no longer be observable. '
87 'Observable fields should be put in an observable objects.',
88 _getSpan(sourceFile, declaration));
89 }
90 }
91 }
92 return code;
93 }
94
95 /** Parse [code] using analyzer_experimental. */
96 CompilationUnit _parseCompilationUnit(String code) {
97 var errorListener = new _ErrorCollector();
98 var scanner = new StringScanner(null, code, errorListener);
99 var token = scanner.tokenize();
100 var parser = new Parser(null, errorListener);
101 return parser.parseCompilationUnit(token);
102 }
103
104 class _ErrorCollector extends AnalysisErrorListener {
105 final errors = new List<AnalysisError>();
Bob Nystrom 2013/08/08 22:47:09 final errors = <AnalysisError>[];
Siggi Cherem (dart-lang) 2013/08/08 23:10:41 Done.
106 onError(error) => errors.add(error);
107 }
108
109 _getSpan(SourceFile file, ASTNode node) => file.span(node.offset, node.end);
110
111 /** True if the node has the `@observable` annotation. */
112 bool _hasObservable(AnnotatedNode node) => _hasAnnotation(node, 'observable');
113
114 bool _hasAnnotation(AnnotatedNode node, String name) {
115 // TODO(jmesserly): this isn't correct if the annotation has been imported
116 // with a prefix, or cases like that. We should technically be resolving, but
117 // that is expensive.
118 return node.metadata.any((m) => m.name.name == name &&
119 m.constructorName == null && m.arguments == null);
120 }
121
122 void _transformClass(ClassDeclaration cls, TextEditTransaction code,
123 SourceFile file, Log log) {
124
125 if (_hasObservable(cls)) {
126 log.warning('@observable on a class no longer has any effect. '
127 'It should be placed on individual fields.',
128 _getSpan(file, cls));
129 }
130
131 // We'd like to track whether observable was declared explicitly, otherwise
132 // report a warning later below. Because we don't have type analysis (only
133 // syntactic understanding of the code), we only report warnings that are
134 // known to be true.
135 var declaresObservable = false;
136 if (cls.extendsClause != null) {
137 var id = _getSimpleIdentifier(cls.extendsClause.superclass.name);
138 if (id.name == 'ObservableBase') {
139 code.edit(id.offset, id.end, 'ChangeNotifierBase');
140 declaresObservable = true;
141 } else if (id.name == 'ChangeNotifierBase') {
142 declaresObservable = true;
143 } else if (id.name != 'PolymerElement' && id.name != 'CustomElement'
144 && id.name != 'Object') {
145 // TODO(sigmund): this is conservative, consider using type-resolution to
146 // improve this check.
147 declaresObservable = true;
148 }
149 }
150
151 if (cls.withClause != null) {
152 for (var type in cls.withClause.mixinTypes) {
153 var id = _getSimpleIdentifier(type.name);
154 if (id.name == 'ObservableMixin') {
155 code.edit(id.offset, id.end, 'ChangeNotifierMixin');
156 declaresObservable = true;
157 break;
158 } else if (id.name == 'ChangeNotifierMixin') {
159 declaresObservable = true;
160 break;
161 } else {
162 // TODO(sigmund): this is conservative, consider using type-resolution
163 // to improve this check.
164 declaresObservable = true;
165 }
166 }
167 }
168
169 if (!declaresObservable && cls.implementsClause != null) {
170 // TODO(sigmund): consider adding type-resolution to give a more precise
171 // answer.
172 declaresObservable = true;
173 }
174
175 // Track fields that were transformed.
176 var instanceFields = new Set<String>();
177 var getters = new List<String>();
178 var setters = new List<String>();
179
180 for (var member in cls.members) {
181 if (member is FieldDeclaration) {
182 bool isStatic = _hasKeyword(member.keyword, Keyword.STATIC);
183 if (isStatic) {
184 if (_hasObservable(member)){
185 log.warning('Static fields can no longer be observable. '
186 'Observable fields should be put in an observable objects.',
187 _getSpan(file, member));
188 }
189 continue;
190 }
191 if (_hasObservable(member)) {
192 if (!declaresObservable) {
193 log.warning('Observable fields should be put in an observable'
194 ' objects. Please declare that this class extends from '
195 'ObservableBase, includes ObservableMixin, or implements '
196 'Observable.',
197 _getSpan(file, member));
198
199 }
200 _transformFields(member.fields, code, member.offset, member.end);
201
202 var names = member.fields.variables.map((v) => v.name.name);
203
204 getters.addAll(names);
205 if (!_isReadOnly(member.fields)) {
206 setters.addAll(names);
207 instanceFields.addAll(names);
208 }
209 }
210 }
211 // TODO(jmesserly): this is a temporary workaround until we can remove
212 // getValueWorkaround and setValueWorkaround.
213 if (member is MethodDeclaration) {
214 if (_hasKeyword(member.propertyKeyword, Keyword.GET)) {
215 getters.add(member.name.name);
216 } else if (_hasKeyword(member.propertyKeyword, Keyword.SET)) {
217 setters.add(member.name.name);
218 }
219 }
220 }
221
222 // If nothing was @observable, bail.
223 if (instanceFields.length == 0) return;
224
225 // Fix initializers, because they aren't allowed to call the setter.
226 for (var member in cls.members) {
227 if (member is ConstructorDeclaration) {
228 _fixConstructor(member, code, instanceFields);
229 }
230 }
231 }
232
233 SimpleIdentifier _getSimpleIdentifier(Identifier id) =>
234 id is PrefixedIdentifier ? (id as PrefixedIdentifier).identifier : id;
235
236
237 bool _hasKeyword(Token token, Keyword keyword) =>
238 token is KeywordToken && (token as KeywordToken).keyword == keyword;
239
240 String _getOriginalCode(TextEditTransaction code, ASTNode node) =>
241 code.original.substring(node.offset, node.end);
242
243 void _fixConstructor(ConstructorDeclaration ctor, TextEditTransaction code,
244 Set<String> changedFields) {
245
246 // Fix normal initializers
247 for (var initializer in ctor.initializers) {
248 if (initializer is ConstructorFieldInitializer) {
249 var field = initializer.fieldName;
250 if (changedFields.contains(field.name)) {
251 code.edit(field.offset, field.end, '__\$${field.name}');
252 }
253 }
254 }
255
256 // Fix "this." initializer in parameter list. These are tricky:
257 // we need to preserve the name and add an initializer.
258 // Preserving the name is important for named args, and for dartdoc.
259 // BEFORE: Foo(this.bar, this.baz) { ... }
260 // AFTER: Foo(bar, baz) : __$bar = bar, __$baz = baz { ... }
261
262 var thisInit = [];
263 for (var param in ctor.parameters.parameters) {
264 if (param is DefaultFormalParameter) {
265 param = param.parameter;
266 }
267 if (param is FieldFormalParameter) {
268 var name = param.identifier.name;
269 if (changedFields.contains(name)) {
270 thisInit.add(name);
271 // Remove "this." but keep everything else.
272 code.edit(param.thisToken.offset, param.period.end, '');
273 }
274 }
275 }
276
277 if (thisInit.length == 0) return;
278
279 // TODO(jmesserly): smarter formatting with indent, etc.
280 var inserted = thisInit.map((i) => '__\$$i = $i').join(', ');
281
282 int offset;
283 if (ctor.separator != null) {
284 offset = ctor.separator.end;
285 inserted = ' $inserted,';
286 } else {
287 offset = ctor.parameters.end;
288 inserted = ' : $inserted';
289 }
290
291 code.edit(offset, offset, inserted);
292 }
293
294 bool _isReadOnly(VariableDeclarationList fields) {
295 return _hasKeyword(fields.keyword, Keyword.CONST) ||
296 _hasKeyword(fields.keyword, Keyword.FINAL);
297 }
298
299 void _transformFields(VariableDeclarationList fields, TextEditTransaction code,
300 int begin, int end) {
301
302 if (_isReadOnly(fields)) return;
303
304 var indent = guessIndent(code.original, begin);
305 var replace = new StringBuffer();
306
307 // Unfortunately "var" doesn't work in all positions where type annotations
308 // are allowed, such as "var get name". So we use "dynamic" instead.
309 var type = 'dynamic';
310 if (fields.type != null) {
311 type = _getOriginalCode(code, fields.type);
312 }
313
314 for (var field in fields.variables) {
315 var initializer = '';
316 if (field.initializer != null) {
317 initializer = ' = ${_getOriginalCode(code, field.initializer)}';
318 }
319
320 var name = field.name.name;
321
322 // TODO(jmesserly): should we generate this one one line, so source maps
323 // don't break?
324 if (replace.length > 0) replace.write('\n\n$indent');
325 replace.write('''
326 $type __\$$name$initializer;
327 $type get $name => __\$$name;
328 set $name($type value) {
329 __\$$name = notifyPropertyChange(const Symbol('$name'), __\$$name, value);
330 }
331 '''.replaceAll('\n', '\n$indent'));
332 }
333
334 code.edit(begin, end, '$replace');
335 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698