| 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 11 matching lines...) Expand all Loading... |
| 52 var id = idOrAsset is AssetId ? idOrAsset : idOrAsset.id; | 58 var id = idOrAsset is AssetId ? idOrAsset : idOrAsset.id; |
| 53 return new Future.value(id.extension == '.dart' && | 59 return new Future.value(id.extension == '.dart' && |
| 54 (_files == null || _files.contains(id.path))); | 60 (_files == null || _files.contains(id.path))); |
| 55 } | 61 } |
| 56 | 62 |
| 57 Future apply(Transform transform) { | 63 Future apply(Transform transform) { |
| 58 return transform.primaryInput.readAsString().then((content) { | 64 return transform.primaryInput.readAsString().then((content) { |
| 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 null; |
| 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 transform.addOutput(transform.primaryInput); |
| 73 return; | 81 } else { |
| 82 var printer = transaction.commit(); |
| 83 // TODO(sigmund): emit source maps when barback supports it (see |
| 84 // dartbug.com/12340) |
| 85 printer.build(url); |
| 86 transform.addOutput(new Asset.fromString(id, printer.text)); |
| 74 } | 87 } |
| 75 var printer = transaction.commit(); | 88 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 }); | 89 }); |
| 81 } | 90 } |
| 82 } | 91 } |
| 83 | 92 |
| 84 TextEditTransaction _transformCompilationUnit( | 93 TextEditTransaction _transformCompilationUnit( |
| 85 String inputCode, SourceFile sourceFile, TransformLogger logger) { | 94 String inputCode, SourceFile sourceFile, BuildLogger logger) { |
| 86 var unit = parseCompilationUnit(inputCode, suppressErrors: true); | 95 var unit = parseCompilationUnit(inputCode, suppressErrors: true); |
| 87 var code = new TextEditTransaction(inputCode, sourceFile); | 96 var code = new TextEditTransaction(inputCode, sourceFile); |
| 88 for (var directive in unit.directives) { | 97 for (var directive in unit.directives) { |
| 89 if (directive is LibraryDirective && _hasObservable(directive)) { | 98 if (directive is LibraryDirective && _hasObservable(directive)) { |
| 90 logger.warning('@observable on a library no longer has any effect. ' | 99 logger.warning(NO_OBSERVABLE_ON_LIBRARY, |
| 91 'It should be placed on individual fields.', | |
| 92 span: _getSpan(sourceFile, directive)); | 100 span: _getSpan(sourceFile, directive)); |
| 93 break; | 101 break; |
| 94 } | 102 } |
| 95 } | 103 } |
| 96 | 104 |
| 97 for (var declaration in unit.declarations) { | 105 for (var declaration in unit.declarations) { |
| 98 if (declaration is ClassDeclaration) { | 106 if (declaration is ClassDeclaration) { |
| 99 _transformClass(declaration, code, sourceFile, logger); | 107 _transformClass(declaration, code, sourceFile, logger); |
| 100 } else if (declaration is TopLevelVariableDeclaration) { | 108 } else if (declaration is TopLevelVariableDeclaration) { |
| 101 if (_hasObservable(declaration)) { | 109 if (_hasObservable(declaration)) { |
| 102 logger.warning('Top-level fields can no longer be observable. ' | 110 logger.warning(NO_OBSERVABLE_ON_TOP_LEVEL, |
| 103 'Observable fields should be put in an observable objects.', | |
| 104 span: _getSpan(sourceFile, declaration)); | 111 span: _getSpan(sourceFile, declaration)); |
| 105 } | 112 } |
| 106 } | 113 } |
| 107 } | 114 } |
| 108 return code; | 115 return code; |
| 109 } | 116 } |
| 110 | 117 |
| 111 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); | 118 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); |
| 112 | 119 |
| 113 /// True if the node has the `@observable` or `@published` annotation. | 120 /// True if the node has the `@observable` or `@published` annotation. |
| 114 // TODO(jmesserly): it is not good to be hard coding Polymer support here. | 121 // TODO(jmesserly): it is not good to be hard coding Polymer support here. |
| 115 bool _hasObservable(AnnotatedNode node) => | 122 bool _hasObservable(AnnotatedNode node) => |
| 116 node.metadata.any(_isObservableAnnotation); | 123 node.metadata.any(_isObservableAnnotation); |
| 117 | 124 |
| 118 // TODO(jmesserly): this isn't correct if the annotation has been imported | 125 // 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 | 126 // 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. | 127 // that is expensive in analyzer, so it isn't feasible yet. |
| 121 bool _isObservableAnnotation(Annotation node) => | 128 bool _isObservableAnnotation(Annotation node) => |
| 122 _isAnnotationContant(node, 'observable') || | 129 _isAnnotationContant(node, 'observable') || |
| 123 _isAnnotationContant(node, 'published') || | 130 _isAnnotationContant(node, 'published') || |
| 124 _isAnnotationType(node, 'ObservableProperty') || | 131 _isAnnotationType(node, 'ObservableProperty') || |
| 125 _isAnnotationType(node, 'PublishedProperty'); | 132 _isAnnotationType(node, 'PublishedProperty'); |
| 126 | 133 |
| 127 bool _isAnnotationContant(Annotation m, String name) => | 134 bool _isAnnotationContant(Annotation m, String name) => |
| 128 m.name.name == name && m.constructorName == null && m.arguments == null; | 135 m.name.name == name && m.constructorName == null && m.arguments == null; |
| 129 | 136 |
| 130 bool _isAnnotationType(Annotation m, String name) => m.name.name == name; | 137 bool _isAnnotationType(Annotation m, String name) => m.name.name == name; |
| 131 | 138 |
| 132 void _transformClass(ClassDeclaration cls, TextEditTransaction code, | 139 void _transformClass(ClassDeclaration cls, TextEditTransaction code, |
| 133 SourceFile file, TransformLogger logger) { | 140 SourceFile file, BuildLogger logger) { |
| 134 | 141 |
| 135 if (_hasObservable(cls)) { | 142 if (_hasObservable(cls)) { |
| 136 logger.warning('@observable on a class no longer has any effect. ' | 143 logger.warning(NO_OBSERVABLE_ON_CLASS, span: _getSpan(file, cls)); |
| 137 'It should be placed on individual fields.', | |
| 138 span: _getSpan(file, cls)); | |
| 139 } | 144 } |
| 140 | 145 |
| 141 // We'd like to track whether observable was declared explicitly, otherwise | 146 // 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 | 147 // 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 | 148 // syntactic understanding of the code), we only report warnings that are |
| 144 // known to be true. | 149 // known to be true. |
| 145 var explicitObservable = false; | 150 var explicitObservable = false; |
| 146 var implicitObservable = false; | 151 var implicitObservable = false; |
| 147 if (cls.extendsClause != null) { | 152 if (cls.extendsClause != null) { |
| 148 var id = _getSimpleIdentifier(cls.extendsClause.superclass.name); | 153 var id = _getSimpleIdentifier(cls.extendsClause.superclass.name); |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 185 | 190 |
| 186 var declaresObservable = explicitObservable || implicitObservable; | 191 var declaresObservable = explicitObservable || implicitObservable; |
| 187 | 192 |
| 188 // Track fields that were transformed. | 193 // Track fields that were transformed. |
| 189 var instanceFields = new Set<String>(); | 194 var instanceFields = new Set<String>(); |
| 190 | 195 |
| 191 for (var member in cls.members) { | 196 for (var member in cls.members) { |
| 192 if (member is FieldDeclaration) { | 197 if (member is FieldDeclaration) { |
| 193 if (member.isStatic) { | 198 if (member.isStatic) { |
| 194 if (_hasObservable(member)){ | 199 if (_hasObservable(member)){ |
| 195 logger.warning('Static fields can no longer be observable. ' | 200 logger.warning(NO_OBSERVABLE_ON_STATIC_FIELD, |
| 196 'Observable fields should be put in an observable objects.', | |
| 197 span: _getSpan(file, member)); | 201 span: _getSpan(file, member)); |
| 198 } | 202 } |
| 199 continue; | 203 continue; |
| 200 } | 204 } |
| 201 if (_hasObservable(member)) { | 205 if (_hasObservable(member)) { |
| 202 if (!declaresObservable) { | 206 if (!declaresObservable) { |
| 203 logger.warning('Observable fields should be put in an observable ' | 207 logger.warning(REQUIRE_OBSERVABLE_INTERFACE, |
| 204 'objects. Please declare that this class extends from ' | |
| 205 'Observable, includes Observable, or implements ' | |
| 206 'Observable.', | |
| 207 span: _getSpan(file, member)); | 208 span: _getSpan(file, member)); |
| 208 } | 209 } |
| 209 _transformFields(file, member, code, logger); | 210 _transformFields(file, member, code, logger); |
| 210 | 211 |
| 211 var names = member.fields.variables.map((v) => v.name.name); | 212 var names = member.fields.variables.map((v) => v.name.name); |
| 212 | 213 |
| 213 if (!_isReadOnly(member.fields)) instanceFields.addAll(names); | 214 if (!_isReadOnly(member.fields)) instanceFields.addAll(names); |
| 214 } | 215 } |
| 215 } | 216 } |
| 216 } | 217 } |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 305 | 306 |
| 306 code.edit(offset, offset, inserted); | 307 code.edit(offset, offset, inserted); |
| 307 } | 308 } |
| 308 | 309 |
| 309 bool _isReadOnly(VariableDeclarationList fields) { | 310 bool _isReadOnly(VariableDeclarationList fields) { |
| 310 return _hasKeyword(fields.keyword, Keyword.CONST) || | 311 return _hasKeyword(fields.keyword, Keyword.CONST) || |
| 311 _hasKeyword(fields.keyword, Keyword.FINAL); | 312 _hasKeyword(fields.keyword, Keyword.FINAL); |
| 312 } | 313 } |
| 313 | 314 |
| 314 void _transformFields(SourceFile file, FieldDeclaration member, | 315 void _transformFields(SourceFile file, FieldDeclaration member, |
| 315 TextEditTransaction code, TransformLogger logger) { | 316 TextEditTransaction code, BuildLogger logger) { |
| 316 | 317 |
| 317 final fields = member.fields; | 318 final fields = member.fields; |
| 318 if (_isReadOnly(fields)) return; | 319 if (_isReadOnly(fields)) return; |
| 319 | 320 |
| 320 // Private fields aren't supported: | 321 // Private fields aren't supported: |
| 321 for (var field in fields.variables) { | 322 for (var field in fields.variables) { |
| 322 final name = field.name.name; | 323 final name = field.name.name; |
| 323 if (Identifier.isPrivateName(name)) { | 324 if (Identifier.isPrivateName(name)) { |
| 324 logger.warning('Cannot make private field $name observable.', | 325 logger.warning('Cannot make private field $name observable.', |
| 325 span: _getSpan(file, field)); | 326 span: _getSpan(file, field)); |
| (...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 402 token = token.next; | 403 token = token.next; |
| 403 } | 404 } |
| 404 return token; | 405 return token; |
| 405 } | 406 } |
| 406 | 407 |
| 407 // TODO(sigmund): remove hard coded Polymer support (@published). The proper way | 408 // 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 | 409 // to do this would be to switch to use the analyzer to resolve whether |
| 409 // annotations are subtypes of ObservableProperty. | 410 // annotations are subtypes of ObservableProperty. |
| 410 final observableMatcher = | 411 final observableMatcher = |
| 411 new RegExp("@(published|observable|PublishedProperty|ObservableProperty)"); | 412 new RegExp("@(published|observable|PublishedProperty|ObservableProperty)"); |
| OLD | NEW |