OLD | NEW |
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 /// Code transform for @observable. The core transformation is relatively | 5 /// Code transform for @observable. The core transformation is relatively |
6 /// straightforward, and essentially like an editor refactoring. | 6 /// straightforward, and essentially like an editor refactoring. |
7 library observe.transformer; | 7 library observe.transformer; |
8 | 8 |
9 import 'dart:async'; | 9 import 'dart:async'; |
10 | 10 |
11 import 'package:analyzer/analyzer.dart'; | 11 import 'package:analyzer/analyzer.dart'; |
12 import 'package:analyzer/src/generated/ast.dart'; | 12 import 'package:analyzer/src/generated/ast.dart'; |
13 import 'package:analyzer/src/generated/error.dart'; | 13 import 'package:analyzer/src/generated/error.dart'; |
14 import 'package:analyzer/src/generated/parser.dart'; | 14 import 'package:analyzer/src/generated/parser.dart'; |
15 import 'package:analyzer/src/generated/scanner.dart'; | 15 import 'package:analyzer/src/generated/scanner.dart'; |
16 import 'package:barback/barback.dart'; | 16 import 'package:barback/barback.dart'; |
| 17 import 'package:code_transformers/messages/build_logger.dart'; |
17 import 'package:source_maps/refactor.dart'; | 18 import 'package:source_maps/refactor.dart'; |
18 import 'package:source_span/source_span.dart'; | 19 import 'package:source_span/source_span.dart'; |
19 | 20 |
| 21 import 'src/messages.dart'; |
| 22 |
20 /// A [Transformer] that replaces observables based on dirty-checking with an | 23 /// A [Transformer] that replaces observables based on dirty-checking with an |
21 /// implementation based on change notifications. | 24 /// implementation based on change notifications. |
22 /// | 25 /// |
23 /// The transformation adds hooks for field setters and notifies the observation | 26 /// The transformation adds hooks for field setters and notifies the observation |
24 /// system of the change. | 27 /// system of the change. |
25 class ObservableTransformer extends Transformer { | 28 class ObservableTransformer extends Transformer { |
26 | 29 |
| 30 final bool releaseMode; |
27 final List<String> _files; | 31 final List<String> _files; |
28 ObservableTransformer([List<String> files]) : _files = files; | 32 ObservableTransformer([List<String> files, bool releaseMode]) |
| 33 : _files = files, releaseMode = releaseMode == true; |
29 ObservableTransformer.asPlugin(BarbackSettings settings) | 34 ObservableTransformer.asPlugin(BarbackSettings settings) |
30 : _files = _readFiles(settings.configuration['files']); | 35 : _files = _readFiles(settings.configuration['files']), |
| 36 releaseMode = settings.mode == BarbackMode.RELEASE; |
31 | 37 |
32 static List<String> _readFiles(value) { | 38 static List<String> _readFiles(value) { |
33 if (value == null) return null; | 39 if (value == null) return null; |
34 var files = []; | 40 var files = []; |
35 bool error; | 41 bool error; |
36 if (value is List) { | 42 if (value is List) { |
37 files = value; | 43 files = value; |
38 error = value.any((e) => e is! String); | 44 error = value.any((e) => e is! String); |
39 } else if (value is String) { | 45 } else if (value is String) { |
40 files = [value]; | 46 files = [value]; |
(...skipping 18 matching lines...) Expand all Loading... |
59 // Do a quick string check to determine if this is this file even | 65 // Do a quick string check to determine if this is this file even |
60 // plausibly might need to be transformed. If not, we can avoid an | 66 // plausibly might need to be transformed. If not, we can avoid an |
61 // expensive parse. | 67 // expensive parse. |
62 if (!observableMatcher.hasMatch(content)) return; | 68 if (!observableMatcher.hasMatch(content)) return; |
63 | 69 |
64 var id = transform.primaryInput.id; | 70 var id = transform.primaryInput.id; |
65 // TODO(sigmund): improve how we compute this url | 71 // TODO(sigmund): improve how we compute this url |
66 var url = id.path.startsWith('lib/') | 72 var url = id.path.startsWith('lib/') |
67 ? 'package:${id.package}/${id.path.substring(4)}' : id.path; | 73 ? 'package:${id.package}/${id.path.substring(4)}' : id.path; |
68 var sourceFile = new SourceFile(content, url: url); | 74 var sourceFile = new SourceFile(content, url: url); |
| 75 var logger = new BuildLogger(transform, |
| 76 convertErrorsToWarnings: !releaseMode); |
69 var transaction = _transformCompilationUnit( | 77 var transaction = _transformCompilationUnit( |
70 content, sourceFile, transform.logger); | 78 content, sourceFile, logger); |
71 if (!transaction.hasEdits) { | 79 if (transaction.hasEdits) { |
72 transform.addOutput(transform.primaryInput); | 80 var printer = transaction.commit(); |
73 return; | 81 // TODO(sigmund): emit source maps when barback supports it (see |
| 82 // dartbug.com/12340) |
| 83 printer.build(url); |
| 84 transform.addOutput(new Asset.fromString(id, printer.text)); |
74 } | 85 } |
75 var printer = transaction.commit(); | 86 return logger.writeOutput(); |
76 // TODO(sigmund): emit source maps when barback supports it (see | |
77 // dartbug.com/12340) | |
78 printer.build(url); | |
79 transform.addOutput(new Asset.fromString(id, printer.text)); | |
80 }); | 87 }); |
81 } | 88 } |
82 } | 89 } |
83 | 90 |
84 TextEditTransaction _transformCompilationUnit( | 91 TextEditTransaction _transformCompilationUnit( |
85 String inputCode, SourceFile sourceFile, TransformLogger logger) { | 92 String inputCode, SourceFile sourceFile, BuildLogger logger) { |
86 var unit = parseCompilationUnit(inputCode, suppressErrors: true); | 93 var unit = parseCompilationUnit(inputCode, suppressErrors: true); |
87 var code = new TextEditTransaction(inputCode, sourceFile); | 94 var code = new TextEditTransaction(inputCode, sourceFile); |
88 for (var directive in unit.directives) { | 95 for (var directive in unit.directives) { |
89 if (directive is LibraryDirective && _hasObservable(directive)) { | 96 if (directive is LibraryDirective && _hasObservable(directive)) { |
90 logger.warning('@observable on a library no longer has any effect. ' | 97 logger.warning(noObservableOnLibrary, |
91 'It should be placed on individual fields.', | |
92 span: _getSpan(sourceFile, directive)); | 98 span: _getSpan(sourceFile, directive)); |
93 break; | 99 break; |
94 } | 100 } |
95 } | 101 } |
96 | 102 |
97 for (var declaration in unit.declarations) { | 103 for (var declaration in unit.declarations) { |
98 if (declaration is ClassDeclaration) { | 104 if (declaration is ClassDeclaration) { |
99 _transformClass(declaration, code, sourceFile, logger); | 105 _transformClass(declaration, code, sourceFile, logger); |
100 } else if (declaration is TopLevelVariableDeclaration) { | 106 } else if (declaration is TopLevelVariableDeclaration) { |
101 if (_hasObservable(declaration)) { | 107 if (_hasObservable(declaration)) { |
102 logger.warning('Top-level fields can no longer be observable. ' | 108 logger.warning(noObservableOnTopLevel, |
103 'Observable fields should be put in an observable objects.', | |
104 span: _getSpan(sourceFile, declaration)); | 109 span: _getSpan(sourceFile, declaration)); |
105 } | 110 } |
106 } | 111 } |
107 } | 112 } |
108 return code; | 113 return code; |
109 } | 114 } |
110 | 115 |
111 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); | 116 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); |
112 | 117 |
113 /// True if the node has the `@observable` or `@published` annotation. | 118 /// True if the node has the `@observable` or `@published` annotation. |
114 // TODO(jmesserly): it is not good to be hard coding Polymer support here. | 119 // TODO(jmesserly): it is not good to be hard coding Polymer support here. |
115 bool _hasObservable(AnnotatedNode node) => | 120 bool _hasObservable(AnnotatedNode node) => |
116 node.metadata.any(_isObservableAnnotation); | 121 node.metadata.any(_isObservableAnnotation); |
117 | 122 |
118 // TODO(jmesserly): this isn't correct if the annotation has been imported | 123 // 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 | 124 // with a prefix, or cases like that. We should technically be resolving, but |
120 // that is expensive in analyzer, so it isn't feasible yet. | 125 // that is expensive in analyzer, so it isn't feasible yet. |
121 bool _isObservableAnnotation(Annotation node) => | 126 bool _isObservableAnnotation(Annotation node) => |
122 _isAnnotationContant(node, 'observable') || | 127 _isAnnotationContant(node, 'observable') || |
123 _isAnnotationContant(node, 'published') || | 128 _isAnnotationContant(node, 'published') || |
124 _isAnnotationType(node, 'ObservableProperty') || | 129 _isAnnotationType(node, 'ObservableProperty') || |
125 _isAnnotationType(node, 'PublishedProperty'); | 130 _isAnnotationType(node, 'PublishedProperty'); |
126 | 131 |
127 bool _isAnnotationContant(Annotation m, String name) => | 132 bool _isAnnotationContant(Annotation m, String name) => |
128 m.name.name == name && m.constructorName == null && m.arguments == null; | 133 m.name.name == name && m.constructorName == null && m.arguments == null; |
129 | 134 |
130 bool _isAnnotationType(Annotation m, String name) => m.name.name == name; | 135 bool _isAnnotationType(Annotation m, String name) => m.name.name == name; |
131 | 136 |
132 void _transformClass(ClassDeclaration cls, TextEditTransaction code, | 137 void _transformClass(ClassDeclaration cls, TextEditTransaction code, |
133 SourceFile file, TransformLogger logger) { | 138 SourceFile file, BuildLogger logger) { |
134 | 139 |
135 if (_hasObservable(cls)) { | 140 if (_hasObservable(cls)) { |
136 logger.warning('@observable on a class no longer has any effect. ' | 141 logger.warning(noObservableOnClass, span: _getSpan(file, cls)); |
137 'It should be placed on individual fields.', | |
138 span: _getSpan(file, cls)); | |
139 } | 142 } |
140 | 143 |
141 // We'd like to track whether observable was declared explicitly, otherwise | 144 // We'd like to track whether observable was declared explicitly, otherwise |
142 // report a warning later below. Because we don't have type analysis (only | 145 // report a warning later below. Because we don't have type analysis (only |
143 // syntactic understanding of the code), we only report warnings that are | 146 // syntactic understanding of the code), we only report warnings that are |
144 // known to be true. | 147 // known to be true. |
145 var explicitObservable = false; | 148 var explicitObservable = false; |
146 var implicitObservable = false; | 149 var implicitObservable = false; |
147 if (cls.extendsClause != null) { | 150 if (cls.extendsClause != null) { |
148 var id = _getSimpleIdentifier(cls.extendsClause.superclass.name); | 151 var id = _getSimpleIdentifier(cls.extendsClause.superclass.name); |
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
185 | 188 |
186 var declaresObservable = explicitObservable || implicitObservable; | 189 var declaresObservable = explicitObservable || implicitObservable; |
187 | 190 |
188 // Track fields that were transformed. | 191 // Track fields that were transformed. |
189 var instanceFields = new Set<String>(); | 192 var instanceFields = new Set<String>(); |
190 | 193 |
191 for (var member in cls.members) { | 194 for (var member in cls.members) { |
192 if (member is FieldDeclaration) { | 195 if (member is FieldDeclaration) { |
193 if (member.isStatic) { | 196 if (member.isStatic) { |
194 if (_hasObservable(member)){ | 197 if (_hasObservable(member)){ |
195 logger.warning('Static fields can no longer be observable. ' | 198 logger.warning(noObservableOnStaticField, |
196 'Observable fields should be put in an observable objects.', | |
197 span: _getSpan(file, member)); | 199 span: _getSpan(file, member)); |
198 } | 200 } |
199 continue; | 201 continue; |
200 } | 202 } |
201 if (_hasObservable(member)) { | 203 if (_hasObservable(member)) { |
202 if (!declaresObservable) { | 204 if (!declaresObservable) { |
203 logger.warning('Observable fields should be put in an observable ' | 205 logger.warning(requireObservableInterface, |
204 'objects. Please declare that this class extends from ' | |
205 'Observable, includes Observable, or implements ' | |
206 'Observable.', | |
207 span: _getSpan(file, member)); | 206 span: _getSpan(file, member)); |
208 } | 207 } |
209 _transformFields(file, member, code, logger); | 208 _transformFields(file, member, code, logger); |
210 | 209 |
211 var names = member.fields.variables.map((v) => v.name.name); | 210 var names = member.fields.variables.map((v) => v.name.name); |
212 | 211 |
213 if (!_isReadOnly(member.fields)) instanceFields.addAll(names); | 212 if (!_isReadOnly(member.fields)) instanceFields.addAll(names); |
214 } | 213 } |
215 } | 214 } |
216 } | 215 } |
(...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
305 | 304 |
306 code.edit(offset, offset, inserted); | 305 code.edit(offset, offset, inserted); |
307 } | 306 } |
308 | 307 |
309 bool _isReadOnly(VariableDeclarationList fields) { | 308 bool _isReadOnly(VariableDeclarationList fields) { |
310 return _hasKeyword(fields.keyword, Keyword.CONST) || | 309 return _hasKeyword(fields.keyword, Keyword.CONST) || |
311 _hasKeyword(fields.keyword, Keyword.FINAL); | 310 _hasKeyword(fields.keyword, Keyword.FINAL); |
312 } | 311 } |
313 | 312 |
314 void _transformFields(SourceFile file, FieldDeclaration member, | 313 void _transformFields(SourceFile file, FieldDeclaration member, |
315 TextEditTransaction code, TransformLogger logger) { | 314 TextEditTransaction code, BuildLogger logger) { |
316 | 315 |
317 final fields = member.fields; | 316 final fields = member.fields; |
318 if (_isReadOnly(fields)) return; | 317 if (_isReadOnly(fields)) return; |
319 | 318 |
320 // Private fields aren't supported: | 319 // Private fields aren't supported: |
321 for (var field in fields.variables) { | 320 for (var field in fields.variables) { |
322 final name = field.name.name; | 321 final name = field.name.name; |
323 if (Identifier.isPrivateName(name)) { | 322 if (Identifier.isPrivateName(name)) { |
324 logger.warning('Cannot make private field $name observable.', | 323 logger.warning('Cannot make private field $name observable.', |
325 span: _getSpan(file, field)); | 324 span: _getSpan(file, field)); |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
402 token = token.next; | 401 token = token.next; |
403 } | 402 } |
404 return token; | 403 return token; |
405 } | 404 } |
406 | 405 |
407 // TODO(sigmund): remove hard coded Polymer support (@published). The proper way | 406 // TODO(sigmund): remove hard coded Polymer support (@published). The proper way |
408 // to do this would be to switch to use the analyzer to resolve whether | 407 // to do this would be to switch to use the analyzer to resolve whether |
409 // annotations are subtypes of ObservableProperty. | 408 // annotations are subtypes of ObservableProperty. |
410 final observableMatcher = | 409 final observableMatcher = |
411 new RegExp("@(published|observable|PublishedProperty|ObservableProperty)"); | 410 new RegExp("@(published|observable|PublishedProperty|ObservableProperty)"); |
OLD | NEW |