OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2012, 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. You can find the |
| 8 * core implementation in [transformClass], which is ultimately called by |
| 9 * [transformObservables], the entry point to this library. |
| 10 */ |
| 11 library observable_transform; |
| 12 |
| 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/scanner.dart'; |
| 16 import 'dart_parser.dart'; |
| 17 import 'messages.dart'; |
| 18 import 'refactor.dart'; |
| 19 |
| 20 /** |
| 21 * Transform types in Dart [userCode] marked with `@observable` by hooking all |
| 22 * field setters, and notifying the observation system of the change. If the |
| 23 * code was changed this returns true, otherwise returns false. Modified code |
| 24 * can be found in [userCode.code]. |
| 25 * |
| 26 * Note: there is no special checking for transitive immutability. It is up to |
| 27 * the rest of the observation system to handle check for this condition and |
| 28 * handle it appropriately. We do not want to violate reference equality of |
| 29 * any fields that are set into the object. |
| 30 */ |
| 31 bool transformObservables(DartCodeInfo userCode, Messages messages) { |
| 32 if (userCode == null || userCode.compilationUnit == null) return false; |
| 33 |
| 34 var oldCode = userCode.code; |
| 35 var transaction = new TextEditTransaction(oldCode); |
| 36 transformCompilationUnit(userCode.compilationUnit, transaction); |
| 37 |
| 38 var newCode = transaction.commit(); |
| 39 if (identical(oldCode, newCode)) return false; |
| 40 |
| 41 // Replace the code |
| 42 userCode.code = newCode; |
| 43 return true; |
| 44 } |
| 45 |
| 46 void transformCompilationUnit(CompilationUnit unit, TextEditTransaction code) { |
| 47 bool observeAll = unit.directives.any( |
| 48 (d) => d is LibraryDirective && hasObservable(d)); |
| 49 |
| 50 for (var declaration in unit.declarations) { |
| 51 if (declaration is ClassDeclaration) { |
| 52 transformClass(declaration, code, observeAll); |
| 53 } else if (declaration is TopLevelVariableDeclaration) { |
| 54 if (observeAll || hasObservable(declaration)) { |
| 55 transformTopLevelField(declaration, code); |
| 56 } |
| 57 } |
| 58 } |
| 59 } |
| 60 |
| 61 /** True if the code has the `@observable` annotation. */ |
| 62 bool hasObservable(AnnotatedNode node) { |
| 63 // TODO(jmesserly): this isn't correct if observable has been imported |
| 64 // with a prefix, or cases like that. We should technically be resolving, but |
| 65 // that is expensive. |
| 66 return node.metadata.any((m) => m.name.name == 'observable' && |
| 67 m.constructorName == null && m.arguments == null); |
| 68 } |
| 69 |
| 70 void transformClass(ClassDeclaration cls, TextEditTransaction code, |
| 71 bool observeAll) { |
| 72 |
| 73 observeAll = observeAll || hasObservable(cls); |
| 74 |
| 75 var changedFields = new Set<String>(); |
| 76 for (var member in cls.members) { |
| 77 if (member is FieldDeclaration) { |
| 78 if (observeAll || hasObservable(member)) { |
| 79 transformClassFields(member, code, changedFields); |
| 80 } |
| 81 } |
| 82 } |
| 83 |
| 84 if (changedFields.length == 0) return; |
| 85 |
| 86 // Fix initializers, because they aren't allowed to call the setter. |
| 87 for (var member in cls.members) { |
| 88 if (member is ConstructorDeclaration) { |
| 89 fixConstructor(member, code, changedFields); |
| 90 } |
| 91 } |
| 92 } |
| 93 |
| 94 bool hasKeyword(Token token, Keyword keyword) => |
| 95 token is KeywordToken && token.keyword == keyword; |
| 96 |
| 97 String getOriginalCode(TextEditTransaction code, ASTNode node) => |
| 98 code.original.substring(node.offset, node.end); |
| 99 |
| 100 void transformTopLevelField(TopLevelVariableDeclaration field, |
| 101 TextEditTransaction code) { |
| 102 transformFields(field.variables, code, field.offset, field.end); |
| 103 } |
| 104 |
| 105 void transformClassFields(FieldDeclaration member, TextEditTransaction code, |
| 106 Set<String> changedFields) { |
| 107 |
| 108 transformFields(member.fields, code, member.offset, member.end, |
| 109 isStatic: hasKeyword(member.keyword, Keyword.STATIC), |
| 110 changedFields: changedFields); |
| 111 } |
| 112 |
| 113 |
| 114 void fixConstructor(ConstructorDeclaration ctor, TextEditTransaction code, |
| 115 Set<String> changedFields) { |
| 116 |
| 117 // Fix normal initializers |
| 118 for (var initializer in ctor.initializers) { |
| 119 if (initializer is ConstructorFieldInitializer) { |
| 120 var field = initializer.fieldName; |
| 121 if (changedFields.contains(field.name)) { |
| 122 code.edit(field.offset, field.end, '__\$${field.name}'); |
| 123 } |
| 124 } |
| 125 } |
| 126 |
| 127 // Fix "this." initializer in parameter list. These are tricky: |
| 128 // we need to preserve the name and add an initializer. |
| 129 // Preserving the name is important for named args, and for dartdoc. |
| 130 // BEFORE: Foo(this.bar, this.baz) { ... } |
| 131 // AFTER: Foo(bar, baz) : __$bar = bar, __$baz = baz { ... } |
| 132 |
| 133 var thisInit = []; |
| 134 for (var param in ctor.parameters.parameters) { |
| 135 if (param is FieldFormalParameter) { |
| 136 var name = param.identifier.name; |
| 137 if (changedFields.contains(name)) { |
| 138 thisInit.add(name); |
| 139 // Remove "this." but keep everything else. |
| 140 code.edit(param.thisToken.offset, param.period.end, ''); |
| 141 } |
| 142 } |
| 143 } |
| 144 |
| 145 if (thisInit.length == 0) return; |
| 146 |
| 147 // TODO(jmesserly): smarter formatting with indent, etc. |
| 148 var inserted = thisInit.map((i) => '__\$$i = $i').join(', '); |
| 149 |
| 150 int offset; |
| 151 if (ctor.separator != null) { |
| 152 offset = ctor.separator.end; |
| 153 inserted = ' $inserted,'; |
| 154 } else { |
| 155 offset = ctor.parameters.end; |
| 156 inserted = ' : $inserted'; |
| 157 } |
| 158 |
| 159 code.edit(offset, offset, inserted); |
| 160 } |
| 161 |
| 162 void transformFields(VariableDeclarationList fields, TextEditTransaction code, |
| 163 int begin, int end, {bool isStatic: false, Set<String> changedFields}) { |
| 164 |
| 165 if (hasKeyword(fields.keyword, Keyword.CONST) || |
| 166 hasKeyword(fields.keyword, Keyword.FINAL)) { |
| 167 return; |
| 168 } |
| 169 |
| 170 var indent = guessIndent(code.original, begin); |
| 171 var replace = new StringBuffer(); |
| 172 |
| 173 // Unfortunately "var" doesn't work in all positions where type annotations |
| 174 // are allowed, such as "var get name". So we use "dynamic" instead. |
| 175 var type = 'dynamic'; |
| 176 if (fields.type != null) { |
| 177 type = getOriginalCode(code, fields.type); |
| 178 } |
| 179 |
| 180 var mod = isStatic ? 'static ' : ''; |
| 181 |
| 182 for (var field in fields.variables) { |
| 183 var initializer = ''; |
| 184 if (field.initializer != null) { |
| 185 initializer = ' = ${getOriginalCode(code, field.initializer)}'; |
| 186 } |
| 187 |
| 188 var name = field.name.name; |
| 189 |
| 190 if (replace.length > 0) replace.add('\n\n$indent'); |
| 191 replace.add(''' |
| 192 ${mod}$type __\$$name$initializer; |
| 193 ${mod}Object __obs\$$name; |
| 194 ${mod}$type get $name { |
| 195 if (autogenerated.observeReads) { |
| 196 __obs\$$name = autogenerated.notifyRead(__obs\$$name); |
| 197 } |
| 198 return __\$$name; |
| 199 } |
| 200 ${mod}set $name($type value) { |
| 201 if (__obs\$$name != null && __\$$name != value) { |
| 202 __obs\$$name = autogenerated.notifyWrite(__obs\$$name); |
| 203 } |
| 204 __\$$name = value; |
| 205 }'''.replaceAll('\n', '\n$indent')); |
| 206 |
| 207 if (changedFields != null) changedFields.add(name); |
| 208 } |
| 209 |
| 210 code.edit(begin, end, '$replace'); |
| 211 } |
OLD | NEW |