Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1)

Unified Diff: observatory_pub_packages/observe/transformer.dart

Issue 816693004: Add observatory_pub_packages snapshot to third_party (Closed) Base URL: http://dart.googlecode.com/svn/third_party/
Patch Set: Created 6 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « observatory_pub_packages/observe/src/to_observable.dart ('k') | observatory_pub_packages/path/path.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: observatory_pub_packages/observe/transformer.dart
===================================================================
--- observatory_pub_packages/observe/transformer.dart (revision 0)
+++ observatory_pub_packages/observe/transformer.dart (working copy)
@@ -0,0 +1,413 @@
+// Copyright (c) 2013, 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.
+library observe.transformer;
+
+import 'dart:async';
+
+import 'package:analyzer/analyzer.dart';
+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:code_transformers/messages/build_logger.dart';
+import 'package:source_maps/refactor.dart';
+import 'package:source_span/source_span.dart';
+
+import 'src/messages.dart';
+
+/// 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 {
+
+ final bool releaseMode;
+ final List<String> _files;
+ ObservableTransformer([List<String> files, bool releaseMode])
+ : _files = files, releaseMode = releaseMode == true;
+ ObservableTransformer.asPlugin(BarbackSettings settings)
+ : _files = _readFiles(settings.configuration['files']),
+ releaseMode = settings.mode == BarbackMode.RELEASE;
+
+ static List<String> _readFiles(value) {
+ if (value == null) return null;
+ var files = [];
+ bool error;
+ if (value is List) {
+ files = value;
+ error = value.any((e) => e is! String);
+ } else if (value is String) {
+ files = [value];
+ error = false;
+ } else {
+ error = true;
+ }
+ if (error) print('Invalid value for "files" in the observe transformer.');
+ return files;
+ }
+
+ // TODO(nweiz): This should just take an AssetId when barback <0.13.0 support
+ // is dropped.
+ Future<bool> isPrimary(idOrAsset) {
+ var id = idOrAsset is AssetId ? idOrAsset : idOrAsset.id;
+ return new Future.value(id.extension == '.dart' &&
+ (_files == null || _files.contains(id.path)));
+ }
+
+ Future apply(Transform transform) {
+ return transform.primaryInput.readAsString().then((content) {
+ // Do a quick string check to determine if this is this file even
+ // plausibly might need to be transformed. If not, we can avoid an
+ // expensive parse.
+ if (!observableMatcher.hasMatch(content)) return null;
+
+ 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(content, url: url);
+ var logger = new BuildLogger(transform,
+ convertErrorsToWarnings: !releaseMode,
+ detailsUri: 'http://goo.gl/5HPeuP');
+ var transaction = _transformCompilationUnit(
+ content, sourceFile, logger);
+ if (!transaction.hasEdits) {
+ transform.addOutput(transform.primaryInput);
+ } else {
+ 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));
+ }
+ return logger.writeOutput();
+ });
+ }
+}
+
+TextEditTransaction _transformCompilationUnit(
+ String inputCode, SourceFile sourceFile, BuildLogger logger) {
+ var unit = parseCompilationUnit(inputCode, suppressErrors: true);
+ var code = new TextEditTransaction(inputCode, sourceFile);
+ for (var directive in unit.directives) {
+ if (directive is LibraryDirective && _hasObservable(directive)) {
+ logger.warning(NO_OBSERVABLE_ON_LIBRARY,
+ 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(NO_OBSERVABLE_ON_TOP_LEVEL,
+ span: _getSpan(sourceFile, declaration));
+ }
+ }
+ }
+ return code;
+}
+
+_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 == name;
+
+void _transformClass(ClassDeclaration cls, TextEditTransaction code,
+ SourceFile file, BuildLogger logger) {
+
+ if (_hasObservable(cls)) {
+ logger.warning(NO_OBSERVABLE_ON_CLASS, 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>();
+
+ for (var member in cls.members) {
+ if (member is FieldDeclaration) {
+ if (member.isStatic) {
+ if (_hasObservable(member)){
+ logger.warning(NO_OBSERVABLE_ON_STATIC_FIELD,
+ span: _getSpan(file, member));
+ }
+ continue;
+ }
+ if (_hasObservable(member)) {
+ if (!declaresObservable) {
+ logger.warning(REQUIRE_OBSERVABLE_INTERFACE,
+ span: _getSpan(file, member));
+ }
+ _transformFields(file, member, code, logger);
+
+ var names = member.fields.variables.map((v) => v.name.name);
+
+ if (!_isReadOnly(member.fields)) instanceFields.addAll(names);
+ }
+ }
+ }
+
+ // 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, BuildLogger 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;
+}
+
+// TODO(sigmund): remove hard coded Polymer support (@published). The proper way
+// to do this would be to switch to use the analyzer to resolve whether
+// annotations are subtypes of ObservableProperty.
+final observableMatcher =
+ new RegExp("@(published|observable|PublishedProperty|ObservableProperty)");
« no previous file with comments | « observatory_pub_packages/observe/src/to_observable.dart ('k') | observatory_pub_packages/path/path.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698