[Pkg-javascript-commits] [node-ast-util] 01/08: Imported Upstream version 0.6.0

Julien Puydt julien.puydt at laposte.net
Tue Oct 20 06:45:25 UTC 2015


This is an automated email from the git hooks/post-receive script.

jpuydt-guest pushed a commit to branch master
in repository node-ast-util.

commit d8fc2ee1c50471ae9462a9792c3d966397316d79
Author: Julien Puydt <julien.puydt at laposte.net>
Date:   Mon Oct 19 12:52:11 2015 +0200

    Imported Upstream version 0.6.0
---
 .travis.yml                     |   4 +
 Makefile                        |  45 +++
 README.md                       | 243 +++++++++++++++
 bin/make-builder                | 332 +++++++++++++++++++++
 examples/swap-macro.js          | 101 +++++++
 helpers/get.js                  |  19 ++
 helpers/getArrayIterator.js     |  18 ++
 helpers/getIterator.js          |  11 +
 helpers/getIteratorRange.js     |  25 ++
 lib/helpers/get.js              | 160 ++++++++++
 lib/helpers/getArrayIterator.js |  90 ++++++
 lib/helpers/getIterator.js      | 107 +++++++
 lib/helpers/getIteratorRange.js | 150 ++++++++++
 lib/index.js                    | 617 ++++++++++++++++++++++++++++++++++++++
 lib/replacement.js              |  95 ++++++
 lib/types.js                    |  22 ++
 package.json                    |  37 +++
 test/replacement_test.js        | 135 +++++++++
 test/util_test.js               | 634 ++++++++++++++++++++++++++++++++++++++++
 19 files changed, 2845 insertions(+)

diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..18ae2d8
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,4 @@
+language: node_js
+node_js:
+  - "0.11"
+  - "0.10"
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c1c4bb5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,45 @@
+SHELL=bash
+
+all: lib/helpers/get.js lib/helpers/getIterator.js lib/helpers/getArrayIterator.js lib/helpers/getIteratorRange.js
+
+lib/helpers/get.js: helpers/get.js Makefile
+	@mkdir -p lib/helpers
+	@echo "var b = require('ast-types').builders;" > $@
+	@echo 'module.exports = function(scope) {' >> $@
+	@echo -n '  return ' >> $@
+	./bin/make-builder < $< >> $@
+	@echo '};' >> $@
+
+lib/helpers/getIterator.js: helpers/getIterator.js Makefile
+	@mkdir -p lib/helpers
+	@echo "var b = require('ast-types').builders;" > $@
+	@echo 'module.exports = function(scope) {' >> $@
+	@echo "  var getArrayIterator = require('..').getArrayIterator;" >> $@
+	@echo >> $@
+	@echo -n '  return ' >> $@
+	./bin/make-builder 'getArrayIterator=getArrayIterator(scope)' < $< >> $@
+	@echo '};' >> $@
+
+lib/helpers/getArrayIterator.js: helpers/getArrayIterator.js Makefile
+	@mkdir -p lib/helpers
+	@echo "var b = require('ast-types').builders;" > $@
+	@echo 'module.exports = function(scope) {' >> $@
+	@echo -n '  return ' >> $@
+	./bin/make-builder < $< >> $@
+	@echo '};' >> $@
+
+lib/helpers/getIteratorRange.js: helpers/getIteratorRange.js Makefile
+	@mkdir -p lib/helpers
+	@echo "var b = require('ast-types').builders;" > $@
+	@echo 'module.exports = function(scope) {' >> $@
+	@echo -n '  return ' >> $@
+	./bin/make-builder < $< >> $@
+	@echo '};' >> $@
+
+clean:
+	rm -rf lib/helpers
+
+test: all
+	npm test
+
+.PHONY: clean test
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1339ab9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,243 @@
+# ast-util
+
+Utilities for AST transformers.
+
+## Install
+
+```
+$ npm install [--save] ast-util
+```
+
+## API
+
+<a name="callArraySlice" href="#user-content-callArraySlice">#</a> <b>callArraySlice</b>(<i>scope</i>, <i>node</i>[, <i>begin</i>, <i>end</i>])
+
+Returns a call to `Array.prototype.slice` with `node` as the context and
+`begin` and `end` as the arguments to `slice`.
+
+
+<a name="callFunctionBind" href="#user-content-callFunctionBind">#</a> <b>callFunctionBind</b>(<i>scope</i>, <i>fn</i>, <i>context</i>[, <i>args</i>])
+
+Returns a call to `Function.prototype.bind` using either `call` or `apply`
+depending on what the value of `args` is. If `args` is an expression then
+`apply` is used. If `args` is an array of expressions, then `call`.
+
+
+<a name="callGet" href="#user-content-callGet">#</a> <b>callGet</b>(<i>scope</i>, <i>object</i>, <i>property</i>, <i>receiver</i>)
+
+The [[Get]] internal method on objects would look something like
+[helpers/get.js](helpers/get.js).
+
+
+<a name="callGetOwnPropertyDescriptor" href="#user-content-callGetOwnPropertyDescriptor">#</a> <b>callGetOwnPropertyDescriptor</b>(<i>scope</i>, <i>object</i>, <i>property</i>)
+
+Returns a call to `Object.getOwnPropertyDescriptor` with the given `object` and
+`property`.
+
+
+<a name="callGetPrototypeOf" href="#user-content-callGetPrototypeOf">#</a> <b>callGetPrototypeOf</b>(<i>scope</i>, <i>object</i>)
+
+Returns a call to `Object.getPrototypeOf` with the given `object`.
+
+
+<a name="callHasOwnProperty" href="#user-content-callHasOwnProperty">#</a> <b>callHasOwnProperty</b>(<i>scope</i>, <i>node</i>, <i>property</i>)
+
+Returns a call to `hasOwnProperty` with `node` as the context and `property` as
+the property to check.
+
+
+<a name="callSharedMethod" href="#user-content-callSharedMethod">#</a> <b>callSharedMethod</b>(<i>scope</i>, <i>callee</i>, <i>args</i>)
+
+Returns a call to the given `callee` with `args` as the arguments. If `callee`
+is a string then it is treated as a globally-accessible function such as
+`Object.defineProperty` which will be stored in a unique temporary variable.
+Subsequent calls to this function will re-use the same temporary variable.
+
+
+<a name="callSharedMethodWithContext" href="#user-content-callSharedMethodWithContext">#</a> <b>callSharedMethodWithContext</b>(<i>scope</i>, <i>callee</i>, <i>context</i>, <i>args</i>)
+
+Returns a call to the given `callee` with `context` as the method context and
+`args` as the arguments. If `callee` is a string then it is treated as a
+globally-accessible function such as `Array.prototype.slice` which will be
+stored in a unique temporary variable. Subsequent calls to this function will
+re-use the same temporary variable.
+
+
+<a name="getGlobals" href="#user-content-getGlobals">#</a> <b>getGlobals</b>(<i>ast</i>)
+
+Gets a list of identifiers referencing global variables anywhere within the
+given `ast`. Assuming the ast is for this code:
+
+```js
+var a;
+function b(){ return c; }
+b(d);
+```
+
+Then `getGlobals` will return two identifiers, `c` and `d`.
+
+
+<a name="identifierForString" href="#user-content-identifierForString">#</a> <b>identifierForString</b>(<i>string</i>)
+
+Generate a safe JavaScript identifier for the given string.
+
+
+<a name="injectShared" href="#user-content-injectShared">#</a> <b>injectShared</b>(<i>scope</i>, <i>name</i>, <i>expression</i>)
+
+Injects a shared variable with a unique identifier. Only the first call with
+the same `scope` and `name` will result in a variable declaration being
+created. The `expression` passed in can either be an AST node or a function to
+generate one. This function is generally used to inject repeatedly-used values
+and prevent repeated execution.
+
+
+<a name="injectVariable" href="#user-content-injectVariable">#</a> <b>injectVariable</b>(<i>scope</i>, <i>identifier</i>[, <i>init</i>])
+
+Injects a variable with the given `identifier` into the given `scope` as a
+`var` declaration with an optional initial value.
+
+
+<a name="isReference" href="#user-content-isReference">#</a> <b>isReference</b>(<i>path</i>)
+
+Determines whether the given `path` is a value reference. For example, `a` and
+`b` are references, but `c` is not:
+
+```js
+a(b.c);
+```
+
+Only identifiers count as references.
+
+
+<a name="isUsed" href="#user-content-isUsed">#</a> <b>isUsed</b>(<i>scope</i>, <i>name</i>)
+
+Determines whether the given `name` should be considered "used" in the given
+`scope`. For a name to be used, it should either:
+
+  1. Be declared in this scope or a parent scope.
+  2. Be referenced in this scope, a parent scope, or any child scopes.
+
+For example, `a`, `b`, and `d` are used in the global scope of this example
+while `c` is not:
+
+```js
+var a;
+function b() {}
+
+try {
+  a = b(d);
+} catch (c) {
+}
+```
+
+
+<a name="sharedFor" href="#user-content-sharedFor">#</a> <b>sharedFor</b>(<i>scope</i>, <i>name</i>)
+
+Injects a shared variable by getting the named value from a dotted path. For
+example, this will return an identifier that can be used in place of the named
+expression:
+
+```js
+sharedFor(scope, 'Object.defineProperty')
+```
+
+Subsequent calls to `sharedFor` in the same scope will return the same
+identifier.
+
+
+<a name="uniqueIdentifier" href="#user-content-uniqueIdentifier">#</a> <b>uniqueIdentifier</b>(<i>scope</i>[, <i>name</i>])
+
+Generates an identifier guaranteed not to collide with any others in the given
+`scope`. This function will also never generate the same identifier twice for
+any `scope` whose global scope already got that identifier.
+
+Called in a scope with no global references and no variables, the first time
+this function is called it will return an identifier named `$__0`.
+
+When called with a name that name will be used with a prefix, "$\_\_", if
+possible. If that name is already used then it will append incrementing numbers
+until it finds a name that isn't used.
+
+
+## Usage
+
+These methods are useful to source transforms, such as transpilers or macros.
+Such transforms often have to insert variables into scopes and replace
+expressions. Using `injectVariable` and `injectShared` are specifically for
+that purpose. In conjunction with `ast-types`, here's how you'd write a simple
+version of a `swap` macro:
+
+```js
+// var tmp;
+var tmp = util.injectVariable(
+  this.scope,
+  util.uniqueIdentifier(this.scope)
+);
+
+this.replace(
+  b.sequenceExpression([
+    // tmp = left
+    b.assignmentExpression(
+      '=',
+      tmp,
+      left
+    ),
+    // left = right
+    b.assignmentExpression(
+      '=',
+      left,
+      right
+    ),
+    // right = tmp
+    b.assignmentExpression(
+      '=',
+      right,
+      tmp
+    )
+  ])
+);
+```
+
+See [examples/swap-macro.js](examples/swap-macro.js) for a more complete
+example.
+
+## Contributing
+
+[![Build Status](https://travis-ci.org/eventualbuddha/ast-util.png?branch=master)](https://travis-ci.org/eventualbuddha/ast-util)
+
+
+### Setup
+
+First, install the development dependencies:
+
+```
+$ npm install
+```
+
+Then, try running the tests:
+
+```
+$ make test
+```
+
+If you're adding or editing code that injects helpers into a scope, you'll need
+to edit and run the Makefile to have it generate the files in lib/helpers from
+the files in helpers.
+
+
+### Pull Requests
+
+1. Fork it
+2. Create your feature branch (`git checkout -b my-new-feature`)
+3. Commit your changes (`git commit -am 'Add some feature'`)
+4. Push to the branch (`git push origin my-new-feature`)
+5. Create new Pull Request
+
+
+## Acknowledgements
+
+Huge thanks to [Ben Newman][benjamn] for [ast-types][ast-types], on which much
+of this library depends.
+
+[benjamn]: https://github.com/benjamn
+[ast-types]: https://github.com/benjamn/ast-types
diff --git a/bin/make-builder b/bin/make-builder
new file mode 100755
index 0000000..7a71359
--- /dev/null
+++ b/bin/make-builder
@@ -0,0 +1,332 @@
+#!/usr/bin/env node
+
+/* jshint node:true, unused:true, undef:true */
+
+var recast = require('recast');
+var types = recast.types;
+var b = types.builders;
+var n = types.namedTypes;
+var assert = require('assert');
+var vm = require('vm');
+
+var BUILD_PARAMS_CACHE = {
+  'ThisExpression': [],
+  'BreakStatement': []
+};
+
+/**
+ * Get the named parameters to pass the named builder.
+ *
+ * @param {!string} type
+ * @return {[string]}
+ */
+function buildParamsForType(type) {
+  var entry = BUILD_PARAMS_CACHE[type];
+  if (entry) {
+    return entry;
+  }
+
+  try {
+    b[builderNameForType(type)]();
+    assert.ok(false, 'should have failed to build ' + type + ' with no params');
+  } catch (ex) {
+    var message = ex.message;
+    var typeIndex = message.indexOf(type);
+    var openParenIndex = message.indexOf('(', typeIndex);
+    var closeParenIndex = message.indexOf(')', openParenIndex);
+
+    assert.ok(
+      closeParenIndex >= 0,
+      'unexpected exception format trying to parse build params: ' + message
+    );
+
+    var paramsList = message.slice(openParenIndex, closeParenIndex);
+    var result = [];
+    paramsList.replace(/"([^"]+)"/g, function(_, name) {
+      result.push(name);
+    });
+    BUILD_PARAMS_CACHE[type] = result;
+    return result;
+  }
+}
+
+/**
+ * Get the name of the builder given a node type. For example, "ThisExpression"
+ * becomes "thisExpression".
+ *
+ * @param {!string} type
+ * @return {!string}
+ */
+function builderNameForType(type) {
+  return type[0].toLowerCase() + type.slice(1);
+}
+
+/**
+ * Read all the contents of STDIN and call back with it.
+ *
+ * @param {function(!string)} callback
+ */
+function readStdin(callback) {
+  var stdin = '';
+
+  process.stdin.setEncoding('utf8');
+
+  process.stdin.on('readable', function() {
+    var chunk = process.stdin.read();
+    if (chunk !== null) {
+      stdin += chunk;
+    }
+  });
+
+  process.stdin.on('end', function() {
+    callback(stdin);
+  });
+}
+
+/**
+ * Makes the AST for a JavaScript program that will build the given node using
+ * the builder functions from the ast-types package.
+ *
+ * @param {ast-types.Node} node
+ * @param {{string: string}} replacements
+ * @return {ast-types.Node}
+ */
+function makeBuilder(node, replacements) {
+  if (n.File.check(node)) {
+    return b.expressionStatement(makeBuilder(node.program, replacements));
+  } else if (n.Program.check(node)) {
+    return b.callExpression(
+      b.memberExpression(
+        b.identifier('b'),
+        b.identifier('program'),
+        false
+      ),
+      [b.arrayExpression(node.body.map(function(statement) {
+        return makeBuilder(statement, replacements);
+      }))]
+    );
+  } else if (Array.isArray(node)) {
+    return b.arrayExpression(
+      node.map(function(item) { return makeBuilder(item, replacements); })
+    );
+  } else if (node && node.constructor === RegExp) {
+    return b.literal(node);
+  }
+
+  assert.ok(
+    n.Node.check(node),
+    'unexpected node type: ' + JSON.stringify(node)
+  );
+
+  if (n.Identifier.check(node)) {
+    var replacement = replacements[node.name];
+    if (replacement) {
+      var newReplacements = Object.create(replacements);
+      newReplacements[node.name] = undefined;
+      return recast.parse(replacement).program.body[0].expression;
+    }
+  }
+
+  return b.callExpression(
+    b.memberExpression(
+      b.identifier('b'),
+      b.identifier(node.type[0].toLowerCase() + node.type.slice(1)),
+      false
+    ),
+    buildParamsForType(node.type).reduce(function(result, key) {
+      if (key !== 'type' && key !== 'loc') {
+        var value;
+        if (node[key] !== null && typeof node[key] === 'object') {
+          value = makeBuilder(node[key], replacements);
+        } else if (node[key] !== undefined) {
+          value = b.literal(node[key]);
+        }
+
+        if (value) {
+          result.push(value);
+        }
+      }
+
+      return result;
+    }, [])
+  );
+}
+
+/**
+ * Determines whether, when printed, the node should be on multiple lines.
+ *
+ * @param {ast-types.Node}
+ * @param {boolean}
+ */
+function isMultiline(node) {
+  switch (node.type) {
+    case 'ExpressionStatement':
+      return isMultiline(node.expression);
+
+    case 'CallExpression':
+      return node.arguments.length > 1 || node.arguments.some(isMultiline);
+
+    case 'MemberExpression':
+      return isMultiline(node.object) || isMultiline(node.property);
+
+    case 'Identifier':
+      return false;
+
+    case 'ArrayExpression':
+      return node.elements.length > 1 || (node.elements.length === 1 && isMultiline(node.elements[0]));
+
+    case 'Literal':
+      return (node.raw || JSON.stringify(node.value)).indexOf('\n') >= 0;
+
+    default:
+      throw new Error('unexpected node type: ' + node.type);
+  }
+}
+
+/**
+ * @const
+ */
+var INDENT = '  ';
+
+/**
+ * Prints the given list of AST nodes as JavaScript as part of a list of array
+ * elements or function arguments.
+ *
+ * @param {[ast-types.Node]} list
+ * @param {string=} indent
+ */
+function printListLines(list, indent) {
+  if (!indent) { indent = ''; }
+
+  var output = '';
+
+  list.forEach(function(item, i) {
+    output += indent + print(item, indent);
+    if (i !== list.length - 1) {
+      output += ',';
+    }
+    output += '\n';
+  });
+
+  return output;
+}
+
+/**
+ * Prints the given AST node as JavaScript, formatted so as to favor shorter
+ * lines. For example, this:
+ *
+ *   b.callExpression(b.identifier('a'), [b.literal(1), b.literal(2)]);
+ *
+ * Would be printed as:
+ *
+ *   b.callExpression(
+ *     b.identifier('a'),
+ *     [
+ *       b.literal(1),
+ *       b.literal(2)
+ *     ]
+ *   )
+ *
+ * @param {ast-types.Node} node
+ * @param {string=} indent
+ * @return {string}
+ */
+function print(node, indent) {
+  if (!indent) { indent = ''; }
+
+  switch (node.type) {
+    case 'ExpressionStatement':
+      return print(node.expression, indent) + ';';
+
+    case 'CallExpression':
+      if (isMultiline(node)) {
+        if (node.arguments.length === 1 && node.arguments[0].type === 'ArrayExpression') {
+          return print(node.callee, indent) + '([\n' +
+            printListLines(node.arguments[0].elements, indent + INDENT) +
+            indent + '])';
+        } else {
+          return print(node.callee, indent) + '(\n' +
+            printListLines(node.arguments, indent + INDENT) +
+            indent + ')';
+        }
+      }
+
+      return print(node.callee, indent) + '(' +
+        node.arguments.map(function(arg) { return print(arg, indent); }).join(', ') +
+        ')';
+
+    case 'MemberExpression':
+      if (node.computed) {
+        return print(node.object, indent) + '[' + print(node.property, indent) + ']';
+      } else {
+        return print(node.object, indent) + '.' + print(node.property, indent);
+      }
+      break;
+
+    case 'Identifier':
+      return node.name;
+
+    case 'ArrayExpression':
+      if (isMultiline(node)) {
+        return '[\n' +
+          printListLines(node.elements, indent + INDENT) +
+          indent + ']';
+      } else {
+        return '[' +
+          node.elements.map(function(element) { return print(element, indent); }).join(', ') +
+          ']';
+      }
+      break;
+
+    case 'Literal':
+      if (typeof node.value === 'string') {
+        return "'" + node.value.replace(/'/g, "\\'") + "'";
+      } else {
+        return node.raw || JSON.stringify(node.value);
+      }
+      break;
+
+    default:
+      throw new Error('unexpected node type: ' + node.type);
+  }
+}
+
+var replacements = process.argv.slice(2).reduce(function(map, arg) {
+  var parts = arg.split('=');
+  if (parts.length === 2) {
+    map[parts[0]] = parts[1];
+  }
+  return map;
+}, {});
+
+var TEST = process.argv.indexOf('--test') >= 0;
+
+readStdin(function(stdin) {
+  var inputSource = stdin;
+  var inputAST = recast.parse(inputSource);
+  var body = inputAST.program.body;
+  var ast = inputAST.program;
+
+  if (body.length === 1) {
+    var statement = body[0];
+    // Favor processing just an expression if possible.
+    ast = n.ExpressionStatement.check(statement) ?
+      statement.expression : statement;
+  }
+
+  var code = print(makeBuilder(ast, replacements));
+
+  if (TEST) {
+    // verify the result
+    var context = { b: b };
+    vm.runInNewContext('result = ' + code, context);
+    var normalizedInputSource = recast.prettyPrint(inputAST).code;
+    var normalizedBuiltSource = recast.prettyPrint(context.result).code;
+    assert.equal(
+      normalizedBuiltSource,
+      normalizedInputSource
+    );
+  }
+
+  process.stdout.write(code);
+});
diff --git a/examples/swap-macro.js b/examples/swap-macro.js
new file mode 100755
index 0000000..201d540
--- /dev/null
+++ b/examples/swap-macro.js
@@ -0,0 +1,101 @@
+#!/usr/bin/env node
+
+var recast = require('recast');
+var util = require('../lib');
+var types = util.types;
+var n = types.namedTypes;
+var b = types.builders;
+var assert = require('assert');
+
+/**
+ * Treats calls to `swap` as a macro to swap the identifiers passed to swap.
+ *
+ *   swap(left, right);
+ *
+ * Becomes
+ *
+ *   var tmp;
+ *   tmp = left;
+ *   left = right;
+ *   right = tmp;
+ *
+ * @param {ast-types.Node} ast
+ * @return {ast-types.Node} Returns `ast`.
+ */
+function transform(ast) {
+  var replaced = [];
+
+  types.traverse(ast, function(node) {
+    if (n.ExpressionStatement.check(node)) {
+      if (isSwapCall(this.get('expression'))) {
+        // swap(left, right)
+        var expression = node.expression;
+        assert.equal(expression.arguments.length, 2, 'expected 2 arguments to `swap`, got ' + expression.arguments.length);
+
+        var left = expression.arguments[0];
+        var right = expression.arguments[1];
+
+        assert.ok(
+          n.Identifier.check(left) || n.MemberExpression.check(left),
+          'expected first argument of `swap` to be an Identifier or MemberExpression, found ' + left.type
+        );
+        assert.ok(
+          n.Identifier.check(right) || n.MemberExpression.check(right),
+          'expected second argument of `swap` to be an Identifier or MemberExpression, found ' + right.type
+        );
+
+        var tmp = util.uniqueIdentifier(this.scope);
+
+        replaced.push(expression);
+        this.replace(
+          // var tmp = left;
+          b.variableDeclaration(
+            'var',
+            [b.variableDeclarator(tmp, left)]
+          ),
+          // left = right
+          b.expressionStatement(b.assignmentExpression(
+            '=',
+            left,
+            right
+          )),
+          // right = tmp
+          b.expressionStatement(b.assignmentExpression(
+            '=',
+            right,
+            tmp
+          ))
+        );
+      }
+    } else if (isSwapCall(this) && replaced.indexOf(node) < 0) {
+      throw new Error('unexpected `swap` macro used as an expression instead of a statement');
+    }
+  });
+
+  return ast;
+}
+
+function isSwapCall(path) {
+  return n.CallExpression.check(path.value) && util.isReference(path.get('callee'), 'swap');
+}
+
+function readStdin(callback) {
+  var stdin = '';
+
+  process.stdin.setEncoding('utf8');
+
+  process.stdin.on('readable', function() {
+    var chunk = process.stdin.read();
+    if (chunk !== null) {
+      stdin += chunk;
+    }
+  });
+
+  process.stdin.on('end', function() {
+    callback(stdin);
+  });
+}
+
+readStdin(function(stdin) {
+  process.stdout.write(recast.print(transform(recast.parse(stdin))).code);
+});
diff --git a/helpers/get.js b/helpers/get.js
new file mode 100644
index 0000000..d143cc7
--- /dev/null
+++ b/helpers/get.js
@@ -0,0 +1,19 @@
+(function get(object, property, receiver) {
+  var desc = Object.getOwnPropertyDescriptor(object, property);
+  if (desc === void 0) {
+    var parent = Object.getPrototypeOf(object);
+    if (parent === null) {
+      return void 0;
+    } else {
+      return get(parent, property, receiver);
+    }
+  } else if ('value' in desc && 'writable' in desc) {
+    return desc.value;
+  } else {
+    var getter = desc.get;
+    if (getter === void 0) {
+      return void 0;
+    }
+    return getter.call(receiver);
+  }
+});
diff --git a/helpers/getArrayIterator.js b/helpers/getArrayIterator.js
new file mode 100644
index 0000000..a0a7890
--- /dev/null
+++ b/helpers/getArrayIterator.js
@@ -0,0 +1,18 @@
+(function(array) {
+  var index = 0;
+  return {
+    next: function() {
+      if (index < array.length) {
+        return {
+          done: false,
+          value: array[index++]
+        };
+      } else {
+        return {
+          done: true,
+          value: void 0
+        };
+      }
+    }
+  };
+});
diff --git a/helpers/getIterator.js b/helpers/getIterator.js
new file mode 100644
index 0000000..8044dd5
--- /dev/null
+++ b/helpers/getIterator.js
@@ -0,0 +1,11 @@
+(function(iterable) {
+  var sym = typeof Symbol === "function" && Symbol.iterator || "@@iterator";
+
+  if (typeof iterable[sym] === "function") {
+    return iterable[sym]();
+  } else if (typeof iterable === "object" || typeof iterable === "function") {
+    return getArrayIterator(iterable);
+  } else {
+    throw new TypeError();
+  }
+});
diff --git a/helpers/getIteratorRange.js b/helpers/getIteratorRange.js
new file mode 100644
index 0000000..c64bc1c
--- /dev/null
+++ b/helpers/getIteratorRange.js
@@ -0,0 +1,25 @@
+(function(iterator, index, begin, len) {
+  if (index > begin) {
+    throw new RangeError();
+  }
+  if (typeof len === "undefined") {
+    len = Infinity;
+  }
+
+  var range = [], end = begin + len;
+  while (index < end) {
+    var next = iterator.next();
+    if (next.done) {
+      break;
+    }
+    if (index >= begin) {
+      range.push(next.value);
+    }
+    index++;
+  }
+
+  return {
+    range: range,
+    index: index
+  };
+});
diff --git a/lib/helpers/get.js b/lib/helpers/get.js
new file mode 100644
index 0000000..dfdc154
--- /dev/null
+++ b/lib/helpers/get.js
@@ -0,0 +1,160 @@
+var b = require('ast-types').builders;
+module.exports = function(scope) {
+  return b.functionExpression(
+  b.identifier('get'),
+  [
+    b.identifier('object'),
+    b.identifier('property'),
+    b.identifier('receiver')
+  ],
+  b.blockStatement([
+    b.variableDeclaration(
+      'var',
+      [
+        b.variableDeclarator(
+          b.identifier('desc'),
+          b.callExpression(
+            b.memberExpression(
+              b.identifier('Object'),
+              b.identifier('getOwnPropertyDescriptor'),
+              false
+            ),
+            [
+              b.identifier('object'),
+              b.identifier('property')
+            ]
+          )
+        )
+      ]
+    ),
+    b.ifStatement(
+      b.binaryExpression(
+        '===',
+        b.identifier('desc'),
+        b.unaryExpression(
+          'void',
+          b.literal(0),
+          true
+        )
+      ),
+      b.blockStatement([
+        b.variableDeclaration(
+          'var',
+          [
+            b.variableDeclarator(
+              b.identifier('parent'),
+              b.callExpression(
+                b.memberExpression(
+                  b.identifier('Object'),
+                  b.identifier('getPrototypeOf'),
+                  false
+                ),
+                [b.identifier('object')]
+              )
+            )
+          ]
+        ),
+        b.ifStatement(
+          b.binaryExpression(
+            '===',
+            b.identifier('parent'),
+            b.literal(null)
+          ),
+          b.blockStatement([
+            b.returnStatement(
+              b.unaryExpression(
+                'void',
+                b.literal(0),
+                true
+              )
+            )
+          ]),
+          b.blockStatement([
+            b.returnStatement(
+              b.callExpression(
+                b.identifier('get'),
+                [
+                  b.identifier('parent'),
+                  b.identifier('property'),
+                  b.identifier('receiver')
+                ]
+              )
+            )
+          ])
+        )
+      ]),
+      b.ifStatement(
+        b.logicalExpression(
+          '&&',
+          b.binaryExpression(
+            'in',
+            b.literal('value'),
+            b.identifier('desc')
+          ),
+          b.binaryExpression(
+            'in',
+            b.literal('writable'),
+            b.identifier('desc')
+          )
+        ),
+        b.blockStatement([
+          b.returnStatement(
+            b.memberExpression(
+              b.identifier('desc'),
+              b.identifier('value'),
+              false
+            )
+          )
+        ]),
+        b.blockStatement([
+          b.variableDeclaration(
+            'var',
+            [
+              b.variableDeclarator(
+                b.identifier('getter'),
+                b.memberExpression(
+                  b.identifier('desc'),
+                  b.identifier('get'),
+                  false
+                )
+              )
+            ]
+          ),
+          b.ifStatement(
+            b.binaryExpression(
+              '===',
+              b.identifier('getter'),
+              b.unaryExpression(
+                'void',
+                b.literal(0),
+                true
+              )
+            ),
+            b.blockStatement([
+              b.returnStatement(
+                b.unaryExpression(
+                  'void',
+                  b.literal(0),
+                  true
+                )
+              )
+            ]),
+            null
+          ),
+          b.returnStatement(
+            b.callExpression(
+              b.memberExpression(
+                b.identifier('getter'),
+                b.identifier('call'),
+                false
+              ),
+              [b.identifier('receiver')]
+            )
+          )
+        ])
+      )
+    )
+  ]),
+  false,
+  false
+)};
diff --git a/lib/helpers/getArrayIterator.js b/lib/helpers/getArrayIterator.js
new file mode 100644
index 0000000..0072f6e
--- /dev/null
+++ b/lib/helpers/getArrayIterator.js
@@ -0,0 +1,90 @@
+var b = require('ast-types').builders;
+module.exports = function(scope) {
+  return b.functionExpression(
+  null,
+  [b.identifier('array')],
+  b.blockStatement([
+    b.variableDeclaration(
+      'var',
+      [
+        b.variableDeclarator(
+          b.identifier('index'),
+          b.literal(0)
+        )
+      ]
+    ),
+    b.returnStatement(
+      b.objectExpression([
+        b.property(
+          'init',
+          b.identifier('next'),
+          b.functionExpression(
+            null,
+            [],
+            b.blockStatement([
+              b.ifStatement(
+                b.binaryExpression(
+                  '<',
+                  b.identifier('index'),
+                  b.memberExpression(
+                    b.identifier('array'),
+                    b.identifier('length'),
+                    false
+                  )
+                ),
+                b.blockStatement([
+                  b.returnStatement(
+                    b.objectExpression([
+                      b.property(
+                        'init',
+                        b.identifier('done'),
+                        b.literal(false)
+                      ),
+                      b.property(
+                        'init',
+                        b.identifier('value'),
+                        b.memberExpression(
+                          b.identifier('array'),
+                          b.updateExpression(
+                            '++',
+                            b.identifier('index'),
+                            false
+                          ),
+                          true
+                        )
+                      )
+                    ])
+                  )
+                ]),
+                b.blockStatement([
+                  b.returnStatement(
+                    b.objectExpression([
+                      b.property(
+                        'init',
+                        b.identifier('done'),
+                        b.literal(true)
+                      ),
+                      b.property(
+                        'init',
+                        b.identifier('value'),
+                        b.unaryExpression(
+                          'void',
+                          b.literal(0),
+                          true
+                        )
+                      )
+                    ])
+                  )
+                ])
+              )
+            ]),
+            false,
+            false
+          )
+        )
+      ])
+    )
+  ]),
+  false,
+  false
+)};
diff --git a/lib/helpers/getIterator.js b/lib/helpers/getIterator.js
new file mode 100644
index 0000000..d63e785
--- /dev/null
+++ b/lib/helpers/getIterator.js
@@ -0,0 +1,107 @@
+var b = require('ast-types').builders;
+module.exports = function(scope) {
+  var getArrayIterator = require('..').getArrayIterator;
+
+  return b.functionExpression(
+  null,
+  [b.identifier('iterable')],
+  b.blockStatement([
+    b.variableDeclaration(
+      'var',
+      [
+        b.variableDeclarator(
+          b.identifier('sym'),
+          b.logicalExpression(
+            '||',
+            b.logicalExpression(
+              '&&',
+              b.binaryExpression(
+                '===',
+                b.unaryExpression(
+                  'typeof',
+                  b.identifier('Symbol'),
+                  true
+                ),
+                b.literal('function')
+              ),
+              b.memberExpression(
+                b.identifier('Symbol'),
+                b.identifier('iterator'),
+                false
+              )
+            ),
+            b.literal('@@iterator')
+          )
+        )
+      ]
+    ),
+    b.ifStatement(
+      b.binaryExpression(
+        '===',
+        b.unaryExpression(
+          'typeof',
+          b.memberExpression(
+            b.identifier('iterable'),
+            b.identifier('sym'),
+            true
+          ),
+          true
+        ),
+        b.literal('function')
+      ),
+      b.blockStatement([
+        b.returnStatement(
+          b.callExpression(
+            b.memberExpression(
+              b.identifier('iterable'),
+              b.identifier('sym'),
+              true
+            ),
+            []
+          )
+        )
+      ]),
+      b.ifStatement(
+        b.logicalExpression(
+          '||',
+          b.binaryExpression(
+            '===',
+            b.unaryExpression(
+              'typeof',
+              b.identifier('iterable'),
+              true
+            ),
+            b.literal('object')
+          ),
+          b.binaryExpression(
+            '===',
+            b.unaryExpression(
+              'typeof',
+              b.identifier('iterable'),
+              true
+            ),
+            b.literal('function')
+          )
+        ),
+        b.blockStatement([
+          b.returnStatement(
+            b.callExpression(
+              getArrayIterator(scope),
+              [b.identifier('iterable')]
+            )
+          )
+        ]),
+        b.blockStatement([
+          b.throwStatement(
+            b.newExpression(
+              b.identifier('TypeError'),
+              []
+            )
+          )
+        ])
+      )
+    )
+  ]),
+  false,
+  false
+)};
diff --git a/lib/helpers/getIteratorRange.js b/lib/helpers/getIteratorRange.js
new file mode 100644
index 0000000..800a8ce
--- /dev/null
+++ b/lib/helpers/getIteratorRange.js
@@ -0,0 +1,150 @@
+var b = require('ast-types').builders;
+module.exports = function(scope) {
+  return b.functionExpression(
+  null,
+  [
+    b.identifier('iterator'),
+    b.identifier('index'),
+    b.identifier('begin'),
+    b.identifier('len')
+  ],
+  b.blockStatement([
+    b.ifStatement(
+      b.binaryExpression(
+        '>',
+        b.identifier('index'),
+        b.identifier('begin')
+      ),
+      b.blockStatement([
+        b.throwStatement(
+          b.newExpression(
+            b.identifier('RangeError'),
+            []
+          )
+        )
+      ]),
+      null
+    ),
+    b.ifStatement(
+      b.binaryExpression(
+        '===',
+        b.unaryExpression(
+          'typeof',
+          b.identifier('len'),
+          true
+        ),
+        b.literal('undefined')
+      ),
+      b.blockStatement([
+        b.expressionStatement(
+          b.assignmentExpression(
+            '=',
+            b.identifier('len'),
+            b.identifier('Infinity')
+          )
+        )
+      ]),
+      null
+    ),
+    b.variableDeclaration(
+      'var',
+      [
+        b.variableDeclarator(
+          b.identifier('range'),
+          b.arrayExpression([])
+        ),
+        b.variableDeclarator(
+          b.identifier('end'),
+          b.binaryExpression(
+            '+',
+            b.identifier('begin'),
+            b.identifier('len')
+          )
+        )
+      ]
+    ),
+    b.whileStatement(
+      b.binaryExpression(
+        '<',
+        b.identifier('index'),
+        b.identifier('end')
+      ),
+      b.blockStatement([
+        b.variableDeclaration(
+          'var',
+          [
+            b.variableDeclarator(
+              b.identifier('next'),
+              b.callExpression(
+                b.memberExpression(
+                  b.identifier('iterator'),
+                  b.identifier('next'),
+                  false
+                ),
+                []
+              )
+            )
+          ]
+        ),
+        b.ifStatement(
+          b.memberExpression(
+            b.identifier('next'),
+            b.identifier('done'),
+            false
+          ),
+          b.blockStatement([b.breakStatement()]),
+          null
+        ),
+        b.ifStatement(
+          b.binaryExpression(
+            '>=',
+            b.identifier('index'),
+            b.identifier('begin')
+          ),
+          b.blockStatement([
+            b.expressionStatement(
+              b.callExpression(
+                b.memberExpression(
+                  b.identifier('range'),
+                  b.identifier('push'),
+                  false
+                ),
+                [
+                  b.memberExpression(
+                    b.identifier('next'),
+                    b.identifier('value'),
+                    false
+                  )
+                ]
+              )
+            )
+          ]),
+          null
+        ),
+        b.expressionStatement(
+          b.updateExpression(
+            '++',
+            b.identifier('index'),
+            false
+          )
+        )
+      ])
+    ),
+    b.returnStatement(
+      b.objectExpression([
+        b.property(
+          'init',
+          b.identifier('range'),
+          b.identifier('range')
+        ),
+        b.property(
+          'init',
+          b.identifier('index'),
+          b.identifier('index')
+        )
+      ])
+    )
+  ]),
+  false,
+  false
+)};
diff --git a/lib/index.js b/lib/index.js
new file mode 100644
index 0000000..4340e9a
--- /dev/null
+++ b/lib/index.js
@@ -0,0 +1,617 @@
+/* jshint node:true, undef:true, unused:true */
+
+var types = require('ast-types');
+var b = types.builders;
+var n = types.namedTypes;
+var NodePath = types.NodePath;
+
+var getSecret = require('private').makeAccessor();
+var hasOwnProp = Object.prototype.hasOwnProperty;
+
+var assert = require('assert');
+
+/**
+ * Re-export ast-types for ease of our users.
+ */
+exports.types = types;
+
+/**
+ * Export the Replacement helper for anything that needs to delay replacement.
+ */
+exports.Replacement = require('./replacement');
+
+/**
+ * Returns a call to `Array.prototype.slice` with `node` as the context and
+ * `begin` and `end` as the arguments to `slice`.
+ *
+ * @param {Scope} scope
+ * @param {Expression} node
+ * @param {Expression|number=} begin
+ * @param {Expression|number=} end
+ * @return {CallExpression}
+ */
+function callArraySlice(scope, node, begin, end) {
+  if (typeof begin === 'number') {
+    begin = b.literal(begin);
+  }
+
+  if (typeof end === 'number') {
+    end = b.literal(end);
+  }
+
+  var args = [];
+  if (begin) { args.push(begin); }
+  if (end) { args.push(end); }
+
+  return callSharedMethodWithContext(
+    scope,
+    'Array.prototype.slice',
+    node,
+    args
+  );
+}
+exports.callArraySlice = callArraySlice;
+
+/**
+ * Returns a call to `Function.prototype.bind` using either `call` or `apply`
+ * depending on what the value of `args` is. If `args` is an expression then
+ * `apply` is used. If `args` is an array of expressions, then `call`.
+ *
+ * @param {Scope} scope
+ * @param {Expression} fn
+ * @param {Expression} context
+ * @param {Expression|Array.<Expression>} args
+ * @return {CallExpression}
+ */
+function callFunctionBind(scope, fn, context, args) {
+  var bind = sharedFor(scope, 'Function.prototype.bind');
+
+  if (n.Expression.check(args)) {
+    return b.callExpression(
+      b.memberExpression(bind, b.identifier('apply'), false),
+      [fn, b.callExpression(
+        b.memberExpression(
+          b.arrayExpression([context]),
+          b.identifier('concat'),
+          false
+        ),
+        [args]
+      )]
+    );
+  } else {
+    return b.callExpression(
+      b.memberExpression(bind, b.identifier('call'), false),
+      [fn, context].concat(args || [])
+    );
+  }
+}
+exports.callFunctionBind = callFunctionBind;
+
+/**
+ * Gets an iterator for the value representing the given expression.
+ *
+ * @param {Scope} scope
+ * @param {Expression} expression
+ * @return {CallExpression}
+ */
+function callGetIterator(scope, expression) {
+  var getIterator = injectGetIteratorHelper(scope.getGlobalScope());
+  return b.callExpression(getIterator, [expression]);
+}
+exports.callGetIterator = callGetIterator;
+
+/**
+ * Returns a reference to a shared function that implements the default
+ * `@@iterator` for arrays.
+ *
+ * @private
+ * @param {Scope} scope
+ * @return {CallExpression}
+ */
+function getArrayIterator(scope) {
+  return injectGetArrayIteratorHelper(scope.getGlobalScope());
+}
+exports.getArrayIterator = getArrayIterator;
+
+/**
+ * return a range of value from an iterator
+ *
+ * @param {Scope} scope
+ * @param {Expression} iterator
+ * @param {Literal} index
+ * @param {Literal} begin
+ * @param {Literal} len
+ * @return {CallExpression}
+ */
+function callGetIteratorRange(scope, iterator, index, begin, len) {
+  var getIteratorRange = injectGetIteratorRangeHelper(scope.getGlobalScope());
+  return b.callExpression(getIteratorRange, [iterator, index, begin, len]);
+}
+exports.callGetIteratorRange = callGetIteratorRange;
+
+/**
+ * The [[Get]] internal method on objects.
+ *
+ * @param {Scope} scope
+ * @param {Expression} object
+ * @param {Expression} property
+ * @param {Expression} receiver
+ * @return {CallExpression}
+ */
+function callGet(scope, object, property, receiver) {
+  var get = injectGetHelper(scope.getGlobalScope());
+  return b.callExpression(get, [object, property, receiver]);
+}
+exports.callGet = callGet;
+
+/**
+ * Returns a call to `Object.getOwnPropertyDescriptor` with the given `object`
+ * and `property`.
+ *
+ * @param {Scope} scope
+ * @param {Expression} object
+ * @param {Expression|string} property
+ * @return {CallExpression}
+ */
+function callGetOwnPropertyDescriptor(scope, object, property) {
+  if (typeof property === 'string') {
+    property = b.literal(property);
+  }
+
+  return callSharedMethod(
+    scope,
+    'Object.getOwnPropertyDescriptor',
+    [object, property]
+  );
+}
+exports.callGetOwnPropertyDescriptor = callGetOwnPropertyDescriptor;
+
+/**
+ * Returns a call to `Object.getPrototypeOf` with the given `object`.
+ *
+ * @param {Scope} scope
+ * @param {Expression} object
+ * @return {CallExpression}
+ */
+function callGetPrototypeOf(scope, object) {
+  return callSharedMethod(scope, 'Object.getPrototypeOf', [object]);
+}
+exports.callGetPrototypeOf = callGetPrototypeOf;
+
+/**
+ * Returns a call to `hasOwnProperty` with `node` as the context and `property`
+ * as the property to check.
+ *
+ * @param {Scope} scope
+ * @param {Expression} node
+ * @param {Expression|string} property
+ * @return {CallExpression}
+ */
+function callHasOwnProperty(scope, node, property) {
+  if (typeof property === 'string') {
+    property = b.literal(property);
+  }
+
+  return callSharedMethodWithContext(
+    scope,
+    'Object.prototype.hasOwnProperty',
+    node,
+    [property]
+  );
+}
+exports.callHasOwnProperty = callHasOwnProperty;
+
+/**
+ * Returns a call to the given `callee` with `args` as the arguments. If
+ * `callee` is a string then it is treated as a globally-accessible function
+ * such as `Object.defineProperty` which will be stored in a unique temporary
+ * variable. Subsequent calls to this function will re-use the same temporary
+ * variable.
+ *
+ * @param {Scope} scope
+ * @param {Expression|string} callee
+ * @param {Array.<Expression>} args
+ * @return {CallExpression}
+ */
+function callSharedMethod(scope, callee, args) {
+  if (typeof callee === 'string') {
+    callee = sharedFor(scope, callee);
+  }
+
+  return b.callExpression(callee, args);
+}
+exports.callSharedMethod = callSharedMethod;
+
+/**
+ * Returns a call to the given `callee` with `context` as the method context
+ * and `args` as the arguments. If `callee` is a string then it is treated as a
+ * globally-accessible function such as `Array.prototype.slice` which will be
+ * stored in a unique temporary variable. Subsequent calls to this function
+ * will re-use the same temporary variable.
+ *
+ * @param {Scope} scope
+ * @param {Expression|string} callee
+ * @param {Expression} context
+ * @param {Array.<Expression>} args
+ * @return {CallExpression}
+ */
+function callSharedMethodWithContext(scope, callee, context, args) {
+  if (typeof callee === 'string') {
+    callee = sharedFor(scope, callee);
+  }
+
+  return b.callExpression(
+    b.memberExpression(callee, b.identifier('call'), false),
+    [context].concat(args)
+  );
+}
+exports.callSharedMethodWithContext = callSharedMethodWithContext;
+
+/**
+ * Gets a list of identifiers referencing global variables anywhere within the
+ * given `ast`. Assuming the ast is for this code:
+ *
+ *   var a;
+ *   function b(){ return c; }
+ *   b(d);
+ *
+ * Then `getGlobals` will return two identifiers, `c` and `d`.
+ *
+ * @param {Node} ast
+ * @return {Array.<Identifier>}
+ */
+function getGlobals(ast) {
+  var globals = [];
+  var seen = Object.create(null);
+
+  types.visit(ast, {
+    visitNode: function(path) {
+      this.traverse(path);
+      var node = path.value;
+
+      if (isReference(path) && !path.scope.lookup(node.name)) {
+        if (!(node.name in seen)) {
+          seen[node.name] = true;
+          globals.push(node);
+        }
+      }
+    }
+  });
+
+  return globals;
+}
+exports.getGlobals = getGlobals;
+
+/**
+ * Generate a safe JavaScript identifier for the given string.
+ *
+ * @param {string} string
+ * @return {string}
+ * @private
+ */
+function identifierForString(string) {
+  // TODO: Verify this regex.
+  return string.replace(/[^\w\d\$_]/g, '$');
+}
+
+/**
+ * Injects the 'get' pre-built helper.
+ *
+ * @param {Scope} scope
+ * @return {Identifier}
+ */
+function injectGetHelper(scope) {
+  return injectShared(
+    scope,
+    'get',
+    function() {
+      return require('./helpers/get')(scope);
+    }
+  );
+}
+
+/**
+ * Injects the 'getArrayIterator' pre-built helper.
+ *
+ * @param {Scope} scope
+ * @return {Identifier}
+ */
+function injectGetArrayIteratorHelper(scope) {
+  return injectShared(
+    scope,
+    'getArrayIterator',
+    function() {
+      return require('./helpers/getArrayIterator')(scope);
+    }
+  );
+}
+
+/**
+ * Injects the 'getIterator' pre-built helper.
+ *
+ * @param {Scope} scope
+ * @return {Identifier}
+ */
+function injectGetIteratorHelper(scope) {
+  return injectShared(
+    scope,
+    'getIterator',
+    function() {
+      return require('./helpers/getIterator')(scope);
+    }
+  );
+}
+
+/**
+ * Injects the 'getIteratorRange' pre-built helper.
+ *
+ * @param {Scope} scope
+ * @return {Identifier}
+ */
+function injectGetIteratorRangeHelper(scope) {
+  return injectShared(
+    scope,
+    'getIteratorRange',
+    function() {
+      return require('./helpers/getIteratorRange')(scope);
+    }
+  );
+}
+
+/**
+ * Injects a shared variable with a unique identifier. Only the first call with
+ * the same `scope` and `name` will result in a variable declaration being
+ * created. The `expression` passed in can either be an AST node or a function
+ * to generate one. This function is generally used to inject repeatedly-used
+ * values and prevent repeated execution.
+ *
+ * @param {Scope} scope
+ * @param {string} name
+ * @param {Expression|function(): Expression} expression
+ * @return {Identifier}
+ */
+function injectShared(scope, name, expression) {
+  var scopeSecret = getSecret(scope);
+
+  if (!(name in scopeSecret)) {
+    scopeSecret[name] = injectVariable(
+      scope,
+      uniqueIdentifier(scope, name),
+      typeof expression === 'function' ?
+        expression() :
+        expression
+    );
+  }
+
+  return scopeSecret[name];
+}
+exports.injectShared = injectShared;
+
+/**
+ * Injects a variable with the given `identifier` into the given `scope` as a
+ * `var` declaration with an optional initial value.
+ *
+ * @param {Scope} scope
+ * @param {Identifier} identifier
+ * @param {Expression=} init
+ * @return {Identifier} Returns the given `identifier`.
+ */
+function injectVariable(scope, identifier, init) {
+  var bodyPath = scope.path.get('body');
+
+  if (n.BlockStatement.check(bodyPath.value)) {
+    bodyPath = bodyPath.get('body');
+  }
+
+  var declarationIndex;
+  var bodyStatements = bodyPath.node.body;
+
+  for (declarationIndex = 0; declarationIndex < bodyStatements.length; declarationIndex++) {
+    var statement = bodyStatements[declarationIndex];
+    if (!isDirectivePrologue(statement)) {
+      break;
+    }
+  }
+
+  bodyPath.insertAt(
+    declarationIndex,
+    b.variableDeclaration(
+      'var',
+      [b.variableDeclarator(identifier, init || null)]
+    )
+  );
+
+  // Ensure this identifier counts as used in this scope.
+  var name = identifier.name;
+  var bindings = scope.getBindings();
+  if (!hasOwnProp.call(bindings, name)) {
+    bindings[name] = [];
+  }
+  bindings[name].push(new NodePath(identifier));
+
+  return identifier;
+}
+exports.injectVariable = injectVariable;
+
+/**
+ * Determines whether the given statement is a directive prologue, e.g.
+ * "use strict".
+ *
+ * @param {Statement} statement
+ * @returns {boolean}
+ */
+function isDirectivePrologue(statement) {
+  if (n.ExpressionStatement.check(statement)) {
+    var expression = statement.expression;
+    if (n.Literal.check(expression)) {
+      return typeof expression.value === 'string';
+    }
+  }
+
+  return false;
+}
+
+/**
+ * Determines whether the given `path` is a value reference. For example, `a`
+ * and `b` are references, but `c` is not:
+ *
+ *    a(b.c);
+ *
+ * Only identifiers count as references.
+ *
+ * @param {NodePath} path
+ * @param {string=} name
+ * @return {boolean}
+ */
+function isReference(path, name) {
+  var node = path.value;
+  assert.ok(n.Node.check(node));
+
+  if (n.Identifier.check(node)) {
+    if (name && node.name !== name) { return false; }
+
+    var parent = path.parent.value;
+    if (n.VariableDeclarator.check(parent)) {
+      return parent.init === node;
+    } else if (n.MemberExpression.check(parent)) {
+      return parent.object === node || (
+        parent.computed && parent.property === node
+      );
+    } else if (n.Function.check(parent)) {
+      return parent.id !== node && !parent.params.some(function(param) {
+        return param === node;
+      });
+    } else if (n.ClassDeclaration.check(parent) || n.ClassExpression.check(parent)) {
+      return parent.id !== node;
+    } else if (n.CatchClause.check(parent)) {
+      return parent.param !== node;
+    } else if (n.Property.check(parent)) {
+      return parent.key !== node;
+    } else if (n.MethodDefinition.check(parent)) {
+      return parent.key !== node;
+    } else if (n.ImportSpecifier.check(parent)) {
+      return false;
+    } else if (n.ImportDefaultSpecifier.check(parent)) {
+      return false;
+    } else if (n.ImportNamespaceSpecifier.check(parent)) {
+      return false;
+    } else if (n.LabeledStatement.check(parent)) {
+      return false;
+    } else {
+      return true;
+    }
+  }
+
+  return false;
+}
+exports.isReference = isReference;
+
+/**
+ * Determines whether the given `name` should be considered "used" in the given
+ * `scope`. For a name to be used, it should either:
+ *
+ *   1. Be declared in this scope or a parent scope.
+ *   2. Be referenced in this scope, a parent scope, or any child scopes.
+ *
+ * For example, `a`, `b`, and `d` are used in the global scope of this example
+ * while `c` is not:
+ *
+ *   var a;
+ *   function b() {}
+ *
+ *   try {
+ *     a = b(d);
+ *   } catch (c) {
+ *   }
+ *
+ * @param {Scope} scope
+ * @param {string} name
+ * @return {boolean}
+ */
+function isUsed(scope, name) {
+  if (scope.lookup(name)) {
+    return true;
+  }
+
+  var globalScope = scope.getGlobalScope();
+  var globalScopeSecret = getSecret(globalScope);
+
+  if (!globalScopeSecret.globals) {
+    globalScopeSecret.globals = getGlobals(globalScope.node);
+  }
+
+  return globalScopeSecret.globals.some(function(global) {
+    return global.name === name;
+  });
+}
+exports.isUsed = isUsed;
+
+/**
+ * Injects a shared variable by getting the named value from a dotted path. For
+ * example, this will return an identifier that can be used in place of the
+ * named expression:
+ *
+ *    sharedFor(scope, 'Object.defineProperty')
+ *
+ * Subsequent calls to `sharedFor` in the same scope will return the same
+ * identifier.
+ *
+ * @param {Scope} scope
+ * @param {string} name
+ * @return {Identifier}
+ */
+function sharedFor(scope, name) {
+  return injectShared(
+    scope,
+    name,
+    function() {
+      var parts = name.split('.');
+      var result = b.identifier(parts[0]);
+
+      for (var i = 1, length = parts.length; i < length; i++) {
+        result = b.memberExpression(
+          result,
+          b.identifier(parts[i]),
+          false
+        );
+      }
+
+      return result;
+    }
+  );
+}
+exports.sharedFor = sharedFor;
+
+/**
+ * Generates an identifier guaranteed not to collide with any others in the
+ * given `scope`. This function will also never generate the same identifier
+ * twice for any `scope` whose global scope already got that identifier.
+ *
+ * Called in a scope with no global references and no variables, the first time
+ * this function is called it will return an identifier named `$__0`.
+ *
+ * When called with a name that name will be used with a prefix, "$__", if
+ * possible. If that name is already used then it will append incrementing
+ * numbers until it finds a name that isn't used.
+ *
+ * @param {Scope} scope
+ * @param {string=} name
+ * @return {Identifier}
+ * @see isUsed
+ */
+function uniqueIdentifier(scope, name) {
+  var prefix = '$__' + identifierForString(name ? name : '');
+  var globalScopeSecret = getSecret(scope.getGlobalScope());
+  var n = globalScopeSecret.nextId || 0;
+  var identifier = name ? prefix : null;
+
+  while (!identifier || isUsed(scope, identifier)) {
+    identifier = prefix + n;
+    n++;
+  }
+
+  globalScopeSecret.nextId = n;
+
+  return b.identifier(identifier);
+}
+exports.uniqueIdentifier = uniqueIdentifier;
diff --git a/lib/replacement.js b/lib/replacement.js
new file mode 100644
index 0000000..939439b
--- /dev/null
+++ b/lib/replacement.js
@@ -0,0 +1,95 @@
+/* jshint node:true, undef:true, unused:true */
+
+/**
+ * Represents a replacement of a node path with zero or more nodes.
+ *
+ * @constructor
+ * @param {NodePath} nodePath
+ * @param {Array.<Node>} nodes
+ */
+function Replacement(nodePath, nodes) {
+  this.queue = [];
+  if (nodePath && nodes) {
+    this.queue.push([nodePath, nodes]);
+  }
+}
+
+/**
+ * Performs the replacement.
+ */
+Replacement.prototype.replace = function() {
+  for (var i = 0, length = this.queue.length; i < length; i++) {
+    var item = this.queue[i];
+    item[0].replace.apply(item[0], item[1]);
+  }
+};
+
+/**
+ * Incorporates the replacements from the given Replacement into this one.
+ *
+ * @param {Replacement} anotherReplacement
+ */
+Replacement.prototype.and = function(anotherReplacement) {
+  this.queue.push.apply(this.queue, anotherReplacement.queue);
+  return this;
+};
+
+/**
+ * Constructs a Replacement that, when run, will remove the node from the AST.
+ *
+ * @param {NodePath} nodePath
+ * @returns {Replacement}
+ */
+Replacement.removes = function(nodePath) {
+  return new Replacement(nodePath, []);
+};
+
+/**
+ * Constructs a Replacement that, when run, will insert the given nodes after
+ * the one in nodePath.
+ *
+ * @param {NodePath} nodePath
+ * @param {Array.<Node>} nodes
+ * @returns {Replacement}
+ */
+Replacement.adds = function(nodePath, nodes) {
+  return new Replacement(nodePath, [nodePath.node].concat(nodes));
+};
+
+/**
+ * Constructs a Replacement that, when run, swaps the node in nodePath with the
+ * given node or nodes.
+ *
+ * @param {NodePath} nodePath
+ * @param {Node|Array.<Node>} nodes
+ */
+Replacement.swaps = function(nodePath, nodes) {
+  if ({}.toString.call(nodes) !== '[object Array]') {
+    nodes = [nodes];
+  }
+  return new Replacement(nodePath, nodes);
+};
+
+/**
+ * Build replacements for each of the items in nodePaths by passing them to the
+ * given callback. If the callback returns null for a given node path then no
+ * replacement will be created for that node.
+ *
+ * @param {Array.<NodePath>} nodePaths
+ * @param {function(NodePath): ?Replacement} callback
+ * @returns {Replacement}
+ */
+Replacement.map = function(nodePaths, callback) {
+  var result = new Replacement();
+
+  nodePaths.each(function(nodePath) {
+    var replacement = callback(nodePath);
+    if (replacement) {
+      result.and(replacement);
+    }
+  });
+
+  return result;
+};
+
+module.exports = Replacement;
diff --git a/lib/types.js b/lib/types.js
new file mode 100644
index 0000000..fc11efd
--- /dev/null
+++ b/lib/types.js
@@ -0,0 +1,22 @@
+/**
+ * This file really only exists to make JSDoc/WebStorm happy.
+ */
+
+var types = require('ast-types');
+var Scope = require('ast-types/lib/scope');
+var NodePath = types.NodePath;
+
+/** @typedef Object */
+var Node;
+
+/** @typedef Node */
+var Expression;
+
+/** @typedef Expression */
+var CallExpression;
+
+/** @typedef Expression */
+var Literal;
+
+/** @typedef Expression */
+var Identifier;
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9778f87
--- /dev/null
+++ b/package.json
@@ -0,0 +1,37 @@
+{
+  "name": "ast-util",
+  "version": "0.6.0",
+  "description": "Utilities for AST transformers.",
+  "main": "lib/index.js",
+  "directories": {
+    "test": "test"
+  },
+  "scripts": {
+    "test": "mocha -R spec"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/eventualbuddha/ast-util.git"
+  },
+  "keywords": [
+    "ast",
+    "transform",
+    "esnext",
+    "es6",
+    "macros"
+  ],
+  "author": "Brian Donovan",
+  "license": "Apache 2",
+  "bugs": {
+    "url": "https://github.com/eventualbuddha/ast-util/issues"
+  },
+  "homepage": "https://github.com/eventualbuddha/ast-util",
+  "devDependencies": {
+    "mocha": "~2.1.0",
+    "recast": "~0.9.11"
+  },
+  "dependencies": {
+    "ast-types": "~0.6.7",
+    "private": "~0.1.6"
+  }
+}
\ No newline at end of file
diff --git a/test/replacement_test.js b/test/replacement_test.js
new file mode 100644
index 0000000..2e17466
--- /dev/null
+++ b/test/replacement_test.js
@@ -0,0 +1,135 @@
+/* jshint node:true, mocha:true, undef:true, unused:true */
+
+var Replacement = require('../lib').Replacement;
+var assert = require('assert');
+
+describe('Replacement', function() {
+  var nodePath;
+
+  function makeNodePath() {
+    var node = {};
+    return {
+      node: node,
+      replace: function() {
+        this.replacedWith = [].slice.call(arguments);
+      }
+    };
+  }
+
+  beforeEach(function() {
+    nodePath = makeNodePath();
+  });
+
+  describe('.removes', function() {
+    it('creates a Replacement that replaces with nothing', function() {
+      var repl = Replacement.removes(nodePath);
+      repl.replace();
+      assert.equal(nodePath.replacedWith.length, 0);
+    });
+  });
+
+  describe('.swaps', function() {
+    it('creates a Replacement that replaces with another', function() {
+      var swappedIn = {};
+      var repl = Replacement.swaps(nodePath, swappedIn);
+      repl.replace();
+      assert.equal(nodePath.replacedWith.length, 1);
+      assert.strictEqual(nodePath.replacedWith[0], swappedIn);
+    });
+
+    it('creates a Replacement that replaces with a list', function() {
+      var swappedIn1 = {};
+      var swappedIn2 = {};
+      var repl = Replacement.swaps(nodePath, [swappedIn1, swappedIn2]);
+      repl.replace();
+      assert.equal(nodePath.replacedWith.length, 2);
+      assert.strictEqual(nodePath.replacedWith[0], swappedIn1);
+      assert.strictEqual(nodePath.replacedWith[1], swappedIn2);
+    });
+  });
+
+  describe('.adds', function() {
+    it('creates a Replacement that adds additional nodes', function() {
+      var added1 = {};
+      var added2 = {};
+      var repl = Replacement.adds(nodePath, [added1, added2]);
+      repl.replace();
+      assert.equal(nodePath.replacedWith.length, 3);
+      assert.strictEqual(nodePath.replacedWith[0], nodePath.node);
+      assert.strictEqual(nodePath.replacedWith[1], added1);
+      assert.strictEqual(nodePath.replacedWith[2], added2);
+    });
+  });
+
+  describe('.map', function() {
+    var from1;
+    var from2;
+    var nodeArrayPath;
+    var to1;
+    var to2;
+
+    beforeEach(function() {
+      from1 = makeNodePath();
+      from2 = makeNodePath();
+      nodeArrayPath = [from1, from2];
+      nodeArrayPath.each = nodeArrayPath.forEach;
+      to1 = {};
+      to2 = {};
+    });
+
+    it('creates a Replacement with the results of the map', function() {
+      var repl = Replacement.map(nodeArrayPath, function(nodePath) {
+        if (nodePath === from1) {
+          return Replacement.swaps(nodePath, to1);
+        } else if (nodePath === from2) {
+          return Replacement.swaps(nodePath, to2);
+        } else {
+          assert.ok(false, 'unexpected argument to callback: ' + nodePath);
+        }
+      });
+
+      repl.replace();
+
+      assert.equal(from1.replacedWith.length, 1);
+      assert.strictEqual(from1.replacedWith[0], to1);
+      assert.equal(from2.replacedWith.length, 1);
+      assert.strictEqual(from2.replacedWith[0], to2);
+    });
+
+    it('ignores null returns from the callback', function() {
+      var repl = Replacement.map(nodeArrayPath, function(nodePath) {
+        if (nodePath === from1) {
+          return Replacement.swaps(nodePath, to1);
+        } else if (nodePath === from2) {
+          return null;
+        } else {
+          assert.ok(false, 'unexpected argument to callback: ' + nodePath);
+        }
+      });
+
+      repl.replace();
+
+      assert.equal(from1.replacedWith.length, 1);
+      assert.strictEqual(from1.replacedWith[0], to1);
+      assert.equal(from2.replacedWith, null);
+    });
+  });
+
+  describe('#and', function() {
+    it('adds the given Replacement to the receiver', function() {
+      var node1 = {};
+      var anotherNodePath = makeNodePath();
+      var repl = Replacement.swaps(
+        nodePath, node1
+      ).and(
+        Replacement.removes(anotherNodePath)
+      );
+
+      repl.replace();
+
+      assert.equal(nodePath.replacedWith.length, 1);
+      assert.strictEqual(nodePath.replacedWith[0], node1);
+      assert.equal(anotherNodePath.replacedWith.length, 0);
+    });
+  });
+});
diff --git a/test/util_test.js b/test/util_test.js
new file mode 100644
index 0000000..ea53401
--- /dev/null
+++ b/test/util_test.js
@@ -0,0 +1,634 @@
+/* jshint node:true, mocha:true, undef:true, unused:true */
+
+var util = require('../lib');
+
+var recast = require('recast');
+var esprima = require('esprima-fb');
+var types = recast.types;
+var n = types.namedTypes;
+var b = types.builders;
+var NodePath = types.NodePath;
+
+var assert = require('assert');
+
+function parse(source) {
+  return recast.parse(source, { esprima: esprima });
+}
+
+function normalize(source) {
+  return recast.prettyPrint(parse(source)).code;
+}
+
+function processIt(source, callback) {
+  var ast = parse(source);
+  types.visit(ast, {
+    visitIdentifier: function(path) {
+      if (path.value.name === 'IT') {
+        callback.call(path, path.value);
+        return false;
+      }
+      this.traverse(path);
+    }
+  });
+  return ast;
+}
+
+function sameSource(actual, expected, message) {
+  actual = (typeof actual === 'object') ?
+    recast.prettyPrint(actual).code :
+    normalize(actual);
+  expected = (typeof expected === 'object') ?
+    recast.prettyPrint(expected).code :
+    normalize(expected);
+
+  assert.equal(actual, expected, message);
+}
+
+describe('#uniqueIdentifier', function() {
+  // Looks for an `IT` identifier and asserts that the first valid unique
+  // identifier in that scope matches `expected`.
+  function check(source, expected, name) {
+    var identifier;
+
+    processIt(source, function() {
+      identifier = util.uniqueIdentifier(this.scope, name);
+    });
+
+    assert.equal(identifier.name, expected);
+  }
+
+  it('returns the first variable name when there are no variables', function() {
+    check('IT;', '$__0');
+  });
+
+  it('returns the first variable not already declared', function() {
+    check('var $__0, $__1; IT', '$__2');
+  });
+
+  it('returns the first variable not hoisted', function() {
+    check('IT; var $__0, $__1;', '$__2');
+  });
+
+  it('skips conflicting argument names', function() {
+    check('function foo(a, $__0, b) { IT; }', '$__1');
+  });
+
+  it('skips conflicting catch arguments', function() {
+    check('try {} catch ($__0) { IT; }', '$__1');
+  });
+
+  it('skips conflicts from parent scopes', function() {
+    check('var $__0; function outer($__1) { function $__2() { IT; } }', '$__3');
+  });
+
+  it('ignores variables that will shadow inner scopes', function() {
+    check('IT; (function($__0){ var $__1; })();', '$__0');
+  });
+
+  it('skips references to global scope', function() {
+    check('$__0; IT;', '$__1');
+  });
+
+  it('allows specifying a descriptive name', function() {
+    check('IT;', '$__aName', 'aName');
+  });
+
+  it('adds numeric suffixes to descriptive names if need be', function() {
+    check('var $__o; $__o0; (function($__o1){ IT; }); function $__o2(){}', '$__o3', 'o');
+  });
+
+  it('converts descriptive names to identifiers', function() {
+    check('IT;', '$__Hello$there$$how$are$you$', 'Hello there, how are you?');
+  });
+});
+
+describe('#isUsed', function() {
+  function check(source, names, expected) {
+    var ast = parse(source).program;
+    var rootPath = new NodePath({ root: ast });
+    var globalScope = rootPath.get('root').scope;
+
+    if (typeof names === 'string') {
+      names = [names];
+    }
+
+    names.forEach(function(name) {
+      var actual = util.isUsed(globalScope, name);
+
+      assert.ok(
+        actual === expected,
+        'expected `' + name + '` ' +
+        (expected ? '' : 'not ') +
+        'to be used globally by `' + JSON.stringify(source) + '`'
+      );
+    });
+  }
+
+  it('is true for globals declared at the top level', function() {
+    check('var a;', 'a', true);
+  });
+
+  it('is true for globals referenced at the top level', function() {
+    check('a;', 'a', true);
+  });
+
+  it('is false for a variable not declared in scope or referenced anywhere', function() {
+    check('var a, b; c;', 'd', false);
+  });
+
+  it('is false in the global scope for variables declared in inner scopes', function() {
+    check('function foo(a) { var b; }', ['a', 'b'], false);
+  });
+
+  it('is true for global references used within inner scopes', function() {
+    check('function foo() { return a + b; }', ['a', 'b'], true);
+  });
+});
+
+describe('#isReference', function() {
+  function check(source, expected, filter) {
+    processIt(source, function() {
+      if (!filter || filter(this)) {
+        assert.equal(
+          util.isReference(this),
+          expected,
+          'expected `IT` in `' + source + '` to ' +
+            (expected ? '' : 'not ') + 'be a reference'
+        );
+      }
+    });
+  }
+
+  it('is false for variable declaration identifiers', function() {
+    check('var IT;', false);
+  });
+
+  it('is true for global property accesses', function() {
+    check('IT;', true);
+    check('IT.foo', true);
+  });
+
+  it('is true for computed member expressions', function() {
+    check('foo[IT];', true);
+  });
+
+  it('is false for non-computed member expressions', function() {
+    check('foo.IT;', false);
+  });
+
+  it('is false for function parameters', function() {
+    check('(function(IT){})', false);
+  });
+
+  it('is false for catch arguments', function() {
+    check('try{}catch(IT){}', false);
+  });
+
+  it('is true for variable references', function() {
+    check('var IT; foo(IT);', true, function(path) {
+      return n.CallExpression.check(path.parent.value);
+    });
+  });
+
+  it('is false for property keys', function() {
+    check('({IT: 1})', false);
+  });
+
+  it('is true for property values', function() {
+    check('({a: IT})', true);
+    check('({IT: IT})', true, function(path) {
+      return path.parent.value === path.value;
+    });
+  });
+
+  it('is true for variable initial values', function() {
+    check('var a = IT;', true);
+  });
+
+  it('is false for labeled statements', function() {
+    check('IT: 1', false);
+    check('IT: IT', true, function(path) {
+      return n.ExpressionStatement.check(path.parent.value);
+    });
+  });
+
+  it('can check names', function() {
+    types.visit(parse('a'), {
+      visitIdentifier: function(path) {
+        assert.ok(util.isReference(path, 'a'));
+        assert.ok(!util.isReference(path, 'b'));
+        return false;
+      }
+    });
+  });
+
+  it('is false for class names', function() {
+    check('class IT {}', false);
+    check('var foo = class IT {};', false);
+  });
+
+  it('is false for method definition identifiers', function() {
+    check('class Foo { IT(){} }', false);
+  });
+
+  it('is false for function definition identifiers', function() {
+    check('function IT(){}', false);
+  });
+
+  it('is true for superclass identifiers', function() {
+    check('class Foo extends IT {}', true);
+  });
+
+  it('is false for name of import specifiers', function() {
+    check('import IT from "IT";', false);
+    check('import { IT } from "IT";', false);
+    check('import { foo as IT } from "IT";', false);
+    check('import { IT as foo } from "IT";', false);
+  });
+
+  it('is true for export default', function() {
+    check('export default IT;', true);
+  });
+});
+
+describe('#injectVariable', function() {
+  it('creates a variable in the scope node', function() {
+    var identifier = b.identifier('a');
+    var ast = processIt('function foo(){ IT; }', function() {
+      assert.strictEqual(
+        util.injectVariable(this.scope, identifier),
+        identifier
+      );
+    });
+
+    sameSource(ast, 'function foo(){ var a; IT; }');
+  });
+
+  it('marks the variable as bound in the given scope', function() {
+    var identifier = b.identifier('a');
+    var scope;
+
+    processIt('function foo(){ IT; }', function() {
+      scope = this.scope;
+      assert.strictEqual(
+        util.injectVariable(scope, identifier),
+        identifier
+      );
+    });
+
+    assert.ok(scope.declares('a'), 'injected variables should count as declared');
+    assert.ok(!scope.parent.declares('a'), 'injected variables should not pollute parent scopes');
+  });
+
+  it('can create a variable with an initial value', function() {
+    var ast = processIt('IT;', function() {
+      util.injectVariable(
+        this.scope,
+        b.identifier('hasOwnProp'),
+        b.memberExpression(
+          b.memberExpression(
+            b.identifier('Object'),
+            b.identifier('prototype'),
+            false
+          ),
+          b.identifier('hasOwnProperty'),
+          false
+        )
+      );
+    });
+
+    sameSource(ast, 'var hasOwnProp = Object.prototype.hasOwnProperty; IT;');
+  });
+
+  it('can inject a variable in a scope at a position that is later replaced', function() {
+    var ast = parse('var a;');
+
+    types.visit(ast, {
+      visitVariableDeclaration: function(path) {
+        util.injectVariable(path.scope, b.identifier('b'));
+        return b.expressionStatement(
+          b.callExpression(b.identifier('replacement'), [])
+        );
+      }
+    });
+
+    sameSource(
+      ast,
+      'var b; replacement();'
+    );
+  });
+
+  it('injects after any global "use strict" pragma', function() {
+    var ast = processIt('"use strict"; IT;', function() {
+      util.injectVariable(
+        this.scope,
+        b.identifier('a'),
+        b.literal(1)
+      )
+    });
+
+    sameSource(ast, '"use strict"; var a = 1; IT;');
+  });
+
+  it('injects after any local "use strict" pragma', function() {
+    var ast = processIt('function getIt() { "use strict"; return IT; }', function() {
+      util.injectVariable(
+        this.scope,
+        b.identifier('a'),
+        b.literal(1)
+      )
+    });
+
+    sameSource(ast, 'function getIt() { "use strict"; var a = 1; return IT; }');
+  });
+});
+
+describe('#injectShared', function() {
+  var hasOwnPropAST = b.memberExpression(
+    b.memberExpression(
+      b.identifier('Object'),
+      b.identifier('prototype'),
+      false
+    ),
+    b.identifier('hasOwnProperty'),
+    false
+  );
+
+  var arraySliceAST = b.memberExpression(
+    b.memberExpression(
+      b.identifier('Array'),
+      b.identifier('prototype'),
+      false
+    ),
+    b.identifier('slice'),
+    false
+  );
+
+  it('can inject a shared value', function() {
+    var ast = processIt('IT;', function() {
+      assert.equal(
+        util.injectShared(
+          this.scope,
+          'hasOwnProperty',
+          hasOwnPropAST
+        ).name,
+        '$__hasOwnProperty'
+      );
+
+      // do it again under the same name
+      assert.equal(
+        util.injectShared(
+          this.scope,
+          'hasOwnProperty',
+          hasOwnPropAST
+        ).name,
+        '$__hasOwnProperty'
+      );
+
+      // add a different shared variable
+      assert.equal(
+        util.injectShared(
+          this.scope,
+          'arraySlice',
+          arraySliceAST
+        ).name,
+        '$__arraySlice'
+      );
+    });
+
+    sameSource(
+      ast,
+      'var $__arraySlice = Array.prototype.slice;' +
+      'var $__hasOwnProperty = Object.prototype.hasOwnProperty;' +
+      'IT;'
+    );
+  });
+});
+
+describe('#callHasOwnProperty', function() {
+  it('returns a CallExpression with the given object and property', function() {
+    var ast = processIt('IT;', function(node) {
+      this.replace(util.callHasOwnProperty(this.scope, node, 'is'));
+    });
+
+    sameSource(
+      ast,
+      'var $__Object$prototype$hasOwnProperty = Object.prototype.hasOwnProperty;' +
+      '$__Object$prototype$hasOwnProperty.call(IT, "is");'
+    );
+  });
+});
+
+describe('#callGetOwnPropertyDescriptor', function() {
+  it('returns a CallExpression with the given object and property', function() {
+    var ast = processIt('IT;', function(node) {
+      this.replace(util.callGetOwnPropertyDescriptor(this.scope, node, 'is'));
+    });
+
+    sameSource(
+      ast,
+      'var $__Object$getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;' +
+      '$__Object$getOwnPropertyDescriptor(IT, "is");'
+    );
+  });
+});
+
+describe('#callGetPrototypeOf', function() {
+  it('returns a CallExpression with the given object', function() {
+    var ast = processIt('IT;', function(node) {
+      this.replace(util.callGetPrototypeOf(this.scope, node));
+    });
+
+    sameSource(
+      ast,
+      'var $__Object$getPrototypeOf = Object.getPrototypeOf;' +
+      '$__Object$getPrototypeOf(IT);'
+    );
+  });
+});
+
+describe('#callArraySlice', function() {
+  it('returns a CallExpression with the given object and omits missing begin/end', function() {
+    var ast = processIt('IT;', function(node) {
+      this.replace(util.callArraySlice(this.scope, node));
+    });
+
+    sameSource(
+      ast,
+      'var $__Array$prototype$slice = Array.prototype.slice;' +
+      '$__Array$prototype$slice.call(IT);'
+    );
+  });
+
+  it('returns a CallExpression with the given object and begin/end', function() {
+    var ast = processIt('IT;', function(node) {
+      this.replace(util.callArraySlice(this.scope, node, 1, 2));
+    });
+
+    sameSource(
+      ast,
+      'var $__Array$prototype$slice = Array.prototype.slice;' +
+      '$__Array$prototype$slice.call(IT, 1, 2);'
+    );
+  });
+});
+
+describe('#callFunctionBind', function() {
+  it('uses call when given args as an array', function() {
+    var ast = processIt('IT;', function(node) {
+      this.replace(util.callFunctionBind(
+        this.scope,
+        node,
+        b.thisExpression(),
+        [b.literal(1)]
+      ));
+    });
+
+    sameSource(
+      ast,
+      'var $__Function$prototype$bind = Function.prototype.bind;' +
+      '$__Function$prototype$bind.call(IT, this, 1);'
+    );
+  });
+
+  it('uses apply when given args as an expression', function() {
+    var ast = processIt('IT;', function(node) {
+      this.replace(util.callFunctionBind(
+        this.scope,
+        node,
+        b.thisExpression(),
+        b.identifier('args')
+      ));
+    });
+
+    sameSource(
+      ast,
+      'var $__Function$prototype$bind = Function.prototype.bind;' +
+      '$__Function$prototype$bind.apply(IT, [this].concat(args));'
+    );
+  });
+});
+
+describe('#callGetIterator', function() {
+  it('calls the get iterator helper', function() {
+    var ast = processIt('IT;', function(node) {
+      this.replace(util.callGetIterator(
+        this.scope, node
+      ));
+    });
+
+    sameSource(
+      ast,
+      'var $__getIterator = function(iterable) {' +
+      '    var sym = typeof Symbol === "function" && Symbol.iterator || "@@iterator";' +
+      '    if (typeof iterable[sym] === "function") {' +
+      '        return iterable[sym]();' +
+      '    } else if (typeof iterable === "object" || typeof iterable === "function") {' +
+      '        return $__getArrayIterator(iterable);' +
+      '    } else {' +
+      '        throw new TypeError();' +
+      '    }' +
+      '};' +
+      'var $__getArrayIterator = function(array) {' +
+      '    var index = 0;' +
+      '    return {' +
+      '        next: function() {' +
+      '            if (index < array.length) {' +
+      '                return {' +
+      '                    done: false,' +
+      '                    value: array[index++]' +
+      '                };' +
+      '            } else {' +
+      '                return {' +
+      '                    done: true,' +
+      '                    value: void 0' +
+      '                };' +
+      '            }' +
+      '        }' +
+      '    };' +
+      '};' +
+      '$__getIterator(IT);'
+    );
+  });
+});
+
+describe('#callGetIteratorRange', function() {
+  it('calls the get iterator range helper', function() {
+    var ast = processIt('IT;', function(node) {
+      this.replace(util.callGetIteratorRange(
+        this.scope, node, b.literal(1), b.literal(2), b.literal(3)
+      ));
+    });
+
+    sameSource(
+      ast,
+      'var $__getIteratorRange = function(iterator, index, begin, len) {' +
+      '    if (index > begin) {' +
+      '        throw new RangeError();' +
+      '    }' +
+      '    if (typeof len === \'undefined\') {' +
+      '        len = Infinity;' +
+      '    }' +
+      '    var range = [], end = begin + len;' +
+      '    while (index < end) {' +
+      '        var next = iterator.next();' +
+      '        if (next.done) {' +
+      '            break;' +
+      '        }' +
+      '        if (index >= begin) {' +
+      '            range.push(next.value);' +
+      '        }' +
+      '        index++;' +
+      '    }' +
+      '    return {' +
+      '        range: range,' +
+      '        index: index' +
+      '    };' +
+      '};'+
+      '$__getIteratorRange(IT, 1, 2, 3);'
+    );
+  });
+});
+
+describe('#getGlobals', function() {
+  function check(source, globals) {
+    assert.deepEqual(
+      util.getGlobals(parse(source).program).map(function(identifier) {
+        return identifier.name;
+      }),
+      globals
+    );
+  }
+
+  it('is empty when there are no references', function() {
+    check('', []);
+  });
+
+  it('has references from the top level', function() {
+    check('a; b(c + d);', ['a', 'b', 'c', 'd']);
+  });
+
+  it('ignores references that have associated variable declarations', function() {
+    check('var a; a + b;', ['b']);
+  });
+
+  it('ignores references that have associated function params', function() {
+    check('function area(r){ return Math.PI * Math.pow(r, 2); }', ['Math']);
+  });
+
+  it('ignores references that have associated catch arguments', function() {
+    check('try {} catch (a) { a + b["c"]; }', ['b']);
+  });
+
+  it('ignores references to declared functions', function() {
+    check('foo(); function foo() {}', []);
+  });
+
+  it('ignores references to things declared in outer scopes', function() {
+    check('var a; function foo() { return a; }', []);
+  });
+
+  it('ignores references to function expression identifiers within the function', function() {
+    check('var a = function b(){ return b; };', []);
+  });
+});

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-ast-util.git



More information about the Pkg-javascript-commits mailing list