| 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 /** | 5 /** |
| 6 * Code transform for @observable. The core transformation is relatively | 6 * Code transform for @observable. The core transformation is relatively |
| 7 * straightforward, and essentially like an editor refactoring. | 7 * straightforward, and essentially like an editor refactoring. |
| 8 */ | 8 */ |
| 9 library observe.transform; | 9 library observe.transform; |
| 10 | 10 |
| (...skipping 16 matching lines...) Expand all Loading... |
| 27 */ | 27 */ |
| 28 class ObservableTransformer extends Transformer { | 28 class ObservableTransformer extends Transformer { |
| 29 | 29 |
| 30 Future<bool> isPrimary(Asset input) { | 30 Future<bool> isPrimary(Asset input) { |
| 31 if (input.id.extension != '.dart') return new Future.value(false); | 31 if (input.id.extension != '.dart') return new Future.value(false); |
| 32 // Note: technically we should parse the file to find accurately the | 32 // Note: technically we should parse the file to find accurately the |
| 33 // observable annotation, but that seems expensive. It would require almost | 33 // observable annotation, but that seems expensive. It would require almost |
| 34 // as much work as applying the transform. We rather have some false | 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 | 35 // positives here, and then generate no outputs when we apply this |
| 36 // transform. | 36 // transform. |
| 37 return input.readAsString().then((c) => c.contains("@observable")); | 37 return input.readAsString().then( |
| 38 (c) => c.contains("@observable") || c.contains("@published")); |
| 38 } | 39 } |
| 39 | 40 |
| 40 Future apply(Transform transform) { | 41 Future apply(Transform transform) { |
| 41 return transform.primaryInput.readAsString().then((content) { | 42 return transform.primaryInput.readAsString().then((content) { |
| 42 var id = transform.primaryInput.id; | 43 var id = transform.primaryInput.id; |
| 43 // TODO(sigmund): improve how we compute this url | 44 // TODO(sigmund): improve how we compute this url |
| 44 var url = id.path.startsWith('lib/') | 45 var url = id.path.startsWith('lib/') |
| 45 ? 'package:${id.package}/${id.path.substring(4)}' : id.path; | 46 ? 'package:${id.package}/${id.path.substring(4)}' : id.path; |
| 46 var sourceFile = new SourceFile.text(url, content); | 47 var sourceFile = new SourceFile.text(url, content); |
| 47 var transaction = _transformCompilationUnit( | 48 var transaction = _transformCompilationUnit( |
| (...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 95 return parser.parseCompilationUnit(token); | 96 return parser.parseCompilationUnit(token); |
| 96 } | 97 } |
| 97 | 98 |
| 98 class _ErrorCollector extends AnalysisErrorListener { | 99 class _ErrorCollector extends AnalysisErrorListener { |
| 99 final errors = <AnalysisError>[]; | 100 final errors = <AnalysisError>[]; |
| 100 onError(error) => errors.add(error); | 101 onError(error) => errors.add(error); |
| 101 } | 102 } |
| 102 | 103 |
| 103 _getSpan(SourceFile file, ASTNode node) => file.span(node.offset, node.end); | 104 _getSpan(SourceFile file, ASTNode node) => file.span(node.offset, node.end); |
| 104 | 105 |
| 105 /** True if the node has the `@observable` annotation. */ | 106 /** True if the node has the `@observable` or `@published` annotation. */ |
| 106 bool _hasObservable(AnnotatedNode node) => _hasAnnotation(node, 'observable'); | 107 // TODO(jmesserly): it is not good to be hard coding these. We should do a |
| 108 // resolve and do a proper ObservableProperty subtype check. However resolve |
| 109 // is very expensive in analyzer_experimental, so it isn't feasible yet. |
| 110 bool _hasObservable(AnnotatedNode node) => |
| 111 _hasAnnotation(node, 'observable') || _hasAnnotation(node, 'published'); |
| 107 | 112 |
| 108 bool _hasAnnotation(AnnotatedNode node, String name) { | 113 bool _hasAnnotation(AnnotatedNode node, String name) { |
| 109 // TODO(jmesserly): this isn't correct if the annotation has been imported | 114 // TODO(jmesserly): this isn't correct if the annotation has been imported |
| 110 // with a prefix, or cases like that. We should technically be resolving, but | 115 // with a prefix, or cases like that. We should technically be resolving, but |
| 111 // that is expensive. | 116 // that is expensive. |
| 112 return node.metadata.any((m) => m.name.name == name && | 117 return node.metadata.any((m) => m.name.name == name && |
| 113 m.constructorName == null && m.arguments == null); | 118 m.constructorName == null && m.arguments == null); |
| 114 } | 119 } |
| 115 | 120 |
| 116 void _transformClass(ClassDeclaration cls, TextEditTransaction code, | 121 void _transformClass(ClassDeclaration cls, TextEditTransaction code, |
| (...skipping 10 matching lines...) Expand all Loading... |
| 127 // syntactic understanding of the code), we only report warnings that are | 132 // syntactic understanding of the code), we only report warnings that are |
| 128 // known to be true. | 133 // known to be true. |
| 129 var declaresObservable = false; | 134 var declaresObservable = false; |
| 130 if (cls.extendsClause != null) { | 135 if (cls.extendsClause != null) { |
| 131 var id = _getSimpleIdentifier(cls.extendsClause.superclass.name); | 136 var id = _getSimpleIdentifier(cls.extendsClause.superclass.name); |
| 132 if (id.name == 'ObservableBase') { | 137 if (id.name == 'ObservableBase') { |
| 133 code.edit(id.offset, id.end, 'ChangeNotifierBase'); | 138 code.edit(id.offset, id.end, 'ChangeNotifierBase'); |
| 134 declaresObservable = true; | 139 declaresObservable = true; |
| 135 } else if (id.name == 'ChangeNotifierBase') { | 140 } else if (id.name == 'ChangeNotifierBase') { |
| 136 declaresObservable = true; | 141 declaresObservable = true; |
| 137 } else if (id.name != 'PolymerElement' && id.name != 'CustomElement' | 142 } else if (id.name != 'HtmlElement' && id.name != 'CustomElement' |
| 138 && id.name != 'Object') { | 143 && id.name != 'Object') { |
| 139 // TODO(sigmund): this is conservative, consider using type-resolution to | 144 // TODO(sigmund): this is conservative, consider using type-resolution to |
| 140 // improve this check. | 145 // improve this check. |
| 141 declaresObservable = true; | 146 declaresObservable = true; |
| 142 } | 147 } |
| 143 } | 148 } |
| 144 | 149 |
| 145 if (cls.withClause != null) { | 150 if (cls.withClause != null) { |
| 146 for (var type in cls.withClause.mixinTypes) { | 151 for (var type in cls.withClause.mixinTypes) { |
| 147 var id = _getSimpleIdentifier(type.name); | 152 var id = _getSimpleIdentifier(type.name); |
| (...skipping 28 matching lines...) Expand all Loading... |
| 176 if (member.isStatic) { | 181 if (member.isStatic) { |
| 177 if (_hasObservable(member)){ | 182 if (_hasObservable(member)){ |
| 178 logger.warning('Static fields can no longer be observable. ' | 183 logger.warning('Static fields can no longer be observable. ' |
| 179 'Observable fields should be put in an observable objects.', | 184 'Observable fields should be put in an observable objects.', |
| 180 _getSpan(file, member)); | 185 _getSpan(file, member)); |
| 181 } | 186 } |
| 182 continue; | 187 continue; |
| 183 } | 188 } |
| 184 if (_hasObservable(member)) { | 189 if (_hasObservable(member)) { |
| 185 if (!declaresObservable) { | 190 if (!declaresObservable) { |
| 186 logger.warning('Observable fields should be put in an observable' | 191 logger.warning('Observable fields should be put in an observable ' |
| 187 ' objects. Please declare that this class extends from ' | 192 'objects. Please declare that this class extends from ' |
| 188 'ObservableBase, includes ObservableMixin, or implements ' | 193 'ObservableBase, includes ObservableMixin, or implements ' |
| 189 'Observable.', | 194 'Observable.', |
| 190 _getSpan(file, member)); | 195 _getSpan(file, member)); |
| 191 | |
| 192 } | 196 } |
| 193 _transformFields(member.fields, code, member.offset, member.end); | 197 _transformFields(file, member, code, logger); |
| 194 | 198 |
| 195 var names = member.fields.variables.map((v) => v.name.name); | 199 var names = member.fields.variables.map((v) => v.name.name); |
| 196 | 200 |
| 197 getters.addAll(names); | 201 getters.addAll(names); |
| 198 if (!_isReadOnly(member.fields)) { | 202 if (!_isReadOnly(member.fields)) { |
| 199 setters.addAll(names); | 203 setters.addAll(names); |
| 200 instanceFields.addAll(names); | 204 instanceFields.addAll(names); |
| 201 } | 205 } |
| 202 } | 206 } |
| 203 } | 207 } |
| (...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 282 } | 286 } |
| 283 | 287 |
| 284 code.edit(offset, offset, inserted); | 288 code.edit(offset, offset, inserted); |
| 285 } | 289 } |
| 286 | 290 |
| 287 bool _isReadOnly(VariableDeclarationList fields) { | 291 bool _isReadOnly(VariableDeclarationList fields) { |
| 288 return _hasKeyword(fields.keyword, Keyword.CONST) || | 292 return _hasKeyword(fields.keyword, Keyword.CONST) || |
| 289 _hasKeyword(fields.keyword, Keyword.FINAL); | 293 _hasKeyword(fields.keyword, Keyword.FINAL); |
| 290 } | 294 } |
| 291 | 295 |
| 292 void _transformFields(VariableDeclarationList fields, TextEditTransaction code, | 296 void _transformFields(SourceFile file, FieldDeclaration member, |
| 293 int begin, int end) { | 297 TextEditTransaction code, TransformLogger logger) { |
| 294 | 298 |
| 299 final fields = member.fields; |
| 295 if (_isReadOnly(fields)) return; | 300 if (_isReadOnly(fields)) return; |
| 296 | 301 |
| 297 var indent = guessIndent(code.original, begin); | 302 // Private fields aren't supported: |
| 298 var replace = new StringBuffer(); | 303 for (var field in fields.variables) { |
| 304 final name = field.name.name; |
| 305 if (Identifier.isPrivateName(name)) { |
| 306 logger.warning('Cannot make private field $name observable.', |
| 307 _getSpan(file, field)); |
| 308 return; |
| 309 } |
| 310 } |
| 299 | 311 |
| 300 // Unfortunately "var" doesn't work in all positions where type annotations | 312 // Unfortunately "var" doesn't work in all positions where type annotations |
| 301 // are allowed, such as "var get name". So we use "dynamic" instead. | 313 // are allowed, such as "var get name". So we use "dynamic" instead. |
| 302 var type = 'dynamic'; | 314 var type = 'dynamic'; |
| 303 if (fields.type != null) { | 315 if (fields.type != null) { |
| 304 type = _getOriginalCode(code, fields.type); | 316 type = _getOriginalCode(code, fields.type); |
| 317 } else if (_hasKeyword(fields.keyword, Keyword.VAR)) { |
| 318 // Replace 'var' with 'dynamic' |
| 319 code.edit(fields.keyword.offset, fields.keyword.end, type); |
| 305 } | 320 } |
| 306 | 321 |
| 307 for (var field in fields.variables) { | 322 // Note: the replacements here are a bit subtle. It needs to support multiple |
| 308 var initializer = ''; | 323 // fields declared via the same @observable, as well as preserving newlines. |
| 309 if (field.initializer != null) { | 324 // (Preserving newlines is important because it allows the generated code to |
| 310 initializer = ' = ${_getOriginalCode(code, field.initializer)}'; | 325 // be debugged without needing a source map.) |
| 311 } | 326 // |
| 327 // For example: |
| 328 // |
| 329 // @observable |
| 330 // @otherMetaData |
| 331 // Foo |
| 332 // foo = 1, bar = 2, |
| 333 // baz; |
| 334 // |
| 335 // Will be transformed into something like: |
| 336 // |
| 337 // @observable |
| 338 // @OtherMetaData() |
| 339 // Foo |
| 340 // get foo => __foo; Foo __foo = 1; set foo ...; ... bar ... |
| 341 // @observable @OtherMetaData() Foo get baz => __baz; Foo baz; ... |
| 342 // |
| 343 // Metadata is moved to the getter. |
| 312 | 344 |
| 313 var name = field.name.name; | 345 String metadata = ''; |
| 314 | 346 if (fields.variables.length > 1) { |
| 315 // TODO(jmesserly): should we generate this one one line, so source maps | 347 metadata = member.metadata |
| 316 // don't break? | 348 .map((m) => _getOriginalCode(code, m)) |
| 317 if (replace.length > 0) replace.write('\n\n$indent'); | 349 .join(' '); |
| 318 replace.write(''' | |
| 319 $type __\$$name$initializer; | |
| 320 $type get $name => __\$$name; | |
| 321 set $name($type value) { | |
| 322 __\$$name = notifyPropertyChange(const Symbol('$name'), __\$$name, value); | |
| 323 } | |
| 324 '''.replaceAll('\n', '\n$indent')); | |
| 325 } | 350 } |
| 326 | 351 |
| 327 code.edit(begin, end, '$replace'); | 352 for (int i = 0; i < fields.variables.length; i++) { |
| 353 final field = fields.variables[i]; |
| 354 final name = field.name.name; |
| 355 |
| 356 var beforeInit = 'get $name => __\$$name; $type __\$$name'; |
| 357 |
| 358 // The first field is expanded differently from subsequent fields, because |
| 359 // we can reuse the metadata and type annotation. |
| 360 if (i > 0) beforeInit = '$metadata $type $beforeInit'; |
| 361 |
| 362 code.edit(field.name.offset, field.name.end, beforeInit); |
| 363 |
| 364 // Replace comma with semicolon |
| 365 final end = _findFieldSeperator(field.endToken.next); |
| 366 if (end.type == TokenType.COMMA) code.edit(end.offset, end.end, ';'); |
| 367 |
| 368 code.edit(end.end, end.end, ' set $name($type value) { ' |
| 369 '__\$$name = notifyPropertyChange(#$name, __\$$name, value); }'); |
| 370 } |
| 328 } | 371 } |
| 372 |
| 373 Token _findFieldSeperator(Token token) { |
| 374 while (token != null) { |
| 375 if (token.type == TokenType.COMMA || token.type == TokenType.SEMICOLON) { |
| 376 break; |
| 377 } |
| 378 token = token.next; |
| 379 } |
| 380 return token; |
| 381 } |
| OLD | NEW |