Major refactor of the transformer, added an `InitializePlugin` class which allows you to hook directly into the transformer and do custom actions at compile time.
R=sigmund@google.com
Review URL: https://codereview.chromium.org//923733002
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fcde62e..b1e5dc5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 0.5.0-dev
+
+* The `InitializePluginTransformer` is gone in favor of a new
+`InitializerPlugin` class which you can pass a list of to the
+`InitializeTransformer`. These plugins now have access to the fully resolved ast
+nodes and can directly control what is output in the bootstrap file.
+
## 0.4.0
Lots of transformer updates:
diff --git a/README.md b/README.md
index 99c8cec..c727811 100644
--- a/README.md
+++ b/README.md
@@ -91,3 +91,15 @@
Now when people use the annotation, it just looks like `@initMethod` without any
parenthesis, and its a bit more efficient since there is a single instance. You
can also make your class private to force users into using the static instance.
+
+## Creating custom transformer plugins
+
+It is possible to create a custom plugin for the initialize transformer which
+allows you to have full control over what happens to your annotations at compile
+time. Implement `InitializerPlugin` class and pass that in to the
+`InitializeTransformer` to make it take effect.
+
+You will need to be familiar with the `analyzer` package in order to write these
+plugins, but they can be extremely powerful. See the `DefaultInitializerPlugin`
+in `lib/build/initializer_plugin.dart` as a reference. Chances are you may want
+to extend that class in order to get a lot of the default functionality.
diff --git a/lib/build/initializer_plugin.dart b/lib/build/initializer_plugin.dart
new file mode 100644
index 0000000..035161e
--- /dev/null
+++ b/lib/build/initializer_plugin.dart
@@ -0,0 +1,265 @@
+// Copyright (c) 2015, 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.
+library initialize.build.initializer_plugin;
+
+import 'package:analyzer/src/generated/ast.dart';
+import 'package:analyzer/src/generated/element.dart';
+import 'package:barback/barback.dart';
+import 'package:code_transformers/resolver.dart';
+import 'package:initialize/transformer.dart';
+import 'package:path/path.dart' as path;
+
+/// A plug which allows an initializer to write out an [InitEntry] given some
+/// [InitializerData] from an annotation that was found.
+abstract class InitializerPlugin {
+ /// Whether or not this plugin should be applied to an [Initializer] given
+ /// some [InitializerData]. If [true] is returned then this plugin will take
+ /// ownership of this [InitializerData] and no subsequent plugins will have
+ /// an opportunity to access it.
+ bool shouldApply(InitializerPluginData data);
+
+ /// Returns a [String] or [null]. The [String] should represent dart code
+ /// which creates a new [InitEntry] and that entry is added to the static
+ /// initializers list. If [null] is returned then no entry is added at all for
+ /// this [InitializerData].
+ String apply(InitializerPluginData data);
+}
+
+/// A class which wraps all the default data passed to an [InitializerPlugin]
+/// for each annotation.
+class InitializerPluginData {
+ final InitializerData initializer;
+ final AssetId bootstrapId;
+ final Map<LibraryElement, String> libraryPrefixes;
+ final TransformLogger logger;
+ final Resolver resolver;
+ InitializerPluginData(this.initializer, this.bootstrapId,
+ this.libraryPrefixes, this.resolver, this.logger);
+}
+
+/// The basic [InitializerPlugin]. This generates a new [InitEntry] to be added
+/// to the static initializers list, and applies to every item it sees.
+class DefaultInitializerPlugin implements InitializerPlugin {
+ const DefaultInitializerPlugin();
+
+ /// Applies to everything. Put other plugins before this one to override this
+ /// behaviour.
+ bool shouldApply(InitializerPluginData data) => true;
+
+ /// Creates a normal [InitEntry] string.
+ String apply(InitializerPluginData pluginData) {
+ var target = buildTarget(pluginData);
+ var meta = buildMeta(pluginData);
+ return 'new InitEntry($meta, $target)';
+ }
+
+ /// Builds a [String] representing the meta of an [InitEntry] given an
+ /// [ElementAnnotation] that was found.
+ String buildMeta(InitializerPluginData pluginData) {
+ var logger = pluginData.logger;
+ var element = pluginData.initializer.targetElement;
+ var elementAnnotation = pluginData.initializer.annotationElement;
+ var elementAnnotationElement = elementAnnotation.element;
+ var libraryPrefixes = pluginData.libraryPrefixes;
+ if (elementAnnotationElement is ConstructorElement) {
+ return buildConstructorMeta(elementAnnotation, pluginData);
+ } else if (elementAnnotationElement is PropertyAccessorElement) {
+ return buildPropertyMeta(elementAnnotation, pluginData);
+ } else {
+ logger.error('Unsupported annotation type. Only constructors and '
+ 'properties are supported as initializers.');
+ }
+ return null;
+ }
+
+ /// Builds a [String] representing the meta of an [InitEntry] given an
+ /// [ElementAnnotation] whose element was a [ConstructorElement].
+ String buildConstructorMeta(
+ ElementAnnotation elementAnnotation, InitializerPluginData pluginData) {
+ var logger = pluginData.logger;
+ var node = pluginData.initializer.targetNode;
+ var metaPrefix =
+ pluginData.libraryPrefixes[elementAnnotation.element.library];
+
+ var annotation = pluginData.initializer.annotationNode;
+ if (annotation == null) {
+ logger.error(
+ 'Initializer annotations are only supported on libraries, classes, '
+ 'and top level methods. Found $node.');
+ }
+ var clazz = annotation.name;
+ var constructor = annotation.constructorName == null
+ ? ''
+ : '.${annotation.constructorName}';
+ // TODO(jakemac): Support more than raw values here
+ // https://github.com/dart-lang/static_init/issues/5
+ var args = buildArgumentList(annotation.arguments, pluginData);
+ return 'const $metaPrefix.${clazz}$constructor$args';
+ }
+
+ /// Builds a [String] representing the meta of an [InitEntry] given an
+ /// [ElementAnnotation] whose element was a [PropertyAccessorElement].
+ String buildPropertyMeta(
+ ElementAnnotation annotation, InitializerPluginData pluginData) {
+ var metaPrefix = pluginData.libraryPrefixes[annotation.element.library];
+ return '$metaPrefix.${annotation.element.name}';
+ }
+
+ /// Builds a [String] for the target of an [InitEntry] given an [Element] that
+ /// was annotated.
+ String buildTarget(InitializerPluginData pluginData) {
+ var element = pluginData.initializer.targetElement;
+ var logger = pluginData.logger;
+ if (element is LibraryElement) {
+ return buildLibraryTarget(element, pluginData);
+ } else if (element is ClassElement) {
+ return buildClassTarget(element, pluginData);
+ } else if (element is FunctionElement) {
+ return buildFunctionTarget(element, pluginData);
+ } else {
+ logger.error('Initializers can only be applied to top level functions, '
+ 'libraries, and classes.');
+ }
+ return null;
+ }
+
+ /// Builds a [String] for the target of an [InitEntry] given [element] which
+ /// is an annotated class.
+ String buildClassTarget(
+ ClassElement element, InitializerPluginData pluginData) =>
+ buildSimpleTarget(element, pluginData);
+
+ /// Builds a [String] for the target of an [InitEntry] given [element] which
+ /// is an annotated function.
+ String buildFunctionTarget(
+ FunctionElement element, InitializerPluginData pluginData) =>
+ buildSimpleTarget(element, pluginData);
+
+ /// Builds a [String] for the target of an [InitEntry] for a simple [Element].
+ /// This is just the library prefix followed by the element name.
+ String buildSimpleTarget(Element element, InitializerPluginData pluginData) =>
+ '${pluginData.libraryPrefixes[element.library]}.${element.name}';
+
+ /// Builds a [String] for the target of an [InitEntry] given [element] which
+ /// is an annotated library.
+ String buildLibraryTarget(
+ LibraryElement element, InitializerPluginData pluginData) {
+ var bootstrapId = pluginData.bootstrapId;
+ var logger = pluginData.logger;
+ var segments = element.source.uri.pathSegments;
+ var package = segments[0];
+ var libraryPath;
+ var packageString;
+ if (bootstrapId.package == package &&
+ bootstrapId.path.startsWith('${segments[1]}/')) {
+ // reset `package` to null, we will do a relative path in this case.
+ packageString = 'null';
+ libraryPath = path.url.relative(
+ path.url.joinAll(segments.getRange(1, segments.length)),
+ from: path.url.dirname(path.url.join(bootstrapId.path)));
+ } else if (segments[1] == 'lib') {
+ packageString = "'$package'";
+ libraryPath = path.url.joinAll(segments.getRange(2, segments.length));
+ } else {
+ logger.error('Unable to import `${element.source.uri.path}` from '
+ '${bootstrapId.path}.');
+ }
+
+ return "const LibraryIdentifier"
+ "(#${element.name}, $packageString, '$libraryPath')";
+ }
+
+ /// Builds a [String] representing an [ArgumentList] taking into account the
+ /// [libraryPrefixes] from [pluginData].
+ String buildArgumentList(
+ ArgumentList args, InitializerPluginData pluginData) {
+ var buffer = new StringBuffer();
+ buffer.write('(');
+ var first = true;
+ for (var arg in args.arguments) {
+ if (!first) buffer.write(', ');
+ first = false;
+
+ Expression expression;
+ if (arg is NamedExpression) {
+ buffer.write('${arg.name.label.name}: ');
+ expression = arg.expression;
+ } else {
+ expression = arg;
+ }
+
+ buffer.write(buildExpression(expression, pluginData));
+ }
+ buffer.write(')');
+ return buffer.toString();
+ }
+
+ /// Builds a [String] representing [expression] taking into account the
+ /// [libraryPrefixes] from [pluginData].
+ String buildExpression(
+ Expression expression, InitializerPluginData pluginData) {
+ var logger = pluginData.logger;
+ var libraryPrefixes = pluginData.libraryPrefixes;
+ var buffer = new StringBuffer();
+ if (expression is StringLiteral) {
+ var value = expression.stringValue;
+ if (value == null) {
+ logger.error('Only const strings are allowed in initializer '
+ 'expressions, found $expression');
+ }
+ value = value.replaceAll(r'\', r'\\').replaceAll(r"'", r"\'");
+ buffer.write("'$value'");
+ } else if (expression is BooleanLiteral ||
+ expression is DoubleLiteral ||
+ expression is IntegerLiteral ||
+ expression is NullLiteral) {
+ buffer.write('${expression}');
+ } else if (expression is ListLiteral) {
+ buffer.write('const [');
+ var first = true;
+ for (Expression listExpression in expression.elements) {
+ if (!first) buffer.write(', ');
+ first = false;
+ buffer.write(buildExpression(listExpression, pluginData));
+ }
+ buffer.write(']');
+ } else if (expression is MapLiteral) {
+ buffer.write('const {');
+ var first = true;
+ for (MapLiteralEntry entry in expression.entries) {
+ if (!first) buffer.write(', ');
+ first = false;
+ buffer.write(buildExpression(entry.key, pluginData));
+ buffer.write(': ');
+ buffer.write(buildExpression(entry.value, pluginData));
+ }
+ buffer.write('}');
+ } else if (expression is Identifier) {
+ var element = expression.bestElement;
+ if (element == null || !element.isPublic) {
+ logger.error('Private constants are not supported in intializer '
+ 'constructors, found $element.');
+ }
+ libraryPrefixes.putIfAbsent(
+ element.library, () => 'i${libraryPrefixes.length}');
+
+ buffer.write('${libraryPrefixes[element.library]}.');
+ if (element is ClassElement) {
+ buffer.write(element.name);
+ } else if (element is PropertyAccessorElement) {
+ var variable = element.variable;
+ if (variable is FieldElement) {
+ buffer.write('${variable.enclosingElement.name}.');
+ }
+ buffer.write('${variable.name}');
+ } else {
+ logger.error('Unsupported argument to initializer constructor.');
+ }
+ } else {
+ logger.error('Only literals and identifiers are allowed for initializer '
+ 'expressions, found $expression.');
+ }
+ return buffer.toString();
+ }
+}
diff --git a/lib/plugin_transformer.dart b/lib/plugin_transformer.dart
deleted file mode 100644
index 5cfa9f5..0000000
--- a/lib/plugin_transformer.dart
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (c) 2015, 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.
-library initialize.plugin_transformer;
-
-import 'dart:async';
-import 'package:analyzer/analyzer.dart';
-import 'package:barback/barback.dart';
-import 'package:source_maps/refactor.dart';
-import 'package:source_span/source_span.dart';
-
-/// Abstract transformer which will call [initEntry] for every [InitEntry]
-/// expression found in the bootstrap file. This is used to centralize logic
-/// for initializers which want to do something special at transform time.
-abstract class InitializePluginTransformer extends AggregateTransformer {
- // Path to the bootstrap file created by the initialize transformer.
- final String _bootstrapFile;
-
- // All the extra assets that were found, if child classes override
- // classifyPrimary this lets them get at the other assets easily.
- final allAssets = <Asset>[];
-
- TransformLogger _logger;
-
- InitializePluginTransformer(this._bootstrapFile);
-
- classifyPrimary(AssetId id) =>
- _bootstrapFile == id.path ? _bootstrapFile : null;
-
- Future apply(AggregateTransform transform) {
- _logger = transform.logger;
- var done = new Completer();
- var listener = transform.primaryInputs.listen((Asset asset) {
- allAssets.add(asset);
- var id = asset.id;
- if (id.path != _bootstrapFile) return;
- // Calls initEntry for each InitEntry expression.
- asset.readAsString().then((dartCode) {
- var url = id.path.startsWith('lib/')
- ? 'package:${id.package}/${id.path.substring(4)}'
- : id.path;
- var source = new SourceFile(dartCode, url: url);
- var transaction = new TextEditTransaction(dartCode, source);
- (parseCompilationUnit(dartCode,
- suppressErrors: true) as dynamic).declarations.firstWhere(
- (d) => d.name.toString() ==
- 'main').functionExpression.body.block.statements.firstWhere(
- (statement) {
- return statement.expression.target.toString() == 'initializers' &&
- statement.expression.methodName.toString() == 'addAll';
- }).expression.argumentList.arguments[0].elements
- .where((arg) => arg is InstanceCreationExpression)
- .forEach((e) => initEntry(e, transaction));
-
- // Apply any transformations.
- if (!transaction.hasEdits) {
- transform.addOutput(asset);
- } else {
- var printer = transaction.commit();
- printer.build(url);
- transform.addOutput(new Asset.fromString(id, printer.text));
- }
- done.complete();
- });
- });
-
- // Make sure all the assets are read before returning.
- return Future.wait([listener.asFuture(), done.future]);
- }
-
- /// Gets called once for each generated [InitEntry] expression in the
- /// bootstrap file. A [TextEditTransaction] is supplied so that the user can
- /// modify the expression however they see fit.
- void initEntry(
- InstanceCreationExpression expression, TextEditTransaction transaction);
-
- /// Convenience method to delete an Initializer expression completely.
- void removeInitializer(
- InstanceCreationExpression expression, TextEditTransaction transaction) {
- // Delete the entire line.
- var line = transaction.file.getLine(expression.offset);
- transaction.edit(transaction.file.getOffset(line),
- transaction.file.getOffset(line + 1), '');
- }
-}
diff --git a/lib/transformer.dart b/lib/transformer.dart
index 8133ba9..390a2e2 100644
--- a/lib/transformer.dart
+++ b/lib/transformer.dart
@@ -16,14 +16,34 @@
import 'package:html5lib/parser.dart' show parse;
import 'package:path/path.dart' as path;
-/// Removes the mirror-based initialization logic and replaces it with static
-/// logic.
+import 'build/initializer_plugin.dart';
+export 'build/initializer_plugin.dart';
+
+/// Create a new [Asset] which inlines your [Initializer] annotations into
+/// a new file that bootstraps your application.
+Asset generateBootstrapFile(Resolver resolver, Transform transform,
+ AssetId primaryAssetId, AssetId newEntryPointId,
+ {bool errorIfNotFound: true, List<InitializerPlugin> plugins,
+ bool appendDefaultPlugin: true}) {
+ if (appendDefaultPlugin) {
+ if (plugins == null) plugins = [];
+ plugins.add(const DefaultInitializerPlugin());
+ }
+ return new _BootstrapFileBuilder(
+ resolver, transform, primaryAssetId, newEntryPointId, errorIfNotFound,
+ plugins: plugins).run();
+}
+
+/// Transformer which removes the mirror-based initialization logic and replaces
+/// it with static logic.
class InitializeTransformer extends Transformer {
final Resolvers _resolvers;
final Iterable<Glob> _entryPointGlobs;
final bool _errorIfNotFound;
+ final List<InitializerPlugin> plugins;
- InitializeTransformer(List<String> entryPoints, {bool errorIfNotFound: true})
+ InitializeTransformer(List<String> entryPoints,
+ {bool errorIfNotFound: true, this.plugins})
: _entryPointGlobs = entryPoints.map((e) => new Glob(e)),
_errorIfNotFound = errorIfNotFound,
_resolvers = new Resolvers.fromMock({
@@ -107,24 +127,50 @@
}
return _resolvers.get(transform, [primaryId]).then((resolver) {
- new _BootstrapFileBuilder(resolver, transform, primaryId,
- newEntryPointId, _errorIfNotFound).run();
+ transform.addOutput(generateBootstrapFile(
+ resolver, transform, primaryId, newEntryPointId,
+ errorIfNotFound: _errorIfNotFound, plugins: plugins));
resolver.release();
return newEntryPointId;
});
});
}
+ // Finds the first (and only) dart script on an html page and returns the
+ // [AssetId] that points to it
+ AssetId _findMainScript(
+ dom.Document document, AssetId entryPoint, Transform transform) {
+ var scripts = document.querySelectorAll('script[type="application/dart"]');
+ if (scripts.length != 1) {
+ transform.logger.error('Expected exactly one dart script in $entryPoint '
+ 'but found ${scripts.length}.');
+ return null;
+ }
+
+ var src = scripts[0].attributes['src'];
+ if (src == null) {
+ // TODO(jakemac): Support inline scripts,
+ transform.logger.error('Inline scripts are not supported at this time, '
+ 'see https://github.com/dart-lang/initialize/issues/20.');
+ return null;
+ }
+
+ return uriToAssetId(
+ entryPoint, src, transform.logger, scripts[0].sourceSpan);
+ }
// Replaces script tags pointing to [originalDartFile] with [newDartFile] in
// [entryPoint].
void _replaceEntryWithBootstrap(Transform transform, dom.Document document,
AssetId entryPoint, AssetId originalDartFile, AssetId newDartFile) {
var found = false;
+
var scripts = document
.querySelectorAll('script[type="application/dart"]')
- .where((script) => uriToAssetId(entryPoint, script.attributes['src'],
- transform.logger, script.sourceSpan) == originalDartFile)
- .toList();
+ .where((script) {
+ var assetId = uriToAssetId(entryPoint, script.attributes['src'],
+ transform.logger, script.sourceSpan);
+ return assetId == originalDartFile;
+ }).toList();
if (scripts.length != 1) {
transform.logger.error(
@@ -136,29 +182,9 @@
from: path.dirname(entryPoint.path));
transform.addOutput(new Asset.fromString(entryPoint, document.outerHtml));
}
-
- AssetId _findMainScript(
- dom.Document document, AssetId entryPoint, Transform transform) {
- var scripts = document.querySelectorAll('script[type="application/dart"]');
- if (scripts.length != 1) {
- transform.logger.error('Expected exactly one dart script in $entryPoint '
- 'but found ${scripts.length}.');
- return null;
- }
-
- var src = scripts[0].attributes['src'];
- // TODO(jakemac): Support inline scripts,
- // https://github.com/dart-lang/initialize/issues/20
- if (src == null) {
- transform.logger.error('Inline scripts are not supported at this time.');
- return null;
- }
-
- return uriToAssetId(
- entryPoint, src, transform.logger, scripts[0].sourceSpan);
- }
}
+// Class which builds a bootstrap file.
class _BootstrapFileBuilder {
final Resolver _resolver;
final Transform _transform;
@@ -172,14 +198,19 @@
ClassElement _initializer;
/// Queue for intialization annotations.
- final _initQueue = new Queue<_InitializerData>();
+ final _initQueue = new Queue<InitializerData>();
/// All the annotations we have seen for each element
final _seenAnnotations = new Map<Element, Set<ElementAnnotation>>();
+ /// The list of [InitializerPlugin]s to apply. The first plugin which asks to
+ /// be applied to a given initializer is the only one that will apply.
+ List<InitializerPlugin> _plugins;
+
TransformLogger _logger;
_BootstrapFileBuilder(this._resolver, this._transform, this._entryPoint,
- this._newEntryPoint, this._errorIfNotFound) {
+ this._newEntryPoint, this._errorIfNotFound,
+ {List<InitializerPlugin> plugins}) {
_logger = _transform.logger;
_initializeLibrary =
_resolver.getLibrary(new AssetId('initialize', 'lib/initialize.dart'));
@@ -190,15 +221,15 @@
'This file must be imported via $_entryPoint or a transitive '
'dependency.');
}
+ _plugins = plugins != null ? plugins : [const DefaultInitializerPlugin()];
}
- /// Adds the new entry point file to the transform. Should only be ran once.
- void run() {
+ /// Creates and returns the new bootstrap file.
+ Asset run() {
var entryLib = _resolver.getLibrary(_entryPoint);
_readLibraries(entryLib);
- _transform.addOutput(
- new Asset.fromString(_newEntryPoint, _buildNewEntryPoint(entryLib)));
+ return new Asset.fromString(_newEntryPoint, _buildNewEntryPoint(entryLib));
}
/// Reads Initializer annotations on this library and all its dependencies in
@@ -253,7 +284,7 @@
return !_seenAnnotations[element].contains(meta);
}).forEach((ElementAnnotation meta) {
_seenAnnotations[element].add(meta);
- _initQueue.addLast(new _InitializerData(element, meta));
+ _initQueue.addLast(new InitializerData._(element, meta));
found = true;
});
return found;
@@ -270,18 +301,28 @@
importsBuffer.writeln("import 'package:initialize/initialize.dart';");
libraryPrefixes[entryLib] = 'i0';
- initializersBuffer.writeln(' initializers.addAll([');
+ initializersBuffer.writeln('initializers.addAll([');
while (_initQueue.isNotEmpty) {
var next = _initQueue.removeFirst();
libraryPrefixes.putIfAbsent(
- next.element.library, () => 'i${libraryPrefixes.length}');
- libraryPrefixes.putIfAbsent(
- next.annotation.element.library, () => 'i${libraryPrefixes.length}');
+ next.targetElement.library, () => 'i${libraryPrefixes.length}');
+ libraryPrefixes.putIfAbsent(next.annotationElement.element.library,
+ () => 'i${libraryPrefixes.length}');
- _writeInitializer(next, libraryPrefixes, initializersBuffer);
+ // Run the first plugin which asks to be ran and then stop.
+ var data = new InitializerPluginData(
+ next, _newEntryPoint, libraryPrefixes, _resolver, _logger);
+ var plugin = _plugins.firstWhere((p) => p.shouldApply(data), orElse: () {
+ _logger.error('No InitializerPlugin handled the annotation: '
+ '${next.annotationElement} on: ${next.targetElement}.');
+ });
+ if (plugin == null) continue;
+
+ var text = plugin.apply(data);
+ if (text != null) initializersBuffer.writeln('$text,');
}
- initializersBuffer.writeln(' ]);');
+ initializersBuffer.writeln(']);');
libraryPrefixes
.forEach((lib, prefix) => _writeImport(lib, prefix, importsBuffer));
@@ -315,164 +356,6 @@
buffer.writeln(' as $prefix;');
}
- _writeInitializer(_InitializerData data,
- Map<LibraryElement, String> libraryPrefixes, StringBuffer buffer) {
- final annotationElement = data.annotation.element;
- final element = data.element;
-
- final metaPrefix = libraryPrefixes[annotationElement.library];
- var elementString;
- if (element is LibraryElement) {
- var segments = element.source.uri.pathSegments;
- var package = segments[0];
- var libraryPath;
- var packageString;
- if (_newEntryPoint.package == package &&
- _newEntryPoint.path.startsWith('${segments[1]}/')) {
- // reset `package` to null, we will do a relative path in this case.
- packageString = 'null';
- libraryPath = path.url.relative(
- path.url.joinAll(segments.getRange(1, segments.length)),
- from: path.url.dirname(path.url.join(_newEntryPoint.path)));
- } else if (segments[1] == 'lib') {
- packageString = "'$package'";
- libraryPath = path.url.joinAll(segments.getRange(2, segments.length));
- } else {
- _logger.error('Unable to import `${element.source.uri.path}` from '
- '${_newEntryPoint.path}.');
- }
-
- elementString = "const LibraryIdentifier("
- "#${element.name}, $packageString, '$libraryPath')";
- } else if (element is ClassElement || element is FunctionElement) {
- elementString =
- '${libraryPrefixes[data.element.library]}.${element.name}';
- } else {
- _logger.error('Initializers can only be applied to top level functins, '
- 'libraries, and classes.');
- }
-
- if (annotationElement is ConstructorElement) {
- var node = data.element.node;
- List<Annotation> astMeta;
- if (node is SimpleIdentifier) {
- astMeta = node.parent.parent.metadata;
- } else if (node is ClassDeclaration || node is FunctionDeclaration) {
- astMeta = node.metadata;
- } else {
- _logger.error(
- 'Initializer annotations are only supported on libraries, classes, '
- 'and top level methods. Found $node.');
- }
- final annotation =
- astMeta.firstWhere((m) => m.elementAnnotation == data.annotation);
- final clazz = annotation.name;
- final constructor = annotation.constructorName == null
- ? ''
- : '.${annotation.constructorName}';
- // TODO(jakemac): Support more than raw values here
- // https://github.com/dart-lang/static_init/issues/5
- final args = _buildArgsString(annotation.arguments, libraryPrefixes);
- buffer.write('''
- new InitEntry(const $metaPrefix.${clazz}$constructor$args, $elementString),
-''');
- } else if (annotationElement is PropertyAccessorElement) {
- buffer.write('''
- new InitEntry($metaPrefix.${annotationElement.name}, $elementString),
-''');
- } else {
- _logger.error('Unsupported annotation type. Only constructors and '
- 'properties are supported as initializers.');
- }
- }
-
- String _buildArgsString(
- ArgumentList args, Map<LibraryElement, String> libraryPrefixes) {
- var buffer = new StringBuffer();
- buffer.write('(');
- var first = true;
- for (var arg in args.arguments) {
- if (!first) buffer.write(', ');
- first = false;
-
- Expression expression;
- if (arg is NamedExpression) {
- buffer.write('${arg.name.label.name}: ');
- expression = arg.expression;
- } else {
- expression = arg;
- }
-
- buffer.write(_expressionString(expression, libraryPrefixes));
- }
- buffer.write(')');
- return buffer.toString();
- }
-
- String _expressionString(
- Expression expression, Map<LibraryElement, String> libraryPrefixes) {
- var buffer = new StringBuffer();
- if (expression is StringLiteral) {
- var value = expression.stringValue;
- if (value == null) {
- _logger.error('Only const strings are allowed in initializer '
- 'expressions, found $expression');
- }
- value = value.replaceAll(r'\', r'\\').replaceAll(r"'", r"\'");
- buffer.write("'$value'");
- } else if (expression is BooleanLiteral ||
- expression is DoubleLiteral ||
- expression is IntegerLiteral ||
- expression is NullLiteral) {
- buffer.write('${expression}');
- } else if (expression is ListLiteral) {
- buffer.write('const [');
- var first = true;
- for (Expression listExpression in expression.elements) {
- if (!first) buffer.write(', ');
- first = false;
- buffer.write(_expressionString(listExpression, libraryPrefixes));
- }
- buffer.write(']');
- } else if (expression is MapLiteral) {
- buffer.write('const {');
- var first = true;
- for (MapLiteralEntry entry in expression.entries) {
- if (!first) buffer.write(', ');
- first = false;
- buffer.write(_expressionString(entry.key, libraryPrefixes));
- buffer.write(': ');
- buffer.write(_expressionString(entry.value, libraryPrefixes));
- }
- buffer.write('}');
- } else if (expression is Identifier) {
- var element = expression.bestElement;
- if (element == null || !element.isPublic) {
- _logger.error('Private constants are not supported in intializer '
- 'constructors, found $element.');
- }
- libraryPrefixes.putIfAbsent(
- element.library, () => 'i${libraryPrefixes.length}');
-
- buffer.write('${libraryPrefixes[element.library]}.');
- if (element is ClassElement) {
- buffer.write(element.name);
- } else if (element is PropertyAccessorElement) {
- var variable = element.variable;
- if (variable is FieldElement) {
- buffer.write('${variable.enclosingElement.name}.');
- }
- buffer.write('${variable.name}');
- } else {
- _logger.error('Unsupported argument to initializer constructor.');
- }
- } else {
- _logger.error('Only literals and identifiers are allowed for initializer '
- 'expressions, found $expression.');
- }
- return buffer.toString();
- }
-
bool _isInitializer(InterfaceType type) {
// If `_initializer` wasn't found then it was never loaded (even
// transitively), and so no annotations can be initializers.
@@ -562,12 +445,43 @@
})).map((import) => import.importedLibrary);
}
-// Element/ElementAnnotation pair.
-class _InitializerData {
- final Element element;
- final ElementAnnotation annotation;
+/// An [Initializer] annotation and the target of that annotation.
+class InitializerData {
+ /// The target [Element] of the annotation.
+ final Element targetElement;
- _InitializerData(this.element, this.annotation);
+ /// The [ElementAnnotation] representing the annotation itself.
+ final ElementAnnotation annotationElement;
+
+ AstNode _targetNode;
+
+ /// The target [AstNode] of the annotation.
+ // TODO(jakemac): We at least cache this for now, but ideally `targetElement`
+ // would actually be the getter, and `targetNode` would be the property.
+ AstNode get targetNode {
+ if (_targetNode == null) _targetNode = targetElement.node;
+ return _targetNode;
+ }
+
+ /// The [Annotation] representing the annotation itself.
+ Annotation get annotationNode {
+ var annotatedNode;
+ if (targetNode is SimpleIdentifier &&
+ targetNode.parent is LibraryIdentifier) {
+ annotatedNode = targetNode.parent.parent;
+ } else if (targetNode is ClassDeclaration ||
+ targetNode is FunctionDeclaration) {
+ annotatedNode = targetNode;
+ } else {
+ return null;
+ }
+ if (annotatedNode is! AnnotatedNode) return null;
+ var astMeta = annotatedNode.metadata;
+
+ return astMeta.firstWhere((m) => m.elementAnnotation == annotationElement);
+ }
+
+ InitializerData._(this.targetElement, this.annotationElement);
}
// Reads a file list from a barback settings configuration field.
diff --git a/pubspec.yaml b/pubspec.yaml
index 1ad78f1..f2de830 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: initialize
-version: 0.4.0
+version: 0.5.0-dev
author: Polymer.dart Authors <web@dartlang.org>
description: Generic building blocks for doing static initialization.
homepage: https://github.com/dart-lang/initialize
diff --git a/test/plugin_transformer_test.dart b/test/plugin_transformer_test.dart
deleted file mode 100644
index df2a6e4..0000000
--- a/test/plugin_transformer_test.dart
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (c) 2015, 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.
-library initialize.plugin_transformer_test;
-
-import 'package:analyzer/analyzer.dart';
-import 'package:initialize/plugin_transformer.dart';
-import 'package:source_maps/refactor.dart';
-import 'package:unittest/compact_vm_config.dart';
-import 'common.dart';
-
-// Simple plugin which just removes all initMethod initializers.
-class DeleteInitMethodPlugin extends InitializePluginTransformer {
- DeleteInitMethodPlugin(String bootstrap) : super(bootstrap);
-
- initEntry(
- InstanceCreationExpression expression, TextEditTransaction transaction) {
- var firstArg = expression.argumentList.arguments[0];
- if (firstArg is PrefixedIdentifier &&
- firstArg.identifier.toString() == 'initMethod') {
- removeInitializer(expression, transaction);
- }
- }
-}
-
-main() {
- useCompactVMConfiguration();
-
- var transformer = new DeleteInitMethodPlugin('web/index.bootstrap.dart');
-
- testPhases('basic', [[transformer]], {
- 'a|web/index.bootstrap.dart': '''
-import 'package:initialize/src/static_loader.dart';
-import 'package:initialize/initialize.dart';
-import 'index.dart' as i0;
-
-main() {
- initializers.addAll([
- new InitEntry(i0.initMethod, const LibraryIdentifier(#a, null, 'a.dart')),
- new InitEntry(i0.initMethod, const LibraryIdentifier(#a, 'a', 'a.dart')),
- new InitEntry(i0.initMethod, i0.a),
- new InitEntry(const i0.DynamicInit('a()'), i0.a),
- ]);
-
- i0.main();
-}
-'''
- }, {
- 'a|web/index.bootstrap.dart': '''
-import 'package:initialize/src/static_loader.dart';
-import 'package:initialize/initialize.dart';
-import 'index.dart' as i0;
-
-main() {
- initializers.addAll([
- new InitEntry(const i0.DynamicInit('a()'), i0.a),
- ]);
-
- i0.main();
-}
-'''
- }, []);
-}
diff --git a/test/transformer_test.dart b/test/transformer_test.dart
index ac3b25e..12e3259 100644
--- a/test/transformer_test.dart
+++ b/test/transformer_test.dart
@@ -4,19 +4,26 @@
library initialize.transformer_test;
import 'common.dart';
+import 'package:analyzer/src/generated/element.dart';
import 'package:dart_style/dart_style.dart';
import 'package:initialize/transformer.dart';
import 'package:unittest/compact_vm_config.dart';
+import 'package:unittest/unittest.dart';
+
+var formatter = new DartFormatter();
main() {
useCompactVMConfiguration();
- var formatter = new DartFormatter();
+ group('Html entry points', htmlEntryPointTests);
+ group('Dart entry points', dartEntryPointTests);
+ group('InitializerPlugins', pluginTests);
+}
- var htmlTransformer = new InitializeTransformer(['web/*.html']);
- var dartTransformer = new InitializeTransformer(['web/index.dart']);
+void htmlEntryPointTests() {
+ var phases = [[new InitializeTransformer(['web/*.html'])]];
- testPhases('basic', [[htmlTransformer]], {
+ testPhases('basic', phases, {
'a|web/index.html': '''
<html><head></head><body>
<script type="application/dart" src="index.dart"></script>
@@ -101,8 +108,12 @@
}
''')
}, []);
+}
- testPhases('constructor arguments', [[dartTransformer]], {
+void dartEntryPointTests() {
+ var phases = [[new InitializeTransformer(['web/index.dart'])]];
+
+ testPhases('constructor arguments', phases, {
'a|web/index.dart': '''
@DynamicInit(foo)
@DynamicInit(Foo.foo)
@@ -158,3 +169,65 @@
''')
}, []);
}
+
+class SkipConstructorsPlugin extends InitializerPlugin {
+ bool shouldApply(InitializerPluginData data) {
+ return data.initializer.annotationElement.element is ConstructorElement;
+ }
+
+ String apply(_) => null;
+}
+
+void pluginTests() {
+ var phases = [
+ [
+ new InitializeTransformer(['web/index.dart'],
+ plugins: [new SkipConstructorsPlugin()])
+ ]
+ ];
+
+ testPhases('can omit statements', phases, {
+ 'a|web/index.dart': '''
+ library index;
+
+ import 'package:initialize/initialize.dart';
+ import 'package:test_initializers/common.dart';
+ import 'foo.dart';
+
+ @initMethod
+ @DynamicInit('index')
+ index() {}
+ ''',
+ 'a|web/foo.dart': '''
+ library foo;
+
+ import 'package:initialize/initialize.dart';
+ import 'package:test_initializers/common.dart';
+
+ @initMethod
+ @DynamicInit('Foo')
+ foo() {}
+ ''',
+ // Mock out the Initialize package plus some initializers.
+ 'initialize|lib/initialize.dart': mockInitialize,
+ 'test_initializers|lib/common.dart': commonInitializers,
+ }, {
+ 'a|web/index.initialize.dart': formatter.format('''
+ import 'package:initialize/src/static_loader.dart';
+ import 'package:initialize/initialize.dart';
+ import 'index.dart' as i0;
+ import 'foo.dart' as i1;
+ import 'package:initialize/initialize.dart' as i2;
+ import 'package:test_initializers/common.dart' as i3;
+
+ main() {
+ initializers.addAll([
+ new InitEntry(i2.initMethod, i1.foo),
+ new InitEntry(i2.initMethod, i0.index),
+ ]);
+
+ i0.main();
+ }
+ ''')
+ }, []);
+}