Index: lib/src/observable_transform.dart |
diff --git a/lib/src/observable_transform.dart b/lib/src/observable_transform.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..422864d2856739e8dd96b6c0ac4a7f0844dcf414 |
--- /dev/null |
+++ b/lib/src/observable_transform.dart |
@@ -0,0 +1,160 @@ |
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+/** |
+ * Code transform for @observable. The core transformation is relatively |
+ * straightforward, and essentially like an editor refactoring. You can find the |
+ * core implementation in [transformClass], which is ultimately called by |
+ * [transformObservables], the entry point to this library. |
+ */ |
+library observable_transform; |
+ |
+import 'package:compiler_unsupported/implementation/elements/elements.dart'; |
+import 'package:compiler_unsupported/implementation/scanner/scannerlib.dart'; |
+import 'package:compiler_unsupported/implementation/tree/tree.dart'; |
+import 'package:compiler_unsupported/implementation/util/util.dart'; |
+import 'dart_parser.dart'; |
+import 'info.dart'; |
+import 'messages.dart'; |
+import 'refactor.dart'; |
+ |
+// TODO(jmesserly): this doesn't work for "this." in constructors. |
+/** |
+ * Transform types in Dart [code] marked with `@observable` by hooking all field |
+ * setters, and notifying the observation system of the change. If the code was |
+ * changed this returns true, otherwise returns false. Modified code can be |
+ * found in [info.userCode.code]. |
+ * |
+ * Note: there is no special checking for transitive immutability. It is up to |
+ * the rest of the observation system to handle check for this condition and. |
+ * handle it appropriately. We do not want to violate reference equality of |
+ * any fields that are set into the object. |
+ */ |
+bool transformObservables(LibraryInfo info, {Messages messages}) { |
+ if (info.userCode == null) return false; |
+ var oldCode = info.userCode.code; |
+ |
+ // Avoid parsing unless we think it has @observable. |
+ // TODO(jmesserly): investigate why the dart2js parser isn't fast enough. |
+ // If we don't do this check, it adds ~100ms overhead, even for the simple |
+ // TodoMVC example. Because that example uses @observable, it's not just VM |
+ // warm up time for the dart2js code, so I'm not sure what's going on. |
+ if (!oldCode.contains('@observable')) return false; |
+ |
+ var parsed = new DartCodeParser.parse(info.inputPath, oldCode, |
+ messages: messages); |
+ |
+ var newCode = _transformParsedCode(parsed); |
+ if (identical(oldCode, newCode)) return false; |
+ |
+ // Replace the code |
+ info.userCode.code = newCode; |
+ return true; |
+} |
+ |
+String _transformParsedCode(DartCodeParser parser) { |
+ // If parsing failed, don't try to transform the code. |
+ if (!parser.success) return parser.code; |
+ |
+ var transformedCode = new TextEditTransaction(parser.code); |
+ for (var element in parser.unit.localMembers) { |
+ if (element is PartialClassElement) { |
+ transformClass(element, parser, transformedCode); |
+ } |
+ } |
+ return transformedCode.commit(); |
+} |
+ |
+void transformClass(PartialClassElement element, DartCodeParser parser, |
+ TextEditTransaction code) { |
+ |
+ var classNode = parser.parseClass(element); |
+ |
+ // TODO(jmesserly): this isn't correct if observable has been imported |
+ // with a prefix, or cases like that. We should technically be resolving, but |
+ // that is expensive. |
+ bool hasObservable = false; |
+ for (var metadata in element.metadata) { |
+ var node = metadata.parseNode(parser.diagnostics); |
+ if (node is Send && node.selector is Identifier && |
+ node.selector.source.slowToString() == 'observable') { |
+ hasObservable = true; |
+ } |
+ } |
+ |
+ if (!hasObservable) return; |
+ |
+ bool transformed = false; |
+ for (var member in element.localMembers) { |
+ if (member.isField()) { |
+ VariableElement field = member; |
+ if (transformFields(field.variables, parser.diagnostics, code)) { |
+ // TODO(jmesserly): if class was transformed, mixin Observable |
+ transformed = true; |
+ } |
+ } |
+ } |
+ |
+} |
+ |
+bool transformFields(VariableListElement member, DiagnosticListener diagnostics, |
+ TextEditTransaction code) { |
+ |
+ if (member.cachedNode != null) { |
+ // If the cached node already exists, it means we're seeing something like |
+ // "bar" in this example code, and we've already generated it: |
+ // var foo, bar; |
+ // We need to transform both fields together. |
+ return false; |
+ } |
+ |
+ var mod = member.modifiers; |
+ if (mod.isStatic() || mod.isAbstract() || mod.isFinal() || mod.isConst()) { |
+ return false; |
+ } |
+ |
+ var defs = member.parseNode(diagnostics); |
+ int begin = defs.getBeginToken().charOffset; |
+ int end = defs.getEndToken().charOffset + 1; |
+ var indent = guessIndent(code.original, begin); |
+ var replace = new StringBuffer(); |
+ |
+ for (var def in defs.definitions) { |
+ Identifier identifier; |
+ String initializer; |
+ if (def is Identifier) { |
+ identifier = def; |
+ initializer = ''; |
+ } else { |
+ SendSet send = def; |
+ identifier = send.selector; |
+ initializer = ' = ${send.arguments.head}'; |
+ } |
+ |
+ var name = identifier.source.slowToString(); |
+ var type = defs.type; |
+ if (type == null && mod.isVar()) { |
+ type = 'var'; |
+ } |
+ |
+ if (replace.length > 0) replace.add('\n\n$indent'); |
+ replace.add(''' |
+$type __\$$name$initializer; |
+$type get $name { |
+ if (autogenerated.observeReads) { |
+ this.notifyRead(autogenerated.ChangeRecord.FIELD, '$name'); |
+ } |
+ return __\$$name; |
+} |
+set $name($type value) { |
+ if (this.hasObservers) { |
+ this.notifyChange(autogenerated.ChangeRecord.FIELD, '$name', __\$$name, value); |
+ } |
+ __\$$name = value; |
+}'''.replaceAll('\n', '\n$indent')); |
+ } |
+ |
+ code.edit(begin, end, '$replace'); |
+ return true; |
+} |