Chromium Code Reviews| OLD | NEW |
|---|---|
| (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 /** | |
| 6 * Code transform for @observable. The core transformation is relatively | |
| 7 * straightforward, and essentially like an editor refactoring. | |
| 8 */ | |
| 9 library observe.transform; | |
| 10 | |
| 11 import 'dart:async'; | |
| 12 import 'package:path/path.dart' as path; | |
| 13 import 'package:analyzer_experimental/src/generated/ast.dart'; | |
| 14 import 'package:analyzer_experimental/src/generated/error.dart'; | |
| 15 import 'package:analyzer_experimental/src/generated/parser.dart'; | |
| 16 import 'package:analyzer_experimental/src/generated/scanner.dart'; | |
| 17 import 'package:barback/barback.dart'; | |
| 18 import 'package:codegen/refactor.dart'; | |
| 19 import 'package:source_maps/span.dart' show SourceFile; | |
| 20 | |
| 21 /** | |
| 22 * A [Transformer] that replaces observables based on dirty-checking with an | |
| 23 * implementation based on change notifications. | |
| 24 * | |
| 25 * The transformation adds hooks for field setters and notifies the observation | |
| 26 * system of the change. | |
| 27 */ | |
| 28 class ObservableTransformer extends Transformer { | |
| 29 | |
| 30 Future<bool> isPrimary(Asset input) { | |
| 31 if (input.id.extension != '.dart') return new Future.value(false); | |
| 32 // Note: technically we should parse the file to find accurately the | |
| 33 // observable annotation, but that seems expensive. It would require almost | |
| 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 | |
| 36 // transform. | |
|
Bob Nystrom
2013/08/08 22:47:09
You could always do this fast check first and then
Siggi Cherem (dart-lang)
2013/08/08 23:14:02
true, but I feel is ok in that case to do the work
nweiz
2013/08/08 23:34:57
There's no harm in running the transform on a file
Siggi Cherem (dart-lang)
2013/08/09 00:29:54
correct. Or in my case not spit anything =)
| |
| 37 return input.readAsString().then((c) => c.contains("@observable")); | |
| 38 } | |
| 39 | |
| 40 Future apply(Transform transform) { | |
| 41 return transform.primaryInput | |
| 42 .then((input) => input.readAsString()).then((content) { | |
| 43 var id = transform.primaryId; | |
| 44 var sourceFile = new SourceFile.text(id.toString(), content); | |
|
nweiz
2013/08/08 23:34:57
Right now, [id.toString] will return a string of t
Siggi Cherem (dart-lang)
2013/08/09 00:29:54
I see. since the path doesn't have the package con
nweiz
2013/08/09 00:53:48
Not in general. Intermediate assets may have no on
| |
| 45 var transaction = _transformCompilationUnit( | |
| 46 content, sourceFile, transform.log); | |
| 47 if (transaction.hasEdits) { | |
|
Bob Nystrom
2013/08/08 22:47:09
I would do:
if (!transaction.hasEdits) return;
T
Siggi Cherem (dart-lang)
2013/08/08 23:10:41
Done. Yeah, I like doing that too.
nweiz
2013/08/08 23:34:57
If this returns without any outputs, that's not th
Siggi Cherem (dart-lang)
2013/08/09 00:29:54
I'm not sure I see what's the difference, so I'll
Siggi Cherem (dart-lang)
2013/08/09 00:40:24
Fixed. So it turns out that if we dont emit it, it
| |
| 48 var printer = transaction.commit(); | |
| 49 var filename = path.basename(id.path); | |
| 50 printer.add('\n//# sourceMappingURL=$filename.map'); | |
| 51 printer.build(id.path); | |
| 52 transform.addOutput(new Asset.fromString(id, printer.text)); | |
| 53 transform.addOutput( | |
| 54 new Asset.fromString(id.addExtension('.map'), printer.map)); | |
|
nweiz
2013/08/08 23:34:57
This sourcemap is not going to consistently point
Siggi Cherem (dart-lang)
2013/08/09 00:29:54
Oh my! those are good points. I don't know yet wha
nweiz
2013/08/09 00:53:48
I'd just get rid of the sourcemaps here for now un
Siggi Cherem (dart-lang)
2013/08/09 01:17:30
ok, no prob. I removed it for now. Our current use
| |
| 55 } | |
| 56 }); | |
|
Bob Nystrom
2013/08/08 22:47:09
Nice!
Siggi Cherem (dart-lang)
2013/08/08 23:10:41
:)
| |
| 57 } | |
| 58 } | |
| 59 | |
| 60 void _transformCompilationUnit( | |
|
Bob Nystrom
2013/08/08 22:47:09
Should this be void?
Siggi Cherem (dart-lang)
2013/08/08 23:10:41
oops, good catch, thanks.
| |
| 61 String inputCode, SourceFile sourceFile, Log log) { | |
| 62 var unit = _parseCompilationUnit(inputCode); | |
| 63 return transformObservables(unit, sourceFile, inputCode, log); | |
| 64 } | |
| 65 | |
| 66 // TODO(sigmund): make this private. This is currently public so it can be used | |
| 67 // by the polymer.dart package which is not yet entirely migrated to use | |
| 68 // pub-serve and pub-deploy. | |
| 69 TextEditTransaction transformObservables(CompilationUnit unit, | |
| 70 SourceFile sourceFile, String content, Log log) { | |
| 71 var code = new TextEditTransaction(content, sourceFile); | |
| 72 for (var directive in unit.directives) { | |
| 73 if (directive is LibraryDirective && _hasObservable(directive)) { | |
| 74 log.warning('@observable on a library no longer has any effect. ' | |
| 75 'It should be placed on individual fields.', | |
| 76 _getSpan(sourceFile, directive)); | |
| 77 break; | |
| 78 } | |
| 79 } | |
| 80 | |
| 81 for (var declaration in unit.declarations) { | |
| 82 if (declaration is ClassDeclaration) { | |
| 83 _transformClass(declaration, code, sourceFile, log); | |
| 84 } else if (declaration is TopLevelVariableDeclaration) { | |
| 85 if (_hasObservable(declaration)) { | |
| 86 log.warning('Top-level fields can no longer be observable. ' | |
| 87 'Observable fields should be put in an observable objects.', | |
| 88 _getSpan(sourceFile, declaration)); | |
| 89 } | |
| 90 } | |
| 91 } | |
| 92 return code; | |
| 93 } | |
| 94 | |
| 95 /** Parse [code] using analyzer_experimental. */ | |
| 96 CompilationUnit _parseCompilationUnit(String code) { | |
| 97 var errorListener = new _ErrorCollector(); | |
| 98 var scanner = new StringScanner(null, code, errorListener); | |
| 99 var token = scanner.tokenize(); | |
| 100 var parser = new Parser(null, errorListener); | |
| 101 return parser.parseCompilationUnit(token); | |
| 102 } | |
| 103 | |
| 104 class _ErrorCollector extends AnalysisErrorListener { | |
| 105 final errors = new List<AnalysisError>(); | |
|
Bob Nystrom
2013/08/08 22:47:09
final errors = <AnalysisError>[];
Siggi Cherem (dart-lang)
2013/08/08 23:10:41
Done.
| |
| 106 onError(error) => errors.add(error); | |
| 107 } | |
| 108 | |
| 109 _getSpan(SourceFile file, ASTNode node) => file.span(node.offset, node.end); | |
| 110 | |
| 111 /** True if the node has the `@observable` annotation. */ | |
| 112 bool _hasObservable(AnnotatedNode node) => _hasAnnotation(node, 'observable'); | |
| 113 | |
| 114 bool _hasAnnotation(AnnotatedNode node, String name) { | |
| 115 // TODO(jmesserly): this isn't correct if the annotation has been imported | |
| 116 // with a prefix, or cases like that. We should technically be resolving, but | |
| 117 // that is expensive. | |
| 118 return node.metadata.any((m) => m.name.name == name && | |
| 119 m.constructorName == null && m.arguments == null); | |
| 120 } | |
| 121 | |
| 122 void _transformClass(ClassDeclaration cls, TextEditTransaction code, | |
| 123 SourceFile file, Log log) { | |
| 124 | |
| 125 if (_hasObservable(cls)) { | |
| 126 log.warning('@observable on a class no longer has any effect. ' | |
| 127 'It should be placed on individual fields.', | |
| 128 _getSpan(file, cls)); | |
| 129 } | |
| 130 | |
| 131 // We'd like to track whether observable was declared explicitly, otherwise | |
| 132 // report a warning later below. Because we don't have type analysis (only | |
| 133 // syntactic understanding of the code), we only report warnings that are | |
| 134 // known to be true. | |
| 135 var declaresObservable = false; | |
| 136 if (cls.extendsClause != null) { | |
| 137 var id = _getSimpleIdentifier(cls.extendsClause.superclass.name); | |
| 138 if (id.name == 'ObservableBase') { | |
| 139 code.edit(id.offset, id.end, 'ChangeNotifierBase'); | |
| 140 declaresObservable = true; | |
| 141 } else if (id.name == 'ChangeNotifierBase') { | |
| 142 declaresObservable = true; | |
| 143 } else if (id.name != 'PolymerElement' && id.name != 'CustomElement' | |
| 144 && id.name != 'Object') { | |
| 145 // TODO(sigmund): this is conservative, consider using type-resolution to | |
| 146 // improve this check. | |
| 147 declaresObservable = true; | |
| 148 } | |
| 149 } | |
| 150 | |
| 151 if (cls.withClause != null) { | |
| 152 for (var type in cls.withClause.mixinTypes) { | |
| 153 var id = _getSimpleIdentifier(type.name); | |
| 154 if (id.name == 'ObservableMixin') { | |
| 155 code.edit(id.offset, id.end, 'ChangeNotifierMixin'); | |
| 156 declaresObservable = true; | |
| 157 break; | |
| 158 } else if (id.name == 'ChangeNotifierMixin') { | |
| 159 declaresObservable = true; | |
| 160 break; | |
| 161 } else { | |
| 162 // TODO(sigmund): this is conservative, consider using type-resolution | |
| 163 // to improve this check. | |
| 164 declaresObservable = true; | |
| 165 } | |
| 166 } | |
| 167 } | |
| 168 | |
| 169 if (!declaresObservable && cls.implementsClause != null) { | |
| 170 // TODO(sigmund): consider adding type-resolution to give a more precise | |
| 171 // answer. | |
| 172 declaresObservable = true; | |
| 173 } | |
| 174 | |
| 175 // Track fields that were transformed. | |
| 176 var instanceFields = new Set<String>(); | |
| 177 var getters = new List<String>(); | |
| 178 var setters = new List<String>(); | |
| 179 | |
| 180 for (var member in cls.members) { | |
| 181 if (member is FieldDeclaration) { | |
| 182 bool isStatic = _hasKeyword(member.keyword, Keyword.STATIC); | |
| 183 if (isStatic) { | |
| 184 if (_hasObservable(member)){ | |
| 185 log.warning('Static fields can no longer be observable. ' | |
| 186 'Observable fields should be put in an observable objects.', | |
| 187 _getSpan(file, member)); | |
| 188 } | |
| 189 continue; | |
| 190 } | |
| 191 if (_hasObservable(member)) { | |
| 192 if (!declaresObservable) { | |
| 193 log.warning('Observable fields should be put in an observable' | |
| 194 ' objects. Please declare that this class extends from ' | |
| 195 'ObservableBase, includes ObservableMixin, or implements ' | |
| 196 'Observable.', | |
| 197 _getSpan(file, member)); | |
| 198 | |
| 199 } | |
| 200 _transformFields(member.fields, code, member.offset, member.end); | |
| 201 | |
| 202 var names = member.fields.variables.map((v) => v.name.name); | |
| 203 | |
| 204 getters.addAll(names); | |
| 205 if (!_isReadOnly(member.fields)) { | |
| 206 setters.addAll(names); | |
| 207 instanceFields.addAll(names); | |
| 208 } | |
| 209 } | |
| 210 } | |
| 211 // TODO(jmesserly): this is a temporary workaround until we can remove | |
| 212 // getValueWorkaround and setValueWorkaround. | |
| 213 if (member is MethodDeclaration) { | |
| 214 if (_hasKeyword(member.propertyKeyword, Keyword.GET)) { | |
| 215 getters.add(member.name.name); | |
| 216 } else if (_hasKeyword(member.propertyKeyword, Keyword.SET)) { | |
| 217 setters.add(member.name.name); | |
| 218 } | |
| 219 } | |
| 220 } | |
| 221 | |
| 222 // If nothing was @observable, bail. | |
| 223 if (instanceFields.length == 0) return; | |
| 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 SimpleIdentifier _getSimpleIdentifier(Identifier id) => | |
| 234 id is PrefixedIdentifier ? (id as PrefixedIdentifier).identifier : id; | |
| 235 | |
| 236 | |
| 237 bool _hasKeyword(Token token, Keyword keyword) => | |
| 238 token is KeywordToken && (token as KeywordToken).keyword == keyword; | |
| 239 | |
| 240 String _getOriginalCode(TextEditTransaction code, ASTNode node) => | |
| 241 code.original.substring(node.offset, node.end); | |
| 242 | |
| 243 void _fixConstructor(ConstructorDeclaration ctor, TextEditTransaction code, | |
| 244 Set<String> changedFields) { | |
| 245 | |
| 246 // Fix normal initializers | |
| 247 for (var initializer in ctor.initializers) { | |
| 248 if (initializer is ConstructorFieldInitializer) { | |
| 249 var field = initializer.fieldName; | |
| 250 if (changedFields.contains(field.name)) { | |
| 251 code.edit(field.offset, field.end, '__\$${field.name}'); | |
| 252 } | |
| 253 } | |
| 254 } | |
| 255 | |
| 256 // Fix "this." initializer in parameter list. These are tricky: | |
| 257 // we need to preserve the name and add an initializer. | |
| 258 // Preserving the name is important for named args, and for dartdoc. | |
| 259 // BEFORE: Foo(this.bar, this.baz) { ... } | |
| 260 // AFTER: Foo(bar, baz) : __$bar = bar, __$baz = baz { ... } | |
| 261 | |
| 262 var thisInit = []; | |
| 263 for (var param in ctor.parameters.parameters) { | |
| 264 if (param is DefaultFormalParameter) { | |
| 265 param = param.parameter; | |
| 266 } | |
| 267 if (param is FieldFormalParameter) { | |
| 268 var name = param.identifier.name; | |
| 269 if (changedFields.contains(name)) { | |
| 270 thisInit.add(name); | |
| 271 // Remove "this." but keep everything else. | |
| 272 code.edit(param.thisToken.offset, param.period.end, ''); | |
| 273 } | |
| 274 } | |
| 275 } | |
| 276 | |
| 277 if (thisInit.length == 0) return; | |
| 278 | |
| 279 // TODO(jmesserly): smarter formatting with indent, etc. | |
| 280 var inserted = thisInit.map((i) => '__\$$i = $i').join(', '); | |
| 281 | |
| 282 int offset; | |
| 283 if (ctor.separator != null) { | |
| 284 offset = ctor.separator.end; | |
| 285 inserted = ' $inserted,'; | |
| 286 } else { | |
| 287 offset = ctor.parameters.end; | |
| 288 inserted = ' : $inserted'; | |
| 289 } | |
| 290 | |
| 291 code.edit(offset, offset, inserted); | |
| 292 } | |
| 293 | |
| 294 bool _isReadOnly(VariableDeclarationList fields) { | |
| 295 return _hasKeyword(fields.keyword, Keyword.CONST) || | |
| 296 _hasKeyword(fields.keyword, Keyword.FINAL); | |
| 297 } | |
| 298 | |
| 299 void _transformFields(VariableDeclarationList fields, TextEditTransaction code, | |
| 300 int begin, int end) { | |
| 301 | |
| 302 if (_isReadOnly(fields)) return; | |
| 303 | |
| 304 var indent = guessIndent(code.original, begin); | |
| 305 var replace = new StringBuffer(); | |
| 306 | |
| 307 // Unfortunately "var" doesn't work in all positions where type annotations | |
| 308 // are allowed, such as "var get name". So we use "dynamic" instead. | |
| 309 var type = 'dynamic'; | |
| 310 if (fields.type != null) { | |
| 311 type = _getOriginalCode(code, fields.type); | |
| 312 } | |
| 313 | |
| 314 for (var field in fields.variables) { | |
| 315 var initializer = ''; | |
| 316 if (field.initializer != null) { | |
| 317 initializer = ' = ${_getOriginalCode(code, field.initializer)}'; | |
| 318 } | |
| 319 | |
| 320 var name = field.name.name; | |
| 321 | |
| 322 // TODO(jmesserly): should we generate this one one line, so source maps | |
| 323 // don't break? | |
| 324 if (replace.length > 0) replace.write('\n\n$indent'); | |
| 325 replace.write(''' | |
| 326 $type __\$$name$initializer; | |
| 327 $type get $name => __\$$name; | |
| 328 set $name($type value) { | |
| 329 __\$$name = notifyPropertyChange(const Symbol('$name'), __\$$name, value); | |
| 330 } | |
| 331 '''.replaceAll('\n', '\n$indent')); | |
| 332 } | |
| 333 | |
| 334 code.edit(begin, end, '$replace'); | |
| 335 } | |
| OLD | NEW |