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();
+        }
+        ''')
+  }, []);
+}