Spaces:
Running
Running
/* | |
Copyright (C) 2012-2013 Yusuke Suzuki <utatane.tea@gmail.com> | |
Copyright (C) 2012 Ariya Hidayat <ariya.hidayat@gmail.com> | |
Redistribution and use in source and binary forms, with or without | |
modification, are permitted provided that the following conditions are met: | |
* Redistributions of source code must retain the above copyright | |
notice, this list of conditions and the following disclaimer. | |
* Redistributions in binary form must reproduce the above copyright | |
notice, this list of conditions and the following disclaimer in the | |
documentation and/or other materials provided with the distribution. | |
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY | |
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | |
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF | |
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
*/ | |
/*jslint vars:false, bitwise:true*/ | |
/*jshint indent:4*/ | |
/*global exports:true*/ | |
(function clone(exports) { | |
'use strict'; | |
var Syntax, | |
VisitorOption, | |
VisitorKeys, | |
BREAK, | |
SKIP, | |
REMOVE; | |
function deepCopy(obj) { | |
var ret = {}, key, val; | |
for (key in obj) { | |
if (obj.hasOwnProperty(key)) { | |
val = obj[key]; | |
if (typeof val === 'object' && val !== null) { | |
ret[key] = deepCopy(val); | |
} else { | |
ret[key] = val; | |
} | |
} | |
} | |
return ret; | |
} | |
// based on LLVM libc++ upper_bound / lower_bound | |
// MIT License | |
function upperBound(array, func) { | |
var diff, len, i, current; | |
len = array.length; | |
i = 0; | |
while (len) { | |
diff = len >>> 1; | |
current = i + diff; | |
if (func(array[current])) { | |
len = diff; | |
} else { | |
i = current + 1; | |
len -= diff + 1; | |
} | |
} | |
return i; | |
} | |
Syntax = { | |
AssignmentExpression: 'AssignmentExpression', | |
AssignmentPattern: 'AssignmentPattern', | |
ArrayExpression: 'ArrayExpression', | |
ArrayPattern: 'ArrayPattern', | |
ArrowFunctionExpression: 'ArrowFunctionExpression', | |
AwaitExpression: 'AwaitExpression', // CAUTION: It's deferred to ES7. | |
BlockStatement: 'BlockStatement', | |
BinaryExpression: 'BinaryExpression', | |
BreakStatement: 'BreakStatement', | |
CallExpression: 'CallExpression', | |
CatchClause: 'CatchClause', | |
ClassBody: 'ClassBody', | |
ClassDeclaration: 'ClassDeclaration', | |
ClassExpression: 'ClassExpression', | |
ComprehensionBlock: 'ComprehensionBlock', // CAUTION: It's deferred to ES7. | |
ComprehensionExpression: 'ComprehensionExpression', // CAUTION: It's deferred to ES7. | |
ConditionalExpression: 'ConditionalExpression', | |
ContinueStatement: 'ContinueStatement', | |
DebuggerStatement: 'DebuggerStatement', | |
DirectiveStatement: 'DirectiveStatement', | |
DoWhileStatement: 'DoWhileStatement', | |
EmptyStatement: 'EmptyStatement', | |
ExportAllDeclaration: 'ExportAllDeclaration', | |
ExportDefaultDeclaration: 'ExportDefaultDeclaration', | |
ExportNamedDeclaration: 'ExportNamedDeclaration', | |
ExportSpecifier: 'ExportSpecifier', | |
ExpressionStatement: 'ExpressionStatement', | |
ForStatement: 'ForStatement', | |
ForInStatement: 'ForInStatement', | |
ForOfStatement: 'ForOfStatement', | |
FunctionDeclaration: 'FunctionDeclaration', | |
FunctionExpression: 'FunctionExpression', | |
GeneratorExpression: 'GeneratorExpression', // CAUTION: It's deferred to ES7. | |
Identifier: 'Identifier', | |
IfStatement: 'IfStatement', | |
ImportExpression: 'ImportExpression', | |
ImportDeclaration: 'ImportDeclaration', | |
ImportDefaultSpecifier: 'ImportDefaultSpecifier', | |
ImportNamespaceSpecifier: 'ImportNamespaceSpecifier', | |
ImportSpecifier: 'ImportSpecifier', | |
Literal: 'Literal', | |
LabeledStatement: 'LabeledStatement', | |
LogicalExpression: 'LogicalExpression', | |
MemberExpression: 'MemberExpression', | |
MetaProperty: 'MetaProperty', | |
MethodDefinition: 'MethodDefinition', | |
ModuleSpecifier: 'ModuleSpecifier', | |
NewExpression: 'NewExpression', | |
ObjectExpression: 'ObjectExpression', | |
ObjectPattern: 'ObjectPattern', | |
Program: 'Program', | |
Property: 'Property', | |
RestElement: 'RestElement', | |
ReturnStatement: 'ReturnStatement', | |
SequenceExpression: 'SequenceExpression', | |
SpreadElement: 'SpreadElement', | |
Super: 'Super', | |
SwitchStatement: 'SwitchStatement', | |
SwitchCase: 'SwitchCase', | |
TaggedTemplateExpression: 'TaggedTemplateExpression', | |
TemplateElement: 'TemplateElement', | |
TemplateLiteral: 'TemplateLiteral', | |
ThisExpression: 'ThisExpression', | |
ThrowStatement: 'ThrowStatement', | |
TryStatement: 'TryStatement', | |
UnaryExpression: 'UnaryExpression', | |
UpdateExpression: 'UpdateExpression', | |
VariableDeclaration: 'VariableDeclaration', | |
VariableDeclarator: 'VariableDeclarator', | |
WhileStatement: 'WhileStatement', | |
WithStatement: 'WithStatement', | |
YieldExpression: 'YieldExpression' | |
}; | |
VisitorKeys = { | |
AssignmentExpression: ['left', 'right'], | |
AssignmentPattern: ['left', 'right'], | |
ArrayExpression: ['elements'], | |
ArrayPattern: ['elements'], | |
ArrowFunctionExpression: ['params', 'body'], | |
AwaitExpression: ['argument'], // CAUTION: It's deferred to ES7. | |
BlockStatement: ['body'], | |
BinaryExpression: ['left', 'right'], | |
BreakStatement: ['label'], | |
CallExpression: ['callee', 'arguments'], | |
CatchClause: ['param', 'body'], | |
ClassBody: ['body'], | |
ClassDeclaration: ['id', 'superClass', 'body'], | |
ClassExpression: ['id', 'superClass', 'body'], | |
ComprehensionBlock: ['left', 'right'], // CAUTION: It's deferred to ES7. | |
ComprehensionExpression: ['blocks', 'filter', 'body'], // CAUTION: It's deferred to ES7. | |
ConditionalExpression: ['test', 'consequent', 'alternate'], | |
ContinueStatement: ['label'], | |
DebuggerStatement: [], | |
DirectiveStatement: [], | |
DoWhileStatement: ['body', 'test'], | |
EmptyStatement: [], | |
ExportAllDeclaration: ['source'], | |
ExportDefaultDeclaration: ['declaration'], | |
ExportNamedDeclaration: ['declaration', 'specifiers', 'source'], | |
ExportSpecifier: ['exported', 'local'], | |
ExpressionStatement: ['expression'], | |
ForStatement: ['init', 'test', 'update', 'body'], | |
ForInStatement: ['left', 'right', 'body'], | |
ForOfStatement: ['left', 'right', 'body'], | |
FunctionDeclaration: ['id', 'params', 'body'], | |
FunctionExpression: ['id', 'params', 'body'], | |
GeneratorExpression: ['blocks', 'filter', 'body'], // CAUTION: It's deferred to ES7. | |
Identifier: [], | |
IfStatement: ['test', 'consequent', 'alternate'], | |
ImportExpression: ['source'], | |
ImportDeclaration: ['specifiers', 'source'], | |
ImportDefaultSpecifier: ['local'], | |
ImportNamespaceSpecifier: ['local'], | |
ImportSpecifier: ['imported', 'local'], | |
Literal: [], | |
LabeledStatement: ['label', 'body'], | |
LogicalExpression: ['left', 'right'], | |
MemberExpression: ['object', 'property'], | |
MetaProperty: ['meta', 'property'], | |
MethodDefinition: ['key', 'value'], | |
ModuleSpecifier: [], | |
NewExpression: ['callee', 'arguments'], | |
ObjectExpression: ['properties'], | |
ObjectPattern: ['properties'], | |
Program: ['body'], | |
Property: ['key', 'value'], | |
RestElement: [ 'argument' ], | |
ReturnStatement: ['argument'], | |
SequenceExpression: ['expressions'], | |
SpreadElement: ['argument'], | |
Super: [], | |
SwitchStatement: ['discriminant', 'cases'], | |
SwitchCase: ['test', 'consequent'], | |
TaggedTemplateExpression: ['tag', 'quasi'], | |
TemplateElement: [], | |
TemplateLiteral: ['quasis', 'expressions'], | |
ThisExpression: [], | |
ThrowStatement: ['argument'], | |
TryStatement: ['block', 'handler', 'finalizer'], | |
UnaryExpression: ['argument'], | |
UpdateExpression: ['argument'], | |
VariableDeclaration: ['declarations'], | |
VariableDeclarator: ['id', 'init'], | |
WhileStatement: ['test', 'body'], | |
WithStatement: ['object', 'body'], | |
YieldExpression: ['argument'] | |
}; | |
// unique id | |
BREAK = {}; | |
SKIP = {}; | |
REMOVE = {}; | |
VisitorOption = { | |
Break: BREAK, | |
Skip: SKIP, | |
Remove: REMOVE | |
}; | |
function Reference(parent, key) { | |
this.parent = parent; | |
this.key = key; | |
} | |
Reference.prototype.replace = function replace(node) { | |
this.parent[this.key] = node; | |
}; | |
Reference.prototype.remove = function remove() { | |
if (Array.isArray(this.parent)) { | |
this.parent.splice(this.key, 1); | |
return true; | |
} else { | |
this.replace(null); | |
return false; | |
} | |
}; | |
function Element(node, path, wrap, ref) { | |
this.node = node; | |
this.path = path; | |
this.wrap = wrap; | |
this.ref = ref; | |
} | |
function Controller() { } | |
// API: | |
// return property path array from root to current node | |
Controller.prototype.path = function path() { | |
var i, iz, j, jz, result, element; | |
function addToPath(result, path) { | |
if (Array.isArray(path)) { | |
for (j = 0, jz = path.length; j < jz; ++j) { | |
result.push(path[j]); | |
} | |
} else { | |
result.push(path); | |
} | |
} | |
// root node | |
if (!this.__current.path) { | |
return null; | |
} | |
// first node is sentinel, second node is root element | |
result = []; | |
for (i = 2, iz = this.__leavelist.length; i < iz; ++i) { | |
element = this.__leavelist[i]; | |
addToPath(result, element.path); | |
} | |
addToPath(result, this.__current.path); | |
return result; | |
}; | |
// API: | |
// return type of current node | |
Controller.prototype.type = function () { | |
var node = this.current(); | |
return node.type || this.__current.wrap; | |
}; | |
// API: | |
// return array of parent elements | |
Controller.prototype.parents = function parents() { | |
var i, iz, result; | |
// first node is sentinel | |
result = []; | |
for (i = 1, iz = this.__leavelist.length; i < iz; ++i) { | |
result.push(this.__leavelist[i].node); | |
} | |
return result; | |
}; | |
// API: | |
// return current node | |
Controller.prototype.current = function current() { | |
return this.__current.node; | |
}; | |
Controller.prototype.__execute = function __execute(callback, element) { | |
var previous, result; | |
result = undefined; | |
previous = this.__current; | |
this.__current = element; | |
this.__state = null; | |
if (callback) { | |
result = callback.call(this, element.node, this.__leavelist[this.__leavelist.length - 1].node); | |
} | |
this.__current = previous; | |
return result; | |
}; | |
// API: | |
// notify control skip / break | |
Controller.prototype.notify = function notify(flag) { | |
this.__state = flag; | |
}; | |
// API: | |
// skip child nodes of current node | |
Controller.prototype.skip = function () { | |
this.notify(SKIP); | |
}; | |
// API: | |
// break traversals | |
Controller.prototype['break'] = function () { | |
this.notify(BREAK); | |
}; | |
// API: | |
// remove node | |
Controller.prototype.remove = function () { | |
this.notify(REMOVE); | |
}; | |
Controller.prototype.__initialize = function(root, visitor) { | |
this.visitor = visitor; | |
this.root = root; | |
this.__worklist = []; | |
this.__leavelist = []; | |
this.__current = null; | |
this.__state = null; | |
this.__fallback = null; | |
if (visitor.fallback === 'iteration') { | |
this.__fallback = Object.keys; | |
} else if (typeof visitor.fallback === 'function') { | |
this.__fallback = visitor.fallback; | |
} | |
this.__keys = VisitorKeys; | |
if (visitor.keys) { | |
this.__keys = Object.assign(Object.create(this.__keys), visitor.keys); | |
} | |
}; | |
function isNode(node) { | |
if (node == null) { | |
return false; | |
} | |
return typeof node === 'object' && typeof node.type === 'string'; | |
} | |
function isProperty(nodeType, key) { | |
return (nodeType === Syntax.ObjectExpression || nodeType === Syntax.ObjectPattern) && 'properties' === key; | |
} | |
Controller.prototype.traverse = function traverse(root, visitor) { | |
var worklist, | |
leavelist, | |
element, | |
node, | |
nodeType, | |
ret, | |
key, | |
current, | |
current2, | |
candidates, | |
candidate, | |
sentinel; | |
this.__initialize(root, visitor); | |
sentinel = {}; | |
// reference | |
worklist = this.__worklist; | |
leavelist = this.__leavelist; | |
// initialize | |
worklist.push(new Element(root, null, null, null)); | |
leavelist.push(new Element(null, null, null, null)); | |
while (worklist.length) { | |
element = worklist.pop(); | |
if (element === sentinel) { | |
element = leavelist.pop(); | |
ret = this.__execute(visitor.leave, element); | |
if (this.__state === BREAK || ret === BREAK) { | |
return; | |
} | |
continue; | |
} | |
if (element.node) { | |
ret = this.__execute(visitor.enter, element); | |
if (this.__state === BREAK || ret === BREAK) { | |
return; | |
} | |
worklist.push(sentinel); | |
leavelist.push(element); | |
if (this.__state === SKIP || ret === SKIP) { | |
continue; | |
} | |
node = element.node; | |
nodeType = node.type || element.wrap; | |
candidates = this.__keys[nodeType]; | |
if (!candidates) { | |
if (this.__fallback) { | |
candidates = this.__fallback(node); | |
} else { | |
throw new Error('Unknown node type ' + nodeType + '.'); | |
} | |
} | |
current = candidates.length; | |
while ((current -= 1) >= 0) { | |
key = candidates[current]; | |
candidate = node[key]; | |
if (!candidate) { | |
continue; | |
} | |
if (Array.isArray(candidate)) { | |
current2 = candidate.length; | |
while ((current2 -= 1) >= 0) { | |
if (!candidate[current2]) { | |
continue; | |
} | |
if (isProperty(nodeType, candidates[current])) { | |
element = new Element(candidate[current2], [key, current2], 'Property', null); | |
} else if (isNode(candidate[current2])) { | |
element = new Element(candidate[current2], [key, current2], null, null); | |
} else { | |
continue; | |
} | |
worklist.push(element); | |
} | |
} else if (isNode(candidate)) { | |
worklist.push(new Element(candidate, key, null, null)); | |
} | |
} | |
} | |
} | |
}; | |
Controller.prototype.replace = function replace(root, visitor) { | |
var worklist, | |
leavelist, | |
node, | |
nodeType, | |
target, | |
element, | |
current, | |
current2, | |
candidates, | |
candidate, | |
sentinel, | |
outer, | |
key; | |
function removeElem(element) { | |
var i, | |
key, | |
nextElem, | |
parent; | |
if (element.ref.remove()) { | |
// When the reference is an element of an array. | |
key = element.ref.key; | |
parent = element.ref.parent; | |
// If removed from array, then decrease following items' keys. | |
i = worklist.length; | |
while (i--) { | |
nextElem = worklist[i]; | |
if (nextElem.ref && nextElem.ref.parent === parent) { | |
if (nextElem.ref.key < key) { | |
break; | |
} | |
--nextElem.ref.key; | |
} | |
} | |
} | |
} | |
this.__initialize(root, visitor); | |
sentinel = {}; | |
// reference | |
worklist = this.__worklist; | |
leavelist = this.__leavelist; | |
// initialize | |
outer = { | |
root: root | |
}; | |
element = new Element(root, null, null, new Reference(outer, 'root')); | |
worklist.push(element); | |
leavelist.push(element); | |
while (worklist.length) { | |
element = worklist.pop(); | |
if (element === sentinel) { | |
element = leavelist.pop(); | |
target = this.__execute(visitor.leave, element); | |
// node may be replaced with null, | |
// so distinguish between undefined and null in this place | |
if (target !== undefined && target !== BREAK && target !== SKIP && target !== REMOVE) { | |
// replace | |
element.ref.replace(target); | |
} | |
if (this.__state === REMOVE || target === REMOVE) { | |
removeElem(element); | |
} | |
if (this.__state === BREAK || target === BREAK) { | |
return outer.root; | |
} | |
continue; | |
} | |
target = this.__execute(visitor.enter, element); | |
// node may be replaced with null, | |
// so distinguish between undefined and null in this place | |
if (target !== undefined && target !== BREAK && target !== SKIP && target !== REMOVE) { | |
// replace | |
element.ref.replace(target); | |
element.node = target; | |
} | |
if (this.__state === REMOVE || target === REMOVE) { | |
removeElem(element); | |
element.node = null; | |
} | |
if (this.__state === BREAK || target === BREAK) { | |
return outer.root; | |
} | |
// node may be null | |
node = element.node; | |
if (!node) { | |
continue; | |
} | |
worklist.push(sentinel); | |
leavelist.push(element); | |
if (this.__state === SKIP || target === SKIP) { | |
continue; | |
} | |
nodeType = node.type || element.wrap; | |
candidates = this.__keys[nodeType]; | |
if (!candidates) { | |
if (this.__fallback) { | |
candidates = this.__fallback(node); | |
} else { | |
throw new Error('Unknown node type ' + nodeType + '.'); | |
} | |
} | |
current = candidates.length; | |
while ((current -= 1) >= 0) { | |
key = candidates[current]; | |
candidate = node[key]; | |
if (!candidate) { | |
continue; | |
} | |
if (Array.isArray(candidate)) { | |
current2 = candidate.length; | |
while ((current2 -= 1) >= 0) { | |
if (!candidate[current2]) { | |
continue; | |
} | |
if (isProperty(nodeType, candidates[current])) { | |
element = new Element(candidate[current2], [key, current2], 'Property', new Reference(candidate, current2)); | |
} else if (isNode(candidate[current2])) { | |
element = new Element(candidate[current2], [key, current2], null, new Reference(candidate, current2)); | |
} else { | |
continue; | |
} | |
worklist.push(element); | |
} | |
} else if (isNode(candidate)) { | |
worklist.push(new Element(candidate, key, null, new Reference(node, key))); | |
} | |
} | |
} | |
return outer.root; | |
}; | |
function traverse(root, visitor) { | |
var controller = new Controller(); | |
return controller.traverse(root, visitor); | |
} | |
function replace(root, visitor) { | |
var controller = new Controller(); | |
return controller.replace(root, visitor); | |
} | |
function extendCommentRange(comment, tokens) { | |
var target; | |
target = upperBound(tokens, function search(token) { | |
return token.range[0] > comment.range[0]; | |
}); | |
comment.extendedRange = [comment.range[0], comment.range[1]]; | |
if (target !== tokens.length) { | |
comment.extendedRange[1] = tokens[target].range[0]; | |
} | |
target -= 1; | |
if (target >= 0) { | |
comment.extendedRange[0] = tokens[target].range[1]; | |
} | |
return comment; | |
} | |
function attachComments(tree, providedComments, tokens) { | |
// At first, we should calculate extended comment ranges. | |
var comments = [], comment, len, i, cursor; | |
if (!tree.range) { | |
throw new Error('attachComments needs range information'); | |
} | |
// tokens array is empty, we attach comments to tree as 'leadingComments' | |
if (!tokens.length) { | |
if (providedComments.length) { | |
for (i = 0, len = providedComments.length; i < len; i += 1) { | |
comment = deepCopy(providedComments[i]); | |
comment.extendedRange = [0, tree.range[0]]; | |
comments.push(comment); | |
} | |
tree.leadingComments = comments; | |
} | |
return tree; | |
} | |
for (i = 0, len = providedComments.length; i < len; i += 1) { | |
comments.push(extendCommentRange(deepCopy(providedComments[i]), tokens)); | |
} | |
// This is based on John Freeman's implementation. | |
cursor = 0; | |
traverse(tree, { | |
enter: function (node) { | |
var comment; | |
while (cursor < comments.length) { | |
comment = comments[cursor]; | |
if (comment.extendedRange[1] > node.range[0]) { | |
break; | |
} | |
if (comment.extendedRange[1] === node.range[0]) { | |
if (!node.leadingComments) { | |
node.leadingComments = []; | |
} | |
node.leadingComments.push(comment); | |
comments.splice(cursor, 1); | |
} else { | |
cursor += 1; | |
} | |
} | |
// already out of owned node | |
if (cursor === comments.length) { | |
return VisitorOption.Break; | |
} | |
if (comments[cursor].extendedRange[0] > node.range[1]) { | |
return VisitorOption.Skip; | |
} | |
} | |
}); | |
cursor = 0; | |
traverse(tree, { | |
leave: function (node) { | |
var comment; | |
while (cursor < comments.length) { | |
comment = comments[cursor]; | |
if (node.range[1] < comment.extendedRange[0]) { | |
break; | |
} | |
if (node.range[1] === comment.extendedRange[0]) { | |
if (!node.trailingComments) { | |
node.trailingComments = []; | |
} | |
node.trailingComments.push(comment); | |
comments.splice(cursor, 1); | |
} else { | |
cursor += 1; | |
} | |
} | |
// already out of owned node | |
if (cursor === comments.length) { | |
return VisitorOption.Break; | |
} | |
if (comments[cursor].extendedRange[0] > node.range[1]) { | |
return VisitorOption.Skip; | |
} | |
} | |
}); | |
return tree; | |
} | |
exports.version = require('./package.json').version; | |
exports.Syntax = Syntax; | |
exports.traverse = traverse; | |
exports.replace = replace; | |
exports.attachComments = attachComments; | |
exports.VisitorKeys = VisitorKeys; | |
exports.VisitorOption = VisitorOption; | |
exports.Controller = Controller; | |
exports.cloneEnvironment = function () { return clone({}); }; | |
return exports; | |
}(exports)); | |
/* vim: set sw=4 ts=4 et tw=80 : */ | |