pax_global_header00006660000000000000000000000064124510332760014515gustar00rootroot0000000000000052 comment=6ea220d1999979c970c6a0a11faf4c9db4c1fddf ast-util-0.6.0/000077500000000000000000000000001245103327600132625ustar00rootroot00000000000000ast-util-0.6.0/.travis.yml000066400000000000000000000000611245103327600153700ustar00rootroot00000000000000language: node_js node_js: - "0.11" - "0.10" ast-util-0.6.0/Makefile000066400000000000000000000025501245103327600147240ustar00rootroot00000000000000SHELL=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 ast-util-0.6.0/README.md000066400000000000000000000165751245103327600145570ustar00rootroot00000000000000# ast-util Utilities for AST transformers. ## Install ``` $ npm install [--save] ast-util ``` ## API # callArraySlice(scope, node[, begin, end]) Returns a call to `Array.prototype.slice` with `node` as the context and `begin` and `end` as the arguments to `slice`. # callFunctionBind(scope, fn, context[, args]) 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`. # callGet(scope, object, property, receiver) The [[Get]] internal method on objects would look something like [helpers/get.js](helpers/get.js). # callGetOwnPropertyDescriptor(scope, object, property) Returns a call to `Object.getOwnPropertyDescriptor` with the given `object` and `property`. # callGetPrototypeOf(scope, object) Returns a call to `Object.getPrototypeOf` with the given `object`. # callHasOwnProperty(scope, node, property) Returns a call to `hasOwnProperty` with `node` as the context and `property` as the property to check. # callSharedMethod(scope, callee, args) 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. # callSharedMethodWithContext(scope, callee, context, args) 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. # getGlobals(ast) 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`. # identifierForString(string) Generate a safe JavaScript identifier for the given string. # injectShared(scope, name, expression) 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. # injectVariable(scope, identifier[, init]) Injects a variable with the given `identifier` into the given `scope` as a `var` declaration with an optional initial value. # isReference(path) 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. # isUsed(scope, name) 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) { } ``` # sharedFor(scope, name) 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. # uniqueIdentifier(scope[, name]) 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 ast-util-0.6.0/bin/000077500000000000000000000000001245103327600140325ustar00rootroot00000000000000ast-util-0.6.0/bin/make-builder000077500000000000000000000202551245103327600163250ustar00rootroot00000000000000#!/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); }); ast-util-0.6.0/examples/000077500000000000000000000000001245103327600151005ustar00rootroot00000000000000ast-util-0.6.0/examples/swap-macro.js000077500000000000000000000051111245103327600175100ustar00rootroot00000000000000#!/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); }); ast-util-0.6.0/helpers/000077500000000000000000000000001245103327600147245ustar00rootroot00000000000000ast-util-0.6.0/helpers/get.js000066400000000000000000000007731245103327600160500ustar00rootroot00000000000000(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); } }); ast-util-0.6.0/helpers/getArrayIterator.js000066400000000000000000000004531245103327600205540ustar00rootroot00000000000000(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 }; } } }; }); ast-util-0.6.0/helpers/getIterator.js000066400000000000000000000005211245103327600175510ustar00rootroot00000000000000(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(); } }); ast-util-0.6.0/helpers/getIteratorRange.js000066400000000000000000000006511245103327600205320ustar00rootroot00000000000000(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 }; }); ast-util-0.6.0/lib/000077500000000000000000000000001245103327600140305ustar00rootroot00000000000000ast-util-0.6.0/lib/helpers/000077500000000000000000000000001245103327600154725ustar00rootroot00000000000000ast-util-0.6.0/lib/helpers/get.js000066400000000000000000000073461245103327600166210ustar00rootroot00000000000000var 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 )}; ast-util-0.6.0/lib/helpers/getArrayIterator.js000066400000000000000000000046671245103327600213350ustar00rootroot00000000000000var 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 )}; ast-util-0.6.0/lib/helpers/getIterator.js000066400000000000000000000046251245103327600203300ustar00rootroot00000000000000var 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 )}; ast-util-0.6.0/lib/helpers/getIteratorRange.js000066400000000000000000000063571245103327600213110ustar00rootroot00000000000000var 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 )}; ast-util-0.6.0/lib/index.js000066400000000000000000000371531245103327600155060ustar00rootroot00000000000000/* 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.} 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.} 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.} 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.} */ 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; ast-util-0.6.0/lib/replacement.js000066400000000000000000000044711245103327600166730ustar00rootroot00000000000000/* 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.} 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.} 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.} 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.} 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; ast-util-0.6.0/lib/types.js000066400000000000000000000006011245103327600155270ustar00rootroot00000000000000/** * 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; ast-util-0.6.0/package.json000066400000000000000000000013531245103327600155520ustar00rootroot00000000000000{ "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" } }ast-util-0.6.0/test/000077500000000000000000000000001245103327600142415ustar00rootroot00000000000000ast-util-0.6.0/test/replacement_test.js000066400000000000000000000076621245103327600201500ustar00rootroot00000000000000/* 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); }); }); }); ast-util-0.6.0/test/util_test.js000066400000000000000000000417231245103327600166220ustar00rootroot00000000000000/* 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; };', []); }); });