| 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..53f1c0dbeb1361b0f110480b9902f316464100aa
|
| --- /dev/null
|
| +++ b/lib/src/observable_transform.dart
|
| @@ -0,0 +1,211 @@
|
| +// 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:analyzer_experimental/src/generated/ast.dart';
|
| +import 'package:analyzer_experimental/src/generated/error.dart';
|
| +import 'package:analyzer_experimental/src/generated/scanner.dart';
|
| +import 'dart_parser.dart';
|
| +import 'messages.dart';
|
| +import 'refactor.dart';
|
| +
|
| +/**
|
| + * Transform types in Dart [userCode] 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 [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(DartCodeInfo userCode, Messages messages) {
|
| + if (userCode == null || userCode.compilationUnit == null) return false;
|
| +
|
| + var oldCode = userCode.code;
|
| + var transaction = new TextEditTransaction(oldCode);
|
| + transformCompilationUnit(userCode.compilationUnit, transaction);
|
| +
|
| + var newCode = transaction.commit();
|
| + if (identical(oldCode, newCode)) return false;
|
| +
|
| + // Replace the code
|
| + userCode.code = newCode;
|
| + return true;
|
| +}
|
| +
|
| +void transformCompilationUnit(CompilationUnit unit, TextEditTransaction code) {
|
| + bool observeAll = unit.directives.any(
|
| + (d) => d is LibraryDirective && hasObservable(d));
|
| +
|
| + for (var declaration in unit.declarations) {
|
| + if (declaration is ClassDeclaration) {
|
| + transformClass(declaration, code, observeAll);
|
| + } else if (declaration is TopLevelVariableDeclaration) {
|
| + if (observeAll || hasObservable(declaration)) {
|
| + transformTopLevelField(declaration, code);
|
| + }
|
| + }
|
| + }
|
| +}
|
| +
|
| +/** True if the code has the `@observable` annotation. */
|
| +bool hasObservable(AnnotatedNode node) {
|
| + // 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.
|
| + return node.metadata.any((m) => m.name.name == 'observable' &&
|
| + m.constructorName == null && m.arguments == null);
|
| +}
|
| +
|
| +void transformClass(ClassDeclaration cls, TextEditTransaction code,
|
| + bool observeAll) {
|
| +
|
| + observeAll = observeAll || hasObservable(cls);
|
| +
|
| + var changedFields = new Set<String>();
|
| + for (var member in cls.members) {
|
| + if (member is FieldDeclaration) {
|
| + if (observeAll || hasObservable(member)) {
|
| + transformClassFields(member, code, changedFields);
|
| + }
|
| + }
|
| + }
|
| +
|
| + if (changedFields.length == 0) return;
|
| +
|
| + // Fix initializers, because they aren't allowed to call the setter.
|
| + for (var member in cls.members) {
|
| + if (member is ConstructorDeclaration) {
|
| + fixConstructor(member, code, changedFields);
|
| + }
|
| + }
|
| +}
|
| +
|
| +bool hasKeyword(Token token, Keyword keyword) =>
|
| + token is KeywordToken && token.keyword == keyword;
|
| +
|
| +String getOriginalCode(TextEditTransaction code, ASTNode node) =>
|
| + code.original.substring(node.offset, node.end);
|
| +
|
| +void transformTopLevelField(TopLevelVariableDeclaration field,
|
| + TextEditTransaction code) {
|
| + transformFields(field.variables, code, field.offset, field.end);
|
| +}
|
| +
|
| +void transformClassFields(FieldDeclaration member, TextEditTransaction code,
|
| + Set<String> changedFields) {
|
| +
|
| + transformFields(member.fields, code, member.offset, member.end,
|
| + isStatic: hasKeyword(member.keyword, Keyword.STATIC),
|
| + changedFields: changedFields);
|
| +}
|
| +
|
| +
|
| +void fixConstructor(ConstructorDeclaration ctor, TextEditTransaction code,
|
| + Set<String> changedFields) {
|
| +
|
| + // Fix normal initializers
|
| + for (var initializer in ctor.initializers) {
|
| + if (initializer is ConstructorFieldInitializer) {
|
| + var field = initializer.fieldName;
|
| + if (changedFields.contains(field.name)) {
|
| + code.edit(field.offset, field.end, '__\$${field.name}');
|
| + }
|
| + }
|
| + }
|
| +
|
| + // Fix "this." initializer in parameter list. These are tricky:
|
| + // we need to preserve the name and add an initializer.
|
| + // Preserving the name is important for named args, and for dartdoc.
|
| + // BEFORE: Foo(this.bar, this.baz) { ... }
|
| + // AFTER: Foo(bar, baz) : __$bar = bar, __$baz = baz { ... }
|
| +
|
| + var thisInit = [];
|
| + for (var param in ctor.parameters.parameters) {
|
| + if (param is FieldFormalParameter) {
|
| + var name = param.identifier.name;
|
| + if (changedFields.contains(name)) {
|
| + thisInit.add(name);
|
| + // Remove "this." but keep everything else.
|
| + code.edit(param.thisToken.offset, param.period.end, '');
|
| + }
|
| + }
|
| + }
|
| +
|
| + if (thisInit.length == 0) return;
|
| +
|
| + // TODO(jmesserly): smarter formatting with indent, etc.
|
| + var inserted = thisInit.map((i) => '__\$$i = $i').join(', ');
|
| +
|
| + int offset;
|
| + if (ctor.separator != null) {
|
| + offset = ctor.separator.end;
|
| + inserted = ' $inserted,';
|
| + } else {
|
| + offset = ctor.parameters.end;
|
| + inserted = ' : $inserted';
|
| + }
|
| +
|
| + code.edit(offset, offset, inserted);
|
| +}
|
| +
|
| +void transformFields(VariableDeclarationList fields, TextEditTransaction code,
|
| + int begin, int end, {bool isStatic: false, Set<String> changedFields}) {
|
| +
|
| + if (hasKeyword(fields.keyword, Keyword.CONST) ||
|
| + hasKeyword(fields.keyword, Keyword.FINAL)) {
|
| + return;
|
| + }
|
| +
|
| + var indent = guessIndent(code.original, begin);
|
| + var replace = new StringBuffer();
|
| +
|
| + // Unfortunately "var" doesn't work in all positions where type annotations
|
| + // are allowed, such as "var get name". So we use "dynamic" instead.
|
| + var type = 'dynamic';
|
| + if (fields.type != null) {
|
| + type = getOriginalCode(code, fields.type);
|
| + }
|
| +
|
| + var mod = isStatic ? 'static ' : '';
|
| +
|
| + for (var field in fields.variables) {
|
| + var initializer = '';
|
| + if (field.initializer != null) {
|
| + initializer = ' = ${getOriginalCode(code, field.initializer)}';
|
| + }
|
| +
|
| + var name = field.name.name;
|
| +
|
| + if (replace.length > 0) replace.add('\n\n$indent');
|
| + replace.add('''
|
| +${mod}$type __\$$name$initializer;
|
| +${mod}Object __obs\$$name;
|
| +${mod}$type get $name {
|
| + if (autogenerated.observeReads) {
|
| + __obs\$$name = autogenerated.notifyRead(__obs\$$name);
|
| + }
|
| + return __\$$name;
|
| +}
|
| +${mod}set $name($type value) {
|
| + if (__obs\$$name != null && __\$$name != value) {
|
| + __obs\$$name = autogenerated.notifyWrite(__obs\$$name);
|
| + }
|
| + __\$$name = value;
|
| +}'''.replaceAll('\n', '\n$indent'));
|
| +
|
| + if (changedFields != null) changedFields.add(name);
|
| + }
|
| +
|
| + code.edit(begin, end, '$replace');
|
| +}
|
|
|