| 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;
|
| +}
|
|
|