| Index: pkg/observe/lib/transform.dart
|
| diff --git a/pkg/observe/lib/transform.dart b/pkg/observe/lib/transform.dart
|
| index 5e261c02b49b641fe30f36867373bd9a9ca3a9c5..4be227f026e8136123b319905a5c327244d3e7e8 100644
|
| --- a/pkg/observe/lib/transform.dart
|
| +++ b/pkg/observe/lib/transform.dart
|
| @@ -3,413 +3,10 @@
|
| // 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.
|
| + * Code transform for @observable. This library will be removed soon, it simply
|
| + * reexports `observe.transformer`.
|
| */
|
| +// TODO(sigmund): deprecate and delete.
|
| library observe.transform;
|
|
|
| -import 'dart:async';
|
| -
|
| -import 'package:analyzer/src/generated/java_core.dart' show CharSequence;
|
| -import 'package:analyzer/src/generated/ast.dart';
|
| -import 'package:analyzer/src/generated/error.dart';
|
| -import 'package:analyzer/src/generated/parser.dart';
|
| -import 'package:analyzer/src/generated/scanner.dart';
|
| -import 'package:barback/barback.dart';
|
| -import 'package:source_maps/refactor.dart';
|
| -import 'package:source_maps/span.dart' show SourceFile;
|
| -
|
| -/**
|
| - * A [Transformer] that replaces observables based on dirty-checking with an
|
| - * implementation based on change notifications.
|
| - *
|
| - * The transformation adds hooks for field setters and notifies the observation
|
| - * system of the change.
|
| - */
|
| -class ObservableTransformer extends Transformer {
|
| -
|
| - Future<bool> isPrimary(Asset input) {
|
| - if (input.id.extension != '.dart') return new Future.value(false);
|
| - // Note: technically we should parse the file to find accurately the
|
| - // observable annotation, but that seems expensive. It would require almost
|
| - // as much work as applying the transform. We rather have some false
|
| - // positives here, and then generate no outputs when we apply this
|
| - // transform.
|
| - return input.readAsString().then(
|
| - (c) => c.contains("@observable") || c.contains("@published"));
|
| - }
|
| -
|
| - Future apply(Transform transform) {
|
| - return transform.primaryInput.readAsString().then((content) {
|
| - var id = transform.primaryInput.id;
|
| - // TODO(sigmund): improve how we compute this url
|
| - var url = id.path.startsWith('lib/')
|
| - ? 'package:${id.package}/${id.path.substring(4)}' : id.path;
|
| - var sourceFile = new SourceFile.text(url, content);
|
| - var transaction = _transformCompilationUnit(
|
| - content, sourceFile, transform.logger);
|
| - if (!transaction.hasEdits) {
|
| - transform.addOutput(transform.primaryInput);
|
| - return;
|
| - }
|
| - var printer = transaction.commit();
|
| - // TODO(sigmund): emit source maps when barback supports it (see
|
| - // dartbug.com/12340)
|
| - printer.build(url);
|
| - transform.addOutput(new Asset.fromString(id, printer.text));
|
| - });
|
| - }
|
| -}
|
| -
|
| -TextEditTransaction _transformCompilationUnit(
|
| - String inputCode, SourceFile sourceFile, TransformLogger logger) {
|
| - var unit = _parseCompilationUnit(inputCode);
|
| - var code = new TextEditTransaction(inputCode, sourceFile);
|
| - for (var directive in unit.directives) {
|
| - if (directive is LibraryDirective && _hasObservable(directive)) {
|
| - logger.warning('@observable on a library no longer has any effect. '
|
| - 'It should be placed on individual fields.',
|
| - span: _getSpan(sourceFile, directive));
|
| - break;
|
| - }
|
| - }
|
| -
|
| - for (var declaration in unit.declarations) {
|
| - if (declaration is ClassDeclaration) {
|
| - _transformClass(declaration, code, sourceFile, logger);
|
| - } else if (declaration is TopLevelVariableDeclaration) {
|
| - if (_hasObservable(declaration)) {
|
| - logger.warning('Top-level fields can no longer be observable. '
|
| - 'Observable fields should be put in an observable objects.',
|
| - span: _getSpan(sourceFile, declaration));
|
| - }
|
| - }
|
| - }
|
| - return code;
|
| -}
|
| -
|
| -/** Parse [code] using analyzer. */
|
| -CompilationUnit _parseCompilationUnit(String code) {
|
| - var errorListener = new _ErrorCollector();
|
| - var reader = new CharSequenceReader(new CharSequence(code));
|
| - var scanner = new Scanner(null, reader, errorListener);
|
| - var token = scanner.tokenize();
|
| - var parser = new Parser(null, errorListener);
|
| - return parser.parseCompilationUnit(token);
|
| -}
|
| -
|
| -class _ErrorCollector extends AnalysisErrorListener {
|
| - final errors = <AnalysisError>[];
|
| - onError(error) => errors.add(error);
|
| -}
|
| -
|
| -_getSpan(SourceFile file, ASTNode node) => file.span(node.offset, node.end);
|
| -
|
| -/** True if the node has the `@observable` or `@published` annotation. */
|
| -// TODO(jmesserly): it is not good to be hard coding Polymer support here.
|
| -bool _hasObservable(AnnotatedNode node) =>
|
| - node.metadata.any(_isObservableAnnotation);
|
| -
|
| -// TODO(jmesserly): this isn't correct if the annotation has been imported
|
| -// with a prefix, or cases like that. We should technically be resolving, but
|
| -// that is expensive in analyzer, so it isn't feasible yet.
|
| -bool _isObservableAnnotation(Annotation node) =>
|
| - _isAnnotationContant(node, 'observable') ||
|
| - _isAnnotationContant(node, 'published') ||
|
| - _isAnnotationType(node, 'ObservableProperty') ||
|
| - _isAnnotationType(node, 'PublishedProperty');
|
| -
|
| -bool _isAnnotationContant(Annotation m, String name) =>
|
| - m.name.name == name && m.constructorName == null && m.arguments == null;
|
| -
|
| -bool _isAnnotationType(Annotation m, String name) => m.name == name;
|
| -
|
| -void _transformClass(ClassDeclaration cls, TextEditTransaction code,
|
| - SourceFile file, TransformLogger logger) {
|
| -
|
| - if (_hasObservable(cls)) {
|
| - logger.warning('@observable on a class no longer has any effect. '
|
| - 'It should be placed on individual fields.',
|
| - span: _getSpan(file, cls));
|
| - }
|
| -
|
| - // We'd like to track whether observable was declared explicitly, otherwise
|
| - // report a warning later below. Because we don't have type analysis (only
|
| - // syntactic understanding of the code), we only report warnings that are
|
| - // known to be true.
|
| - var explicitObservable = false;
|
| - var implicitObservable = false;
|
| - if (cls.extendsClause != null) {
|
| - var id = _getSimpleIdentifier(cls.extendsClause.superclass.name);
|
| - if (id.name == 'Observable') {
|
| - code.edit(id.offset, id.end, 'ChangeNotifier');
|
| - explicitObservable = true;
|
| - } else if (id.name == 'ChangeNotifier') {
|
| - explicitObservable = true;
|
| - } else if (id.name != 'HtmlElement' && id.name != 'CustomElement'
|
| - && id.name != 'Object') {
|
| - // TODO(sigmund): this is conservative, consider using type-resolution to
|
| - // improve this check.
|
| - implicitObservable = true;
|
| - }
|
| - }
|
| -
|
| - if (cls.withClause != null) {
|
| - for (var type in cls.withClause.mixinTypes) {
|
| - var id = _getSimpleIdentifier(type.name);
|
| - if (id.name == 'Observable') {
|
| - code.edit(id.offset, id.end, 'ChangeNotifier');
|
| - explicitObservable = true;
|
| - break;
|
| - } else if (id.name == 'ChangeNotifier') {
|
| - explicitObservable = true;
|
| - break;
|
| - } else {
|
| - // TODO(sigmund): this is conservative, consider using type-resolution
|
| - // to improve this check.
|
| - implicitObservable = true;
|
| - }
|
| - }
|
| - }
|
| -
|
| - if (cls.implementsClause != null) {
|
| - // TODO(sigmund): consider adding type-resolution to give a more precise
|
| - // answer.
|
| - implicitObservable = true;
|
| - }
|
| -
|
| - var declaresObservable = explicitObservable || implicitObservable;
|
| -
|
| - // Track fields that were transformed.
|
| - var instanceFields = new Set<String>();
|
| - var getters = new List<String>();
|
| - var setters = new List<String>();
|
| -
|
| - for (var member in cls.members) {
|
| - if (member is FieldDeclaration) {
|
| - if (member.isStatic) {
|
| - if (_hasObservable(member)){
|
| - logger.warning('Static fields can no longer be observable. '
|
| - 'Observable fields should be put in an observable objects.',
|
| - span: _getSpan(file, member));
|
| - }
|
| - continue;
|
| - }
|
| - if (_hasObservable(member)) {
|
| - if (!declaresObservable) {
|
| - logger.warning('Observable fields should be put in an observable '
|
| - 'objects. Please declare that this class extends from '
|
| - 'Observable, includes Observable, or implements '
|
| - 'Observable.',
|
| - span: _getSpan(file, member));
|
| - }
|
| - _transformFields(file, member, code, logger);
|
| -
|
| - var names = member.fields.variables.map((v) => v.name.name);
|
| -
|
| - getters.addAll(names);
|
| - if (!_isReadOnly(member.fields)) {
|
| - setters.addAll(names);
|
| - instanceFields.addAll(names);
|
| - }
|
| - }
|
| - }
|
| - // TODO(jmesserly): this is a temporary workaround until we can remove
|
| - // getValueWorkaround and setValueWorkaround.
|
| - if (member is MethodDeclaration) {
|
| - if (_hasKeyword(member.propertyKeyword, Keyword.GET)) {
|
| - getters.add(member.name.name);
|
| - } else if (_hasKeyword(member.propertyKeyword, Keyword.SET)) {
|
| - setters.add(member.name.name);
|
| - }
|
| - }
|
| - }
|
| -
|
| - // If nothing was @observable, bail.
|
| - if (instanceFields.length == 0) return;
|
| -
|
| - if (!explicitObservable) _mixinObservable(cls, code);
|
| -
|
| - // Fix initializers, because they aren't allowed to call the setter.
|
| - for (var member in cls.members) {
|
| - if (member is ConstructorDeclaration) {
|
| - _fixConstructor(member, code, instanceFields);
|
| - }
|
| - }
|
| -}
|
| -
|
| -/** Adds "with ChangeNotifier" and associated implementation. */
|
| -void _mixinObservable(ClassDeclaration cls, TextEditTransaction code) {
|
| - // Note: we need to be careful to put the with clause after extends, but
|
| - // before implements clause.
|
| - if (cls.withClause != null) {
|
| - var pos = cls.withClause.end;
|
| - code.edit(pos, pos, ', ChangeNotifier');
|
| - } else if (cls.extendsClause != null) {
|
| - var pos = cls.extendsClause.end;
|
| - code.edit(pos, pos, ' with ChangeNotifier ');
|
| - } else {
|
| - var params = cls.typeParameters;
|
| - var pos = params != null ? params.end : cls.name.end;
|
| - code.edit(pos, pos, ' extends ChangeNotifier ');
|
| - }
|
| -}
|
| -
|
| -SimpleIdentifier _getSimpleIdentifier(Identifier id) =>
|
| - id is PrefixedIdentifier ? id.identifier : id;
|
| -
|
| -
|
| -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 _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 DefaultFormalParameter) {
|
| - param = param.parameter;
|
| - }
|
| - 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);
|
| -}
|
| -
|
| -bool _isReadOnly(VariableDeclarationList fields) {
|
| - return _hasKeyword(fields.keyword, Keyword.CONST) ||
|
| - _hasKeyword(fields.keyword, Keyword.FINAL);
|
| -}
|
| -
|
| -void _transformFields(SourceFile file, FieldDeclaration member,
|
| - TextEditTransaction code, TransformLogger logger) {
|
| -
|
| - final fields = member.fields;
|
| - if (_isReadOnly(fields)) return;
|
| -
|
| - // Private fields aren't supported:
|
| - for (var field in fields.variables) {
|
| - final name = field.name.name;
|
| - if (Identifier.isPrivateName(name)) {
|
| - logger.warning('Cannot make private field $name observable.',
|
| - span: _getSpan(file, field));
|
| - return;
|
| - }
|
| - }
|
| -
|
| - // 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);
|
| - } else if (_hasKeyword(fields.keyword, Keyword.VAR)) {
|
| - // Replace 'var' with 'dynamic'
|
| - code.edit(fields.keyword.offset, fields.keyword.end, type);
|
| - }
|
| -
|
| - // Note: the replacements here are a bit subtle. It needs to support multiple
|
| - // fields declared via the same @observable, as well as preserving newlines.
|
| - // (Preserving newlines is important because it allows the generated code to
|
| - // be debugged without needing a source map.)
|
| - //
|
| - // For example:
|
| - //
|
| - // @observable
|
| - // @otherMetaData
|
| - // Foo
|
| - // foo = 1, bar = 2,
|
| - // baz;
|
| - //
|
| - // Will be transformed into something like:
|
| - //
|
| - // @reflectable @observable
|
| - // @OtherMetaData()
|
| - // Foo
|
| - // get foo => __foo; Foo __foo = 1; @reflectable set foo ...; ...
|
| - // @observable @OtherMetaData() Foo get baz => __baz; Foo baz; ...
|
| - //
|
| - // Metadata is moved to the getter.
|
| -
|
| - String metadata = '';
|
| - if (fields.variables.length > 1) {
|
| - metadata = member.metadata
|
| - .map((m) => _getOriginalCode(code, m))
|
| - .join(' ');
|
| - metadata = '@reflectable $metadata';
|
| - }
|
| -
|
| - for (int i = 0; i < fields.variables.length; i++) {
|
| - final field = fields.variables[i];
|
| - final name = field.name.name;
|
| -
|
| - var beforeInit = 'get $name => __\$$name; $type __\$$name';
|
| -
|
| - // The first field is expanded differently from subsequent fields, because
|
| - // we can reuse the metadata and type annotation.
|
| - if (i == 0) {
|
| - final begin = member.metadata.first.offset;
|
| - code.edit(begin, begin, '@reflectable ');
|
| - } else {
|
| - beforeInit = '$metadata $type $beforeInit';
|
| - }
|
| -
|
| - code.edit(field.name.offset, field.name.end, beforeInit);
|
| -
|
| - // Replace comma with semicolon
|
| - final end = _findFieldSeperator(field.endToken.next);
|
| - if (end.type == TokenType.COMMA) code.edit(end.offset, end.end, ';');
|
| -
|
| - code.edit(end.end, end.end, ' @reflectable set $name($type value) { '
|
| - '__\$$name = notifyPropertyChange(#$name, __\$$name, value); }');
|
| - }
|
| -}
|
| -
|
| -Token _findFieldSeperator(Token token) {
|
| - while (token != null) {
|
| - if (token.type == TokenType.COMMA || token.type == TokenType.SEMICOLON) {
|
| - break;
|
| - }
|
| - token = token.next;
|
| - }
|
| - return token;
|
| -}
|
| +export 'transformer.dart';
|
|
|