From 0bf95d6aea3fcf76766021080e02d7b347bb9d46 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Mon, 25 May 2015 01:01:21 +0100 Subject: [PATCH] even more split up of path methods --- src/babel/traversal/path/README.md | 10 + src/babel/traversal/path/context.js | 210 +++++++ src/babel/traversal/path/family.js | 118 ++++ src/babel/traversal/path/index.js | 742 +---------------------- src/babel/traversal/path/modification.js | 177 ++++++ src/babel/traversal/path/removal.js | 73 +++ src/babel/traversal/path/verification.js | 168 +++++ 7 files changed, 761 insertions(+), 737 deletions(-) create mode 100644 src/babel/traversal/path/README.md create mode 100644 src/babel/traversal/path/context.js create mode 100644 src/babel/traversal/path/family.js create mode 100644 src/babel/traversal/path/modification.js create mode 100644 src/babel/traversal/path/removal.js create mode 100644 src/babel/traversal/path/verification.js diff --git a/src/babel/traversal/path/README.md b/src/babel/traversal/path/README.md new file mode 100644 index 0000000000..797576fd16 --- /dev/null +++ b/src/babel/traversal/path/README.md @@ -0,0 +1,10 @@ + - `ancestry` - Methods that retrieve or validate anything related to the current paths ancestry. + - `context` - Methods responsible for maintaing TraversalContexts. + - `conversion` - Methods that convert the path node into another node or some other type of data. + - `evaluation` - Methods responsible for statically evaluating code. + - `modification` - Methods that modify the path/node in some ways. + - `resolution` - Methods responsible for type inferrence etc. + - `replacement` - Methods responsible for replacing a node with another. + - `removal` - Methods responsible for removing a node. + - `family` - Methods responsible for dealing with/retrieving children or siblings. + - `verification` - Methodsresponsible for introspecting the current path for certain values. diff --git a/src/babel/traversal/path/context.js b/src/babel/traversal/path/context.js new file mode 100644 index 0000000000..ba19552d1c --- /dev/null +++ b/src/babel/traversal/path/context.js @@ -0,0 +1,210 @@ +import * as messages from "../../messages"; +import NodePath from "./index"; +import traverse from "../index"; + +/** + * Description + */ + +export function call(key) { + var node = this.node; + if (!node) return; + + var opts = this.opts; + if (!opts[key] && !opts[node.type]) return; + + var fns = [].concat(opts[key]); + if (opts[node.type]) fns = fns.concat(opts[node.type][key]); + + for (var fn of (fns: Array)) { + if (!fn) continue; + + let node = this.node; + if (!node) return; + + var previousType = this.type; + + // call the function with the params (node, parent, scope, state) + var replacement = fn.call(this, node, this.parent, this.scope, this.state); + + if (replacement) { + this.replaceWith(replacement, true); + } + + if (this.shouldStop || this.shouldSkip || this.removed) return; + + if (previousType !== this.type) { + this.queueNode(this); + return; + } + } +} + +/** + * Description + */ + +export function isBlacklisted(): boolean { + var blacklist = this.opts.blacklist; + return blacklist && blacklist.indexOf(this.node.type) > -1; +} + +/** + * Description + */ + +export function visit(): boolean { + if (this.isBlacklisted()) return false; + if (this.opts.shouldSkip && this.opts.shouldSkip(this)) return false; + + this.call("enter"); + + if (this.shouldSkip) { + return this.shouldStop; + } + + var node = this.node; + var opts = this.opts; + + if (node) { + if (Array.isArray(node)) { + // traverse over these replacement nodes we purposely don't call exitNode + // as the original node has been destroyed + for (var i = 0; i < node.length; i++) { + traverse.node(node[i], opts, this.scope, this.state, this); + } + } else { + traverse.node(node, opts, this.scope, this.state, this); + this.call("exit"); + } + } + + return this.shouldStop; +} + +/** + * Description + */ + +export function skip() { + this.shouldSkip = true; +} + +/** + * Description + */ + +export function stop() { + this.shouldStop = true; + this.shouldSkip = true; +} + +/** + * Description + */ + +export function setScope(file?) { + if (this.opts && this.opts.noScope) return; + + var target = this.context || this.parentPath; + this.scope = NodePath.getScope(this, target && target.scope, file); +} + +/** + * Description + */ + +export function setContext(context, file) { + this.shouldSkip = false; + this.shouldStop = false; + this.removed = false; + + if (context) { + this.context = context; + this.state = context.state; + this.opts = context.opts; + } + + var log = file && this.type === "Program"; + if (log) file.log.debug("Start scope building"); + this.setScope(file); + if (log) file.log.debug("End scope building"); + + return this; +} + +/** + * Description + */ + +export function update() { + if (this.node === this.container[this.key]) return; + + // grrr, path key is out of sync. this is likely due to a modification to the AST + // not through our path APIs + + if (Array.isArray(this.container)) { + for (var i = 0; i < this.container.length; i++) { + if (this.container[i] === this.node) { + return this.setKey(i); + } + } + } else { + for (var key in this.container) { + if (this.container[key] === this.node) { + return this.setKey(key); + } + } + } + + throw new Error(messages.get("lostTrackNodePath")); +} + +/** + * Description + */ + +export function shiftContext() { + this.contexts.shift(); + this.setContext(this.contexts[0]); +} + +/** + * Description + */ + +export function unshiftContext(context) { + this.contexts.unshift(context); + this.setContext(context); +} + +/** + * Description + */ + +export function setup(parentPath, key) { + this.parentPath = parentPath || this.parentPath; + this.setKey(key); +} + +/** + * Description + */ + +export function setKey(key) { + this.key = key; + this.node = this.container[this.key]; + this.type = this.node && this.node.type; +} + +/** + * Description + */ + +export function queueNode(path) { + for (var context of (this.contexts: Array)) { + if (context.queue) { + context.queue.push(path); + } + } +} diff --git a/src/babel/traversal/path/family.js b/src/babel/traversal/path/family.js new file mode 100644 index 0000000000..023da87e9f --- /dev/null +++ b/src/babel/traversal/path/family.js @@ -0,0 +1,118 @@ +import NodePath from "./index"; +import * as t from "../../types"; + +/** + * Description + */ + +export function getStatementParent(): ?NodePath { + var path = this; + + do { + if (!path.parentPath || (Array.isArray(path.container) && path.isStatement())) { + break; + } else { + path = path.parentPath; + } + } while (path); + + if (path && (path.isProgram() || path.isFile())) { + throw new Error("File/Program node, we can't possibly find a statement parent to this"); + } + + return path; +} + +/** + * Description + */ + +export function getCompletionRecords(): Array { + var paths = []; + + var add = function (path) { + if (path) paths = paths.concat(path.getCompletionRecords()); + }; + + if (this.isIfStatement()) { + add(this.get("consequent")); + add(this.get("alternate")); + } else if (this.isDoExpression() || this.isFor() || this.isWhile()) { + add(this.get("body")); + } else if (this.isProgram() || this.isBlockStatement()) { + add(this.get("body").pop()); + } else if (this.isFunction()) { + return this.get("body").getCompletionRecords(); + } else { + paths.push(this); + } + + return paths; +} + +/** + * Description + */ + +export function getSibling(key) { + return NodePath.get(this.parentPath, this.parent, this.container, key, this.file); +} + +/** + * Description + */ + +export function get(key: string): NodePath { + var parts = key.split("."); + if (parts.length === 1) { // "foo" + return this._getKey(key); + } else { // "foo.bar" + return this._getPattern(parts); + } +} + +/** + * Description + */ + +export function _getKey(key) { + var node = this.node; + var container = node[key]; + + if (Array.isArray(container)) { + // requested a container so give them all the paths + return container.map((_, i) => { + return NodePath.get(this, node, container, i).setContext(); + }); + } else { + return NodePath.get(this, node, node, key).setContext(); + } +} + +/** + * Description + */ + +export function _getPattern(parts) { + var path = this; + for (var part of (parts: Array)) { + if (part === ".") { + path = path.parentPath; + } else { + if (Array.isArray(path)) { + path = path[part]; + } else { + path = path.get(part); + } + } + } + return path; +} + +/** + * Description + */ + +export function getBindingIdentifiers() { + return t.getBindingIdentifiers(this.node); +} diff --git a/src/babel/traversal/path/index.js b/src/babel/traversal/path/index.js index a79969cccd..fe97227ba3 100644 --- a/src/babel/traversal/path/index.js +++ b/src/babel/traversal/path/index.js @@ -1,7 +1,6 @@ import PathHoister from "./lib/hoister"; import * as virtualTypes from "./lib/virtual-types"; import * as messages from "../../messages"; -import * as contextual from "./lib/contextual"; import isBoolean from "lodash/lang/isBoolean"; import isNumber from "lodash/lang/isNumber"; import isRegExp from "lodash/lang/isRegExp"; @@ -66,177 +65,6 @@ export default class NodePath { return ourScope; } - /** - * Check whether this node was a part of the original AST. - */ - - isUser() { - return this.node && !!this.node.loc; - } - - /** - * Check whether this node was generated by us and not a part of the original AST. - */ - - isGenerated() { - return !this.isUser(); - } - - /** - * Description - */ - - queueNode(path) { - for (var context of (this.contexts: Array)) { - if (context.queue) { - context.queue.push(path); - } - } - } - - /** - * Description - */ - - insertBefore(nodes) { - nodes = this._verifyNodeList(nodes); - - if (this.parentPath.isExpressionStatement() || this.parentPath.isLabeledStatement()) { - return this.parentPath.insertBefore(nodes); - } else if (this.isPreviousType("Expression") || (this.parentPath.isForStatement() && this.key === "init")) { - if (this.node) nodes.push(this.node); - this.replaceExpressionWithStatements(nodes); - } else if (this.isPreviousType("Statement") || !this.type) { - this._maybePopFromStatements(nodes); - if (Array.isArray(this.container)) { - this._containerInsertBefore(nodes); - } else if (this.isStatementOrBlock()) { - if (this.node) nodes.push(this.node); - this.node = this.container[this.key] = t.blockStatement(nodes); - } else { - throw new Error("We don't know what to do with this node type. We were previously a Statement but we can't fit in here?"); - } - } else { - throw new Error("No clue what to do with this node type."); - } - } - - _containerInsert(from, nodes) { - this.updateSiblingKeys(from, nodes.length); - - var paths = []; - - for (var i = 0; i < nodes.length; i++) { - var to = from + i; - var node = nodes[i]; - this.container.splice(to, 0, node); - - if (this.context) { - var path = this.context.create(this.parent, this.container, to); - paths.push(path); - this.queueNode(path); - } else { - paths.push(NodePath.get(this, node, this.container, to)); - } - } - - return paths; - } - - _containerInsertBefore(nodes) { - return this._containerInsert(this.key, nodes); - } - - _containerInsertAfter(nodes) { - return this._containerInsert(this.key + 1, nodes); - } - - _maybePopFromStatements(nodes) { - var last = nodes[nodes.length - 1]; - if (t.isExpressionStatement(last) && t.isIdentifier(last.expression) && !this.isCompletionRecord()) { - nodes.pop(); - } - } - - /** - * Description - */ - - isCompletionRecord() { - var path = this; - - do { - var container = path.container; - - if (path.isFunction()) { - return false; - } - - if (Array.isArray(container) && path.key !== container.length - 1) { - return false; - } - } while ((path = path.parentPath) && !path.isProgram()); - - return true; - } - - /** - * Description - */ - - isStatementOrBlock() { - if (t.isLabeledStatement(this.parent) || t.isBlockStatement(this.container)) { - return false; - } else { - return includes(t.STATEMENT_OR_BLOCK_KEYS, this.key); - } - } - - /** - * Description - */ - - insertAfter(nodes) { - nodes = this._verifyNodeList(nodes); - - if (this.parentPath.isExpressionStatement() || this.parentPath.isLabeledStatement()) { - return this.parentPath.insertAfter(nodes); - } else if (this.isPreviousType("Expression") || (this.parentPath.isForStatement() && this.key === "init")) { - if (this.node) { - var temp = this.scope.generateDeclaredUidIdentifier(); - nodes.unshift(t.expressionStatement(t.assignmentExpression("=", temp, this.node))); - nodes.push(t.expressionStatement(temp)); - } - this.replaceExpressionWithStatements(nodes); - } else if (this.isPreviousType("Statement") || !this.type) { - this._maybePopFromStatements(nodes); - if (Array.isArray(this.container)) { - this._containerInsertAfter(nodes); - } else if (this.isStatementOrBlock()) { - if (this.node) nodes.unshift(this.node); - this.node = this.container[this.key] = t.blockStatement(nodes); - } else { - throw new Error("We don't know what to do with this node type. We were previously a Statement but we can't fit in here?"); - } - } else { - throw new Error("No clue what to do with this node type."); - } - } - - /** - * Description - */ - - updateSiblingKeys(fromIndex, incrementBy) { - var paths = this.container._paths; - for (var i = 0; i < paths.length; i++) { - let path = paths[i]; - if (path.key >= fromIndex) { - path.key += incrementBy; - } - } - } - /** * Description */ @@ -255,192 +83,6 @@ export default class NodePath { return val; } - /** - * Description - */ - - setScope(file?) { - if (this.opts && this.opts.noScope) return; - - var target = this.context || this.parentPath; - this.scope = NodePath.getScope(this, target && target.scope, file); - } - - /** - * Description - */ - - setContext(context, file) { - this.shouldSkip = false; - this.shouldStop = false; - this.removed = false; - - if (context) { - this.context = context; - this.state = context.state; - this.opts = context.opts; - } - - var log = file && this.type === "Program"; - if (log) file.log.debug("Start scope building"); - this.setScope(file); - if (log) file.log.debug("End scope building"); - - return this; - } - - /** - * Description - */ - - update() { - if (this.node === this.container[this.key]) return; - - // grrr, path key is out of sync. this is likely due to a modification to the AST - // not through our path APIs - - if (Array.isArray(this.container)) { - for (var i = 0; i < this.container.length; i++) { - if (this.container[i] === this.node) { - return this.setKey(i); - } - } - } else { - for (var key in this.container) { - if (this.container[key] === this.node) { - return this.setKey(key); - } - } - } - - throw new Error(messages.get("lostTrackNodePath")); - } - - /** - * Description - */ - - shiftContext() { - this.contexts.shift(); - this.setContext(this.contexts[0]); - } - - /** - * Description - */ - - unshiftContext(context) { - this.contexts.unshift(context); - this.setContext(context); - } - - /** - * Description - */ - - setup(parentPath, key) { - this.parentPath = parentPath || this.parentPath; - this.setKey(key); - } - - /** - * Description - */ - - setKey(key) { - this.key = key; - this.node = this.container[this.key]; - this.type = this.node && this.node.type; - } - - /** - * Share comments amongst siblings. - */ - - shareCommentsWithSiblings() { - var node = this.node; - if (!node) return; - - var trailing = node.trailingComments; - var leading = node.leadingComments; - if (!trailing && !leading) return; - - var prev = this.getSibling(this.key - 1); - var next = this.getSibling(this.key + 1); - - if (!prev.node) prev = next; - if (!next.node) next = prev; - - prev.giveComments("trailing", leading); - next.giveComments("leading", trailing); - } - - /** - * Give node `comments` of the specified `type`. - */ - - giveComments(type: string, comments: Array) { - if (!comments) return; - - var node = this.node; - if (!node) return; - - var key = `${type}Comments`; - - if (node[key]) { - node[key] = node[key].concat(comments); - } else { - node[key] = comments; - } - } - - /** - * Description - */ - - remove() { - if (this._contextualCall("removers", "pre")) return; - - this.shareCommentsWithSiblings(); - this._remove(); - this.removed = true; - - this._contextualCall("removers", "post"); - } - - _contextualCall(type, position) { - for (var fn of (contextual[type][position]: Array)) { - if (fn(this, this.parentPath)) return; - } - } - - _remove() { - if (Array.isArray(this.container)) { - this.container.splice(this.key, 1); - this.updateSiblingKeys(this.key, -1); - } else { - this.container[this.key] = null; - } - this.node = null; - } - - /** - * Description - */ - - skip() { - this.shouldSkip = true; - } - - /** - * Description - */ - - stop() { - this.shouldStop = true; - this.shouldSkip = true; - } - /** * Description */ @@ -452,392 +94,13 @@ export default class NodePath { return err; } - /** - * Description - */ - - _verifyNodeList(nodes) { - if (nodes.constructor !== Array) { - nodes = [nodes]; - } - - for (var i = 0; i < nodes.length; i++) { - var node = nodes[i]; - if (!node) { - throw new Error(`Node list has falsy node with the index of ${i}`); - } else if (typeof node !== "object") { - throw new Error(`Node list contains a non-object node with the index of ${i}`); - } else if (!node.type) { - throw new Error(`Node list contains a node without a type with the index of ${i}`); - } - } - - return nodes; - } - - /** - * Description - */ - - unshiftContainer(containerKey, nodes) { - nodes = this._verifyNodeList(nodes); - - // get the first path and insert our nodes before it, if it doesn't exist then it - // doesn't matter, our nodes will be inserted anyway - - var container = this.node[containerKey]; - var path = NodePath.get(this, this.node, container, 0); - - return path.insertBefore(nodes); - } - - /** - * Description - */ - - pushContainer(containerKey, nodes) { - nodes = this._verifyNodeList(nodes); - - // get an invisible path that represents the last node + 1 and replace it with our - // nodes, effectively inlining it - - var container = this.node[containerKey]; - var i = container.length; - var path = NodePath.get(this, this.node, container, i); - - return path.replaceWith(nodes, true); - } - - /** - * This checks whether or now we're in one of the following positions: - * - * for (KEY in right); - * for (KEY;;); - * - * This is because these spots allow VariableDeclarations AND normal expressions so we need to tell the - * path replacement that it's ok to replace this with an expression. - */ - - canHaveVariableDeclarationOrExpression() { - return (this.key === "init" || this.key === "left") && this.parentPath.isFor(); - } - - /** - * Description - */ - - getStatementParent(): ?NodePath { - var path = this; - - do { - if (!path.parentPath || (Array.isArray(path.container) && path.isStatement())) { - break; - } else { - path = path.parentPath; - } - } while (path); - - if (path && (path.isProgram() || path.isFile())) { - throw new Error("File/Program node, we can't possibly find a statement parent to this"); - } - - return path; - } - - /** - * Description - */ - - getCompletionRecords(): Array { - var paths = []; - - var add = function (path) { - if (path) paths = paths.concat(path.getCompletionRecords()); - }; - - if (this.isIfStatement()) { - add(this.get("consequent")); - add(this.get("alternate")); - } else if (this.isDoExpression() || this.isFor() || this.isWhile()) { - add(this.get("body")); - } else if (this.isProgram() || this.isBlockStatement()) { - add(this.get("body").pop()); - } else if (this.isFunction()) { - return this.get("body").getCompletionRecords(); - } else { - paths.push(this); - } - - return paths; - } - - - /** - * Description - */ - - call(key) { - var node = this.node; - if (!node) return; - - var opts = this.opts; - if (!opts[key] && !opts[node.type]) return; - - var fns = [].concat(opts[key]); - if (opts[node.type]) fns = fns.concat(opts[node.type][key]); - - for (var fn of (fns: Array)) { - if (!fn) continue; - - let node = this.node; - if (!node) return; - - var previousType = this.type; - - // call the function with the params (node, parent, scope, state) - var replacement = fn.call(this, node, this.parent, this.scope, this.state); - - if (replacement) { - this.replaceWith(replacement, true); - } - - if (this.shouldStop || this.shouldSkip || this.removed) return; - - if (previousType !== this.type) { - this.queueNode(this); - return; - } - } - } - - /** - * Description - */ - - isBlacklisted(): boolean { - var blacklist = this.opts.blacklist; - return blacklist && blacklist.indexOf(this.node.type) > -1; - } - - /** - * Description - */ - - visit(): boolean { - if (this.isBlacklisted()) return false; - if (this.opts.shouldSkip && this.opts.shouldSkip(this)) return false; - - this.call("enter"); - - if (this.shouldSkip) { - return this.shouldStop; - } - - var node = this.node; - var opts = this.opts; - - if (node) { - if (Array.isArray(node)) { - // traverse over these replacement nodes we purposely don't call exitNode - // as the original node has been destroyed - for (var i = 0; i < node.length; i++) { - traverse.node(node[i], opts, this.scope, this.state, this); - } - } else { - traverse.node(node, opts, this.scope, this.state, this); - this.call("exit"); - } - } - - return this.shouldStop; - } - - /** - * Description - */ - - getSibling(key) { - return NodePath.get(this.parentPath, this.parent, this.container, key, this.file); - } - - /** - * Description - */ - - get(key: string): NodePath { - var parts = key.split("."); - if (parts.length === 1) { // "foo" - return this._getKey(key); - } else { // "foo.bar" - return this._getPattern(parts); - } - } - - /** - * Description - */ - - _getKey(key) { - var node = this.node; - var container = node[key]; - - if (Array.isArray(container)) { - // requested a container so give them all the paths - return container.map((_, i) => { - return NodePath.get(this, node, container, i).setContext(); - }); - } else { - return NodePath.get(this, node, node, key).setContext(); - } - } - - /** - * Description - */ - - _getPattern(parts) { - var path = this; - for (var part of (parts: Array)) { - if (part === ".") { - path = path.parentPath; - } else { - if (Array.isArray(path)) { - path = path[part]; - } else { - path = path.get(part); - } - } - } - return path; - } - - /** - * Description - */ - - has(key): boolean { - var val = this.node[key]; - if (val && Array.isArray(val)) { - return !!val.length; - } else { - return !!val; - } - } - - /** - * Description - */ - - is(key): boolean { - return this.has(key); - } - - /** - * Description - */ - - isnt(key): boolean { - return !this.has(key); - } - - /** - * Description - */ - - equals(key, value): boolean { - return this.node[key] === value; - } - - /** - * Description - */ - - isPreviousType(type: string): boolean { - return t.isType(this.type, type); - } - - /** - * Description - */ - - getBindingIdentifiers() { - return t.getBindingIdentifiers(this.node); - } - /** * Description */ traverse(visitor, state) { - if (!this.scope) console.log(this.contexts); traverse(this.node, visitor, this.scope, state, this); } - - /** - * Description - */ - - hoist(scope = this.scope) { - var hoister = new PathHoister(this, scope); - return hoister.run(); - } - - /** - * Match the current node if it matches the provided `pattern`. - * - * For example, given the match `React.createClass` it would match the - * parsed nodes of `React.createClass` and `React["createClass"]`. - */ - - matchesPattern(pattern: string, allowPartial?: boolean): boolean { - var parts = pattern.split("."); - - // not a member expression - if (!this.isMemberExpression()) return false; - - var search = [this.node]; - var i = 0; - - function matches(name) { - var part = parts[i]; - return part === "*" || name === part; - } - - while (search.length) { - var node = search.shift(); - - if (allowPartial && i === parts.length) { - return true; - } - - if (t.isIdentifier(node)) { - // this part doesn't match - if (!matches(node.name)) return false; - } else if (t.isLiteral(node)) { - // this part doesn't match - if (!matches(node.value)) return false; - } else if (t.isMemberExpression(node)) { - if (node.computed && !t.isLiteral(node.property)) { - // we can't deal with this - return false; - } else { - search.push(node.object); - search.push(node.property); - continue; - } - } else { - // we can't deal with this - return false; - } - - // too many parts - if (++i > parts.length) { - return false; - } - } - - return true; - } } assign(NodePath.prototype, require("./ancestry")); @@ -845,6 +108,11 @@ assign(NodePath.prototype, require("./resolution")); assign(NodePath.prototype, require("./replacement")); assign(NodePath.prototype, require("./evaluation")); assign(NodePath.prototype, require("./conversion")); +assign(NodePath.prototype, require("./verification")); +assign(NodePath.prototype, require("./context")); +assign(NodePath.prototype, require("./removal")); +assign(NodePath.prototype, require("./modification")); +assign(NodePath.prototype, require("./family")); for (let type in virtualTypes) { if (type[0] === "_") continue; diff --git a/src/babel/traversal/path/modification.js b/src/babel/traversal/path/modification.js new file mode 100644 index 0000000000..c3bf91fca6 --- /dev/null +++ b/src/babel/traversal/path/modification.js @@ -0,0 +1,177 @@ +import PathHoister from "./lib/hoister"; +import NodePath from "./index"; +import * as t from "../../types"; + +/** + * Description + */ + +export function insertBefore(nodes) { + nodes = this._verifyNodeList(nodes); + + if (this.parentPath.isExpressionStatement() || this.parentPath.isLabeledStatement()) { + return this.parentPath.insertBefore(nodes); + } else if (this.isPreviousType("Expression") || (this.parentPath.isForStatement() && this.key === "init")) { + if (this.node) nodes.push(this.node); + this.replaceExpressionWithStatements(nodes); + } else if (this.isPreviousType("Statement") || !this.type) { + this._maybePopFromStatements(nodes); + if (Array.isArray(this.container)) { + this._containerInsertBefore(nodes); + } else if (this.isStatementOrBlock()) { + if (this.node) nodes.push(this.node); + this.node = this.container[this.key] = t.blockStatement(nodes); + } else { + throw new Error("We don't know what to do with this node type. We were previously a Statement but we can't fit in here?"); + } + } else { + throw new Error("No clue what to do with this node type."); + } +} + +export function _containerInsert(from, nodes) { + this.updateSiblingKeys(from, nodes.length); + + var paths = []; + + for (var i = 0; i < nodes.length; i++) { + var to = from + i; + var node = nodes[i]; + this.container.splice(to, 0, node); + + if (this.context) { + var path = this.context.create(this.parent, this.container, to); + paths.push(path); + this.queueNode(path); + } else { + paths.push(NodePath.get(this, node, this.container, to)); + } + } + + return paths; +} + +export function _containerInsertBefore(nodes) { + return this._containerInsert(this.key, nodes); +} + +export function _containerInsertAfter(nodes) { + return this._containerInsert(this.key + 1, nodes); +} + +export function _maybePopFromStatements(nodes) { + var last = nodes[nodes.length - 1]; + if (t.isExpressionStatement(last) && t.isIdentifier(last.expression) && !this.isCompletionRecord()) { + nodes.pop(); + } +} + +/** + * Description + */ + +export function insertAfter(nodes) { + nodes = this._verifyNodeList(nodes); + + if (this.parentPath.isExpressionStatement() || this.parentPath.isLabeledStatement()) { + return this.parentPath.insertAfter(nodes); + } else if (this.isPreviousType("Expression") || (this.parentPath.isForStatement() && this.key === "init")) { + if (this.node) { + var temp = this.scope.generateDeclaredUidIdentifier(); + nodes.unshift(t.expressionStatement(t.assignmentExpression("=", temp, this.node))); + nodes.push(t.expressionStatement(temp)); + } + this.replaceExpressionWithStatements(nodes); + } else if (this.isPreviousType("Statement") || !this.type) { + this._maybePopFromStatements(nodes); + if (Array.isArray(this.container)) { + this._containerInsertAfter(nodes); + } else if (this.isStatementOrBlock()) { + if (this.node) nodes.unshift(this.node); + this.node = this.container[this.key] = t.blockStatement(nodes); + } else { + throw new Error("We don't know what to do with this node type. We were previously a Statement but we can't fit in here?"); + } + } else { + throw new Error("No clue what to do with this node type."); + } +} + +/** + * Description + */ + +export function updateSiblingKeys(fromIndex, incrementBy) { + var paths = this.container._paths; + for (var i = 0; i < paths.length; i++) { + let path = paths[i]; + if (path.key >= fromIndex) { + path.key += incrementBy; + } + } +} + +/** + * Description + */ + +export function _verifyNodeList(nodes) { + if (nodes.constructor !== Array) { + nodes = [nodes]; + } + + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (!node) { + throw new Error(`Node list has falsy node with the index of ${i}`); + } else if (typeof node !== "object") { + throw new Error(`Node list contains a non-object node with the index of ${i}`); + } else if (!node.type) { + throw new Error(`Node list contains a node without a type with the index of ${i}`); + } + } + + return nodes; +} + +/** + * Description + */ + +export function unshiftContainer(containerKey, nodes) { + nodes = this._verifyNodeList(nodes); + + // get the first path and insert our nodes before it, if it doesn't exist then it + // doesn't matter, our nodes will be inserted anyway + + var container = this.node[containerKey]; + var path = NodePath.get(this, this.node, container, 0); + + return path.insertBefore(nodes); +} + +/** + * Description + */ + +export function pushContainer(containerKey, nodes) { + nodes = this._verifyNodeList(nodes); + + // get an invisible path that represents the last node + 1 and replace it with our + // nodes, effectively inlining it + + var container = this.node[containerKey]; + var i = container.length; + var path = NodePath.get(this, this.node, container, i); + + return path.replaceWith(nodes, true); +} + +/** + * Description + */ + +export function hoist(scope = this.scope) { + var hoister = new PathHoister(this, scope); + return hoister.run(); +} diff --git a/src/babel/traversal/path/removal.js b/src/babel/traversal/path/removal.js new file mode 100644 index 0000000000..fd8b551816 --- /dev/null +++ b/src/babel/traversal/path/removal.js @@ -0,0 +1,73 @@ +import * as removalHooks from "./lib/removal-hooks"; +import * as t from "../../types"; + +/** + * Description + */ + +export function remove() { + if (this._callRemovalHooks("pre")) return; + + this.shareCommentsWithSiblings(); + this._remove(); + this.removed = true; + + this._callRemovalHooks("post"); +} + +export function _callRemovalHooks(position) { + for (var fn of (removalHooks[position]: Array)) { + if (fn(this, this.parentPath)) return; + } +} + +export function _remove() { + if (Array.isArray(this.container)) { + this.container.splice(this.key, 1); + this.updateSiblingKeys(this.key, -1); + } else { + this.container[this.key] = null; + } + this.node = null; +} + +/** + * Share comments amongst siblings. + */ + +export function shareCommentsWithSiblings() { + var node = this.node; + if (!node) return; + + var trailing = node.trailingComments; + var leading = node.leadingComments; + if (!trailing && !leading) return; + + var prev = this.getSibling(this.key - 1); + var next = this.getSibling(this.key + 1); + + if (!prev.node) prev = next; + if (!next.node) next = prev; + + prev.giveComments("trailing", leading); + next.giveComments("leading", trailing); +} + +/** + * Give node `comments` of the specified `type`. + */ + +export function giveComments(type: string, comments: Array) { + if (!comments) return; + + var node = this.node; + if (!node) return; + + var key = `${type}Comments`; + + if (node[key]) { + node[key] = node[key].concat(comments); + } else { + node[key] = comments; + } +} diff --git a/src/babel/traversal/path/verification.js b/src/babel/traversal/path/verification.js new file mode 100644 index 0000000000..8341f49369 --- /dev/null +++ b/src/babel/traversal/path/verification.js @@ -0,0 +1,168 @@ +import includes from "lodash/collection/includes"; +import * as t from "../../types"; + +/** + * Match the current node if it matches the provided `pattern`. + * + * For example, given the match `React.createClass` it would match the + * parsed nodes of `React.createClass` and `React["createClass"]`. + */ + +export function matchesPattern(pattern: string, allowPartial?: boolean): boolean { + var parts = pattern.split("."); + + // not a member expression + if (!this.isMemberExpression()) return false; + + var search = [this.node]; + var i = 0; + + function matches(name) { + var part = parts[i]; + return part === "*" || name === part; + } + + while (search.length) { + var node = search.shift(); + + if (allowPartial && i === parts.length) { + return true; + } + + if (t.isIdentifier(node)) { + // this part doesn't match + if (!matches(node.name)) return false; + } else if (t.isLiteral(node)) { + // this part doesn't match + if (!matches(node.value)) return false; + } else if (t.isMemberExpression(node)) { + if (node.computed && !t.isLiteral(node.property)) { + // we can't deal with this + return false; + } else { + search.push(node.object); + search.push(node.property); + continue; + } + } else { + // we can't deal with this + return false; + } + + // too many parts + if (++i > parts.length) { + return false; + } + } + + return true; +} + +/** + * Description + */ + +export function has(key): boolean { + var val = this.node[key]; + if (val && Array.isArray(val)) { + return !!val.length; + } else { + return !!val; + } +} + +/** + * Description + */ + +export function is(key): boolean { + return this.has(key); +} + +/** + * Description + */ + +export function isnt(key): boolean { + return !this.has(key); +} + +/** + * Description + */ + +export function equals(key, value): boolean { + return this.node[key] === value; +} + +/** + * Description + */ + +export function isPreviousType(type: string): boolean { + return t.isType(this.type, type); +} + +/** + * This checks whether or now we're in one of the following positions: + * + * for (KEY in right); + * for (KEY;;); + * + * This is because these spots allow VariableDeclarations AND normal expressions so we need to tell the + * path replacement that it's ok to replace this with an expression. + */ + + export function canHaveVariableDeclarationOrExpression() { + return (this.key === "init" || this.key === "left") && this.parentPath.isFor(); + } + +/** + * Description + */ + +export function isCompletionRecord() { + var path = this; + + do { + var container = path.container; + + if (path.isFunction()) { + return false; + } + + if (Array.isArray(container) && path.key !== container.length - 1) { + return false; + } + } while ((path = path.parentPath) && !path.isProgram()); + + return true; +} + +/** + * Description + */ + +export function isStatementOrBlock() { + if (t.isLabeledStatement(this.parent) || t.isBlockStatement(this.container)) { + return false; + } else { + return includes(t.STATEMENT_OR_BLOCK_KEYS, this.key); + } +} + +/** + * Check whether this node was a part of the original AST. + */ + +export function isUser() { + return this.node && !!this.node.loc; +} + +/** + * Check whether this node was generated by us and not a part of the original AST. + */ + +export function isGenerated() { + return !this.isUser(); +}