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

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

Issue 817483003: delete observe from the repo (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years 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/observe/lib/src/to_observable.dart ('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 /// Code transform for @observable. The core transformation is relatively
6 /// straightforward, and essentially like an editor refactoring.
7 library observe.transformer;
8
9 import 'dart:async';
10
11 import 'package:analyzer/analyzer.dart';
12 import 'package:analyzer/src/generated/ast.dart';
13 import 'package:analyzer/src/generated/error.dart';
14 import 'package:analyzer/src/generated/parser.dart';
15 import 'package:analyzer/src/generated/scanner.dart';
16 import 'package:barback/barback.dart';
17 import 'package:code_transformers/messages/build_logger.dart';
18 import 'package:source_maps/refactor.dart';
19 import 'package:source_span/source_span.dart';
20
21 import 'src/messages.dart';
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 class ObservableTransformer extends Transformer {
29
30 final bool releaseMode;
31 final List<String> _files;
32 ObservableTransformer([List<String> files, bool releaseMode])
33 : _files = files, releaseMode = releaseMode == true;
34 ObservableTransformer.asPlugin(BarbackSettings settings)
35 : _files = _readFiles(settings.configuration['files']),
36 releaseMode = settings.mode == BarbackMode.RELEASE;
37
38 static List<String> _readFiles(value) {
39 if (value == null) return null;
40 var files = [];
41 bool error;
42 if (value is List) {
43 files = value;
44 error = value.any((e) => e is! String);
45 } else if (value is String) {
46 files = [value];
47 error = false;
48 } else {
49 error = true;
50 }
51 if (error) print('Invalid value for "files" in the observe transformer.');
52 return files;
53 }
54
55 // TODO(nweiz): This should just take an AssetId when barback <0.13.0 support
56 // is dropped.
57 Future<bool> isPrimary(idOrAsset) {
58 var id = idOrAsset is AssetId ? idOrAsset : idOrAsset.id;
59 return new Future.value(id.extension == '.dart' &&
60 (_files == null || _files.contains(id.path)));
61 }
62
63 Future apply(Transform transform) {
64 return transform.primaryInput.readAsString().then((content) {
65 // Do a quick string check to determine if this is this file even
66 // plausibly might need to be transformed. If not, we can avoid an
67 // expensive parse.
68 if (!observableMatcher.hasMatch(content)) return null;
69
70 var id = transform.primaryInput.id;
71 // TODO(sigmund): improve how we compute this url
72 var url = id.path.startsWith('lib/')
73 ? 'package:${id.package}/${id.path.substring(4)}' : id.path;
74 var sourceFile = new SourceFile(content, url: url);
75 var logger = new BuildLogger(transform,
76 convertErrorsToWarnings: !releaseMode,
77 detailsUri: 'http://goo.gl/5HPeuP');
78 var transaction = _transformCompilationUnit(
79 content, sourceFile, logger);
80 if (!transaction.hasEdits) {
81 transform.addOutput(transform.primaryInput);
82 } else {
83 var printer = transaction.commit();
84 // TODO(sigmund): emit source maps when barback supports it (see
85 // dartbug.com/12340)
86 printer.build(url);
87 transform.addOutput(new Asset.fromString(id, printer.text));
88 }
89 return logger.writeOutput();
90 });
91 }
92 }
93
94 TextEditTransaction _transformCompilationUnit(
95 String inputCode, SourceFile sourceFile, BuildLogger logger) {
96 var unit = parseCompilationUnit(inputCode, suppressErrors: true);
97 var code = new TextEditTransaction(inputCode, sourceFile);
98 for (var directive in unit.directives) {
99 if (directive is LibraryDirective && _hasObservable(directive)) {
100 logger.warning(NO_OBSERVABLE_ON_LIBRARY,
101 span: _getSpan(sourceFile, directive));
102 break;
103 }
104 }
105
106 for (var declaration in unit.declarations) {
107 if (declaration is ClassDeclaration) {
108 _transformClass(declaration, code, sourceFile, logger);
109 } else if (declaration is TopLevelVariableDeclaration) {
110 if (_hasObservable(declaration)) {
111 logger.warning(NO_OBSERVABLE_ON_TOP_LEVEL,
112 span: _getSpan(sourceFile, declaration));
113 }
114 }
115 }
116 return code;
117 }
118
119 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end);
120
121 /// True if the node has the `@observable` or `@published` annotation.
122 // TODO(jmesserly): it is not good to be hard coding Polymer support here.
123 bool _hasObservable(AnnotatedNode node) =>
124 node.metadata.any(_isObservableAnnotation);
125
126 // TODO(jmesserly): this isn't correct if the annotation has been imported
127 // with a prefix, or cases like that. We should technically be resolving, but
128 // that is expensive in analyzer, so it isn't feasible yet.
129 bool _isObservableAnnotation(Annotation node) =>
130 _isAnnotationContant(node, 'observable') ||
131 _isAnnotationContant(node, 'published') ||
132 _isAnnotationType(node, 'ObservableProperty') ||
133 _isAnnotationType(node, 'PublishedProperty');
134
135 bool _isAnnotationContant(Annotation m, String name) =>
136 m.name.name == name && m.constructorName == null && m.arguments == null;
137
138 bool _isAnnotationType(Annotation m, String name) => m.name.name == name;
139
140 void _transformClass(ClassDeclaration cls, TextEditTransaction code,
141 SourceFile file, BuildLogger logger) {
142
143 if (_hasObservable(cls)) {
144 logger.warning(NO_OBSERVABLE_ON_CLASS, span: _getSpan(file, cls));
145 }
146
147 // We'd like to track whether observable was declared explicitly, otherwise
148 // report a warning later below. Because we don't have type analysis (only
149 // syntactic understanding of the code), we only report warnings that are
150 // known to be true.
151 var explicitObservable = false;
152 var implicitObservable = false;
153 if (cls.extendsClause != null) {
154 var id = _getSimpleIdentifier(cls.extendsClause.superclass.name);
155 if (id.name == 'Observable') {
156 code.edit(id.offset, id.end, 'ChangeNotifier');
157 explicitObservable = true;
158 } else if (id.name == 'ChangeNotifier') {
159 explicitObservable = true;
160 } else if (id.name != 'HtmlElement' && id.name != 'CustomElement'
161 && id.name != 'Object') {
162 // TODO(sigmund): this is conservative, consider using type-resolution to
163 // improve this check.
164 implicitObservable = true;
165 }
166 }
167
168 if (cls.withClause != null) {
169 for (var type in cls.withClause.mixinTypes) {
170 var id = _getSimpleIdentifier(type.name);
171 if (id.name == 'Observable') {
172 code.edit(id.offset, id.end, 'ChangeNotifier');
173 explicitObservable = true;
174 break;
175 } else if (id.name == 'ChangeNotifier') {
176 explicitObservable = true;
177 break;
178 } else {
179 // TODO(sigmund): this is conservative, consider using type-resolution
180 // to improve this check.
181 implicitObservable = true;
182 }
183 }
184 }
185
186 if (cls.implementsClause != null) {
187 // TODO(sigmund): consider adding type-resolution to give a more precise
188 // answer.
189 implicitObservable = true;
190 }
191
192 var declaresObservable = explicitObservable || implicitObservable;
193
194 // Track fields that were transformed.
195 var instanceFields = new Set<String>();
196
197 for (var member in cls.members) {
198 if (member is FieldDeclaration) {
199 if (member.isStatic) {
200 if (_hasObservable(member)){
201 logger.warning(NO_OBSERVABLE_ON_STATIC_FIELD,
202 span: _getSpan(file, member));
203 }
204 continue;
205 }
206 if (_hasObservable(member)) {
207 if (!declaresObservable) {
208 logger.warning(REQUIRE_OBSERVABLE_INTERFACE,
209 span: _getSpan(file, member));
210 }
211 _transformFields(file, member, code, logger);
212
213 var names = member.fields.variables.map((v) => v.name.name);
214
215 if (!_isReadOnly(member.fields)) instanceFields.addAll(names);
216 }
217 }
218 }
219
220 // If nothing was @observable, bail.
221 if (instanceFields.length == 0) return;
222
223 if (!explicitObservable) _mixinObservable(cls, code);
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 /// Adds "with ChangeNotifier" and associated implementation.
234 void _mixinObservable(ClassDeclaration cls, TextEditTransaction code) {
235 // Note: we need to be careful to put the with clause after extends, but
236 // before implements clause.
237 if (cls.withClause != null) {
238 var pos = cls.withClause.end;
239 code.edit(pos, pos, ', ChangeNotifier');
240 } else if (cls.extendsClause != null) {
241 var pos = cls.extendsClause.end;
242 code.edit(pos, pos, ' with ChangeNotifier ');
243 } else {
244 var params = cls.typeParameters;
245 var pos = params != null ? params.end : cls.name.end;
246 code.edit(pos, pos, ' extends ChangeNotifier ');
247 }
248 }
249
250 SimpleIdentifier _getSimpleIdentifier(Identifier id) =>
251 id is PrefixedIdentifier ? id.identifier : id;
252
253
254 bool _hasKeyword(Token token, Keyword keyword) =>
255 token is KeywordToken && token.keyword == keyword;
256
257 String _getOriginalCode(TextEditTransaction code, AstNode node) =>
258 code.original.substring(node.offset, node.end);
259
260 void _fixConstructor(ConstructorDeclaration ctor, TextEditTransaction code,
261 Set<String> changedFields) {
262
263 // Fix normal initializers
264 for (var initializer in ctor.initializers) {
265 if (initializer is ConstructorFieldInitializer) {
266 var field = initializer.fieldName;
267 if (changedFields.contains(field.name)) {
268 code.edit(field.offset, field.end, '__\$${field.name}');
269 }
270 }
271 }
272
273 // Fix "this." initializer in parameter list. These are tricky:
274 // we need to preserve the name and add an initializer.
275 // Preserving the name is important for named args, and for dartdoc.
276 // BEFORE: Foo(this.bar, this.baz) { ... }
277 // AFTER: Foo(bar, baz) : __$bar = bar, __$baz = baz { ... }
278
279 var thisInit = [];
280 for (var param in ctor.parameters.parameters) {
281 if (param is DefaultFormalParameter) {
282 param = param.parameter;
283 }
284 if (param is FieldFormalParameter) {
285 var name = param.identifier.name;
286 if (changedFields.contains(name)) {
287 thisInit.add(name);
288 // Remove "this." but keep everything else.
289 code.edit(param.thisToken.offset, param.period.end, '');
290 }
291 }
292 }
293
294 if (thisInit.length == 0) return;
295
296 // TODO(jmesserly): smarter formatting with indent, etc.
297 var inserted = thisInit.map((i) => '__\$$i = $i').join(', ');
298
299 int offset;
300 if (ctor.separator != null) {
301 offset = ctor.separator.end;
302 inserted = ' $inserted,';
303 } else {
304 offset = ctor.parameters.end;
305 inserted = ' : $inserted';
306 }
307
308 code.edit(offset, offset, inserted);
309 }
310
311 bool _isReadOnly(VariableDeclarationList fields) {
312 return _hasKeyword(fields.keyword, Keyword.CONST) ||
313 _hasKeyword(fields.keyword, Keyword.FINAL);
314 }
315
316 void _transformFields(SourceFile file, FieldDeclaration member,
317 TextEditTransaction code, BuildLogger logger) {
318
319 final fields = member.fields;
320 if (_isReadOnly(fields)) return;
321
322 // Private fields aren't supported:
323 for (var field in fields.variables) {
324 final name = field.name.name;
325 if (Identifier.isPrivateName(name)) {
326 logger.warning('Cannot make private field $name observable.',
327 span: _getSpan(file, field));
328 return;
329 }
330 }
331
332 // Unfortunately "var" doesn't work in all positions where type annotations
333 // are allowed, such as "var get name". So we use "dynamic" instead.
334 var type = 'dynamic';
335 if (fields.type != null) {
336 type = _getOriginalCode(code, fields.type);
337 } else if (_hasKeyword(fields.keyword, Keyword.VAR)) {
338 // Replace 'var' with 'dynamic'
339 code.edit(fields.keyword.offset, fields.keyword.end, type);
340 }
341
342 // Note: the replacements here are a bit subtle. It needs to support multiple
343 // fields declared via the same @observable, as well as preserving newlines.
344 // (Preserving newlines is important because it allows the generated code to
345 // be debugged without needing a source map.)
346 //
347 // For example:
348 //
349 // @observable
350 // @otherMetaData
351 // Foo
352 // foo = 1, bar = 2,
353 // baz;
354 //
355 // Will be transformed into something like:
356 //
357 // @reflectable @observable
358 // @OtherMetaData()
359 // Foo
360 // get foo => __foo; Foo __foo = 1; @reflectable set foo ...; ...
361 // @observable @OtherMetaData() Foo get baz => __baz; Foo baz; ...
362 //
363 // Metadata is moved to the getter.
364
365 String metadata = '';
366 if (fields.variables.length > 1) {
367 metadata = member.metadata
368 .map((m) => _getOriginalCode(code, m))
369 .join(' ');
370 metadata = '@reflectable $metadata';
371 }
372
373 for (int i = 0; i < fields.variables.length; i++) {
374 final field = fields.variables[i];
375 final name = field.name.name;
376
377 var beforeInit = 'get $name => __\$$name; $type __\$$name';
378
379 // The first field is expanded differently from subsequent fields, because
380 // we can reuse the metadata and type annotation.
381 if (i == 0) {
382 final begin = member.metadata.first.offset;
383 code.edit(begin, begin, '@reflectable ');
384 } else {
385 beforeInit = '$metadata $type $beforeInit';
386 }
387
388 code.edit(field.name.offset, field.name.end, beforeInit);
389
390 // Replace comma with semicolon
391 final end = _findFieldSeperator(field.endToken.next);
392 if (end.type == TokenType.COMMA) code.edit(end.offset, end.end, ';');
393
394 code.edit(end.end, end.end, ' @reflectable set $name($type value) { '
395 '__\$$name = notifyPropertyChange(#$name, __\$$name, value); }');
396 }
397 }
398
399 Token _findFieldSeperator(Token token) {
400 while (token != null) {
401 if (token.type == TokenType.COMMA || token.type == TokenType.SEMICOLON) {
402 break;
403 }
404 token = token.next;
405 }
406 return token;
407 }
408
409 // TODO(sigmund): remove hard coded Polymer support (@published). The proper way
410 // to do this would be to switch to use the analyzer to resolve whether
411 // annotations are subtypes of ObservableProperty.
412 final observableMatcher =
413 new RegExp("@(published|observable|PublishedProperty|ObservableProperty)");
OLDNEW
« no previous file with comments | « pkg/observe/lib/src/to_observable.dart ('k') | pkg/observe/pubspec.yaml » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698