[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