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

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

Powered by Google App Engine
This is Rietveld 408576698