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

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

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

Powered by Google App Engine
This is Rietveld 408576698