From 6a4e93bf0f002dd03e885a2c0bf641a21ad099e9 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Tue, 5 May 2015 01:44:01 +0100 Subject: [PATCH] make visitor validation more anal, add node type wrappers, add mixin support to visitor explosion, allow visitor entrance/exit to provide an array of callbacks to use rather than limiting it to a single callback --- src/babel/messages.js | 4 +- .../helpers/build-react-transformer.js | 2 +- src/babel/transformation/modules/_default.js | 83 +++++++------ .../transformers/es6/modules.js | 2 +- .../transformers/optimisation/flow.for-of.js | 4 +- src/babel/traversal/explode.js | 19 --- src/babel/traversal/index.js | 44 +------ src/babel/traversal/path/index.js | 19 ++- src/babel/traversal/scope.js | 42 ++++--- src/babel/traversal/visitors/index.js | 114 ++++++++++++++++++ src/babel/traversal/visitors/wrappers.js | 10 ++ 11 files changed, 217 insertions(+), 126 deletions(-) delete mode 100644 src/babel/traversal/explode.js create mode 100644 src/babel/traversal/visitors/index.js create mode 100644 src/babel/traversal/visitors/wrappers.js diff --git a/src/babel/messages.js b/src/babel/messages.js index ec426958fa..f81932466a 100644 --- a/src/babel/messages.js +++ b/src/babel/messages.js @@ -23,10 +23,12 @@ export const MESSAGES = { missingTemplatesDirectory: "no templates directory - this is most likely the result of a broken `npm publish`. Please report to https://github.com/babel/babel/issues", unsupportedOutputType: "Unsupported output type $1", illegalMethodName: "Illegal method name $1", + traverseNeedsParent: "Must pass a scope and parentPath unless traversing a Program/File got a $1 node", traverseVerifyRootFunction: "You passed `traverse()` a function when it expected a visitor object, are you sure you didn't mean `{ enter: Function }`?", - traverseVerifyVisitorFunction: "Hey! You passed \`traverse()\` a visitor object with the key $1 that's a straight up `Function` instead of `{ enter: Function }`. You need to normalise it with `traverse.explode(visitor)`.", + traverseVerifyVisitorFunction: "You passed \`traverse()\` a visitor object with the key $1 that's a `Function` instead of `{ enter: Function }`. You need to normalise your visitor with `traverse.explode(visitor)`.", traverseVerifyVisitorProperty: "You passed `traverse()` a visitor object with the property $1 that has the invalid property $2", + traverseVerifyNodeType: "You gave us a visitor for the node type $1 but it's not a valid type", pluginIllegalKind: "Illegal kind $1 for plugin $2", pluginIllegalPosition: "Illegal position $1 for plugin $2", diff --git a/src/babel/transformation/helpers/build-react-transformer.js b/src/babel/transformation/helpers/build-react-transformer.js index e21b54e390..8b9e4897b0 100644 --- a/src/babel/transformation/helpers/build-react-transformer.js +++ b/src/babel/transformation/helpers/build-react-transformer.js @@ -10,7 +10,7 @@ import * as react from "./react"; import * as t from "../../types"; export default function (exports, opts) { - exports.check = function (node) { + exports.shouldVisit = function (node) { if (t.isJSX(node)) return true; if (react.isCreateClass(node)) return true; return false; diff --git a/src/babel/transformation/modules/_default.js b/src/babel/transformation/modules/_default.js index f4d8f016a1..06eac02f99 100644 --- a/src/babel/transformation/modules/_default.js +++ b/src/babel/transformation/modules/_default.js @@ -5,8 +5,14 @@ import object from "../../helpers/object"; import * as util from "../../util"; import * as t from "../../types"; -var remapVisitor = { +var remapVisitor = traverse.explode({ enter(node, parent, scope, formatter) { + if (node._skipModulesRemap) { + return this.skip(); + } + }, + + Identifier(node, parent, scope, formatter) { var remap = formatter.internalRemap[node.name]; if (this.isReferencedIdentifier() && remap && node !== remap) { @@ -14,53 +20,50 @@ var remapVisitor = { return remap; } } + }, - if (t.isUpdateExpression(node)) { - var exported = formatter.getExport(node.argument, scope); - - if (exported) { - this.skip(); - - // expand to long file assignment expression - var assign = t.assignmentExpression(node.operator[0] + "=", node.argument, t.literal(1)); - - // remap this assignment expression - var remapped = formatter.remapExportAssignment(assign, exported); - - // we don't need to change the result - if (t.isExpressionStatement(parent) || node.prefix) { - return remapped; + AssignmentExpression: { + exit(node, parent, scope, formatter) { + if (!node._ignoreModulesRemap) { + var exported = formatter.getExport(node.left, scope); + if (exported) { + return formatter.remapExportAssignment(node, exported); } - - var nodes = []; - nodes.push(remapped); - - var operator; - if (node.operator === "--") { - operator = "+"; - } else { // "++" - operator = "-"; - } - nodes.push(t.binaryExpression(operator, node.argument, t.literal(1))); - - return t.sequenceExpression(nodes); } } - - if (node._skipModulesRemap) { - return this.skip(); - } }, - exit(node, parent, scope, formatter) { - if (t.isAssignmentExpression(node) && !node._ignoreModulesRemap) { - var exported = formatter.getExport(node.left, scope); - if (exported) { - return formatter.remapExportAssignment(node, exported); - } + UpdateExpression(node, parent, scope, formatter) { + var exported = formatter.getExport(node.argument, scope); + if (!exported) return; + + this.skip(); + + // expand to long file assignment expression + var assign = t.assignmentExpression(node.operator[0] + "=", node.argument, t.literal(1)); + + // remap this assignment expression + var remapped = formatter.remapExportAssignment(assign, exported); + + // we don't need to change the result + if (t.isExpressionStatement(parent) || node.prefix) { + return remapped; } + + var nodes = []; + nodes.push(remapped); + + var operator; + if (node.operator === "--") { + operator = "+"; + } else { // "++" + operator = "-"; + } + nodes.push(t.binaryExpression(operator, node.argument, t.literal(1))); + + return t.sequenceExpression(nodes); } -}; +}); var importsVisitor = { ImportDeclaration: { diff --git a/src/babel/transformation/transformers/es6/modules.js b/src/babel/transformation/transformers/es6/modules.js index fea77b4f9d..5abf2b1af4 100644 --- a/src/babel/transformation/transformers/es6/modules.js +++ b/src/babel/transformation/transformers/es6/modules.js @@ -1,6 +1,6 @@ import * as t from "../../../types"; -export { check } from "../internal/modules"; +export { shouldVisit } from "../internal/modules"; function keepBlockHoist(node, nodes) { if (node._blockHoist) { diff --git a/src/babel/transformation/transformers/optimisation/flow.for-of.js b/src/babel/transformation/transformers/optimisation/flow.for-of.js index a7bf59b174..f69525e307 100644 --- a/src/babel/transformation/transformers/optimisation/flow.for-of.js +++ b/src/babel/transformation/transformers/optimisation/flow.for-of.js @@ -2,7 +2,9 @@ import { _ForOfStatementArray } from "../es6/for-of"; import * as t from "../../../types"; export var shouldVisit = t.isForOfStatement; -export var optional = true; +export var metadata = { + optional: true +}; export function ForOfStatement(node, parent, scope, file) { if (this.get("right").isTypeGeneric("Array")) { diff --git a/src/babel/traversal/explode.js b/src/babel/traversal/explode.js deleted file mode 100644 index 3f3696fd55..0000000000 --- a/src/babel/traversal/explode.js +++ /dev/null @@ -1,19 +0,0 @@ -import * as t from "../types"; - -export default function (obj) { - for (var type in obj) { - var fns = obj[type]; - if (typeof fns === "function") { - obj[type] = fns = { enter: fns }; - } - - var aliases = t.FLIPPED_ALIAS_KEYS[type]; - if (aliases) { - for (var i = 0; i < aliases.length; i++) { - var alias = aliases[i]; - obj[alias] = obj[alias] || fns; - } - } - } - return obj; -} diff --git a/src/babel/traversal/index.js b/src/babel/traversal/index.js index 194d412cb9..1f2ccccf12 100644 --- a/src/babel/traversal/index.js +++ b/src/babel/traversal/index.js @@ -1,5 +1,5 @@ import TraversalContext from "./context"; -import explode from "./explode"; +import { explode, verify } from "./visitors"; import * as messages from "../messages"; import includes from "lodash/collection/includes"; import * as t from "../types"; @@ -14,7 +14,7 @@ export default function traverse(parent, opts, scope, state, parentPath) { } if (!opts) opts = {}; - traverse.verify(opts); + verify(opts); // array of nodes if (Array.isArray(parent)) { @@ -26,42 +26,8 @@ export default function traverse(parent, opts, scope, state, parentPath) { } } -/** - * Quickly iterate over some traversal options and validate them. - */ - -traverse.verify = function (opts) { - if (opts._verified) return; - - if (typeof opts === "function") { - throw new Error(messages.get("traverseVerifyRootFunction")); - } - - if (!opts.enter) opts.enter = function () { }; - if (!opts.exit) opts.exit = function () { }; - if (!opts.shouldSkip) opts.shouldSkip = function () { return false; }; - - for (var key in opts) { - // it's all good - if (key === "blacklist") continue; - - var opt = opts[key]; - - if (typeof opt === "function") { - // it's all good, it's fine for this key to be a function - if (key === "enter" || key === "exit" || key === "shouldSkip") continue; - - throw new Error(messages.get("traverseVerifyVisitorFunction", key)); - } else if (typeof opt === "object") { - for (var key2 in opt) { - if (key2 === "enter" || key2 === "exit") continue; - throw new Error(messages.get("traverseVerifyVisitorProperty", key, key2)); - } - } - } - - opts._verified = true; -}; +traverse.verify = verify; +traverse.explode = explode; traverse.node = function (node, opts, scope, state, parentPath) { var keys = t.VISITOR_KEYS[node.type]; @@ -113,8 +79,6 @@ traverse.removeProperties = function (tree) { return tree; }; -traverse.explode = explode; - function hasBlacklistedType(node, parent, scope, state) { if (node.type === state.type) { state.has = true; diff --git a/src/babel/traversal/path/index.js b/src/babel/traversal/path/index.js index f65bddd89d..0fc9937999 100644 --- a/src/babel/traversal/path/index.js +++ b/src/babel/traversal/path/index.js @@ -662,12 +662,21 @@ export default class TraversalPath { if (!node) return; var opts = this.opts; - var fn = opts[key] || opts; - if (opts[node.type]) fn = opts[node.type][key] || fn; + var fns = [opts[key]]; - // 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 (opts[node.type]) { + fns = fns.concat(opts[node.type][key]); + } + + for (var fn of (fns: Array)) { + if (!fn) continue; + + // 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) break; + } } /** diff --git a/src/babel/traversal/scope.js b/src/babel/traversal/scope.js index 087c4893aa..dc007577d3 100644 --- a/src/babel/traversal/scope.js +++ b/src/babel/traversal/scope.js @@ -1,5 +1,5 @@ import includes from "lodash/collection/includes"; -import explode from "./explode"; +import { explode } from "./visitors"; import traverse from "./index"; import defaults from "lodash/object/defaults"; import * as messages from "../messages"; @@ -38,26 +38,32 @@ var functionVariableVisitor = { } }; -var programReferenceVisitor = { - enter(node, parent, scope, state) { - if (t.isReferencedIdentifier(node, parent)) { - var bindingInfo = scope.getBinding(node.name); - if (bindingInfo) { - bindingInfo.reference(); - } else { - state.addGlobal(node); - } - } else if (t.isLabeledStatement(node)) { +var programReferenceVisitor = explode({ + ReferencedIdentifier(node, parent, scope, state) { + var bindingInfo = scope.getBinding(node.name); + if (bindingInfo) { + bindingInfo.reference(); + } else { state.addGlobal(node); - } else if (t.isAssignmentExpression(node)) { - scope.registerConstantViolation(this.get("left"), this.get("right")); - } else if (t.isUpdateExpression(node)) { - scope.registerConstantViolation(this.get("argument"), null); - } else if (t.isUnaryExpression(node) && node.operator === "delete") { - scope.registerConstantViolation(this.get("left"), null); } + }, + + LabeledStatement(node, parent, scope, state) { + state.addGlobal(node); + }, + + AssignmentExpression(node, parent, scope, state) { + scope.registerConstantViolation(this.get("left"), this.get("right")); + }, + + UpdateExpression(node, parent, scope, state) { + scope.registerConstantViolation(this.get("argument"), null); + }, + + UnaryExpression(node, parent, scope, state) { + if (node.operator === "delete") scope.registerConstantViolation(this.get("left"), null); } -}; +}); var blockVariableVisitor = { enter(node, parent, scope, state) { diff --git a/src/babel/traversal/visitors/index.js b/src/babel/traversal/visitors/index.js new file mode 100644 index 0000000000..8ab9e0e55d --- /dev/null +++ b/src/babel/traversal/visitors/index.js @@ -0,0 +1,114 @@ +import * as typeWrappers from "./wrappers"; +import * as messages from "../../messages"; +import * as t from "../../types"; + +export function explode(visitor, mergeConflicts) { + // make sure there's no __esModule type since this is because we're using loose mode + // and it sets __esModule to be enumerable on all modules :( + delete visitor.__esModule; + + for (let nodeType in visitor) { + if (shouldIgnoreKey(nodeType)) continue; + + var fns = visitor[nodeType]; + + if (typeof fns === "function") { + visitor[nodeType] = fns = { enter: fns }; + } + + var aliases = t.FLIPPED_ALIAS_KEYS[nodeType]; + if (!aliases) continue; + + // clear it form the visitor + delete visitor[nodeType]; + + for (var alias of (aliases: Array)) { + var existing = visitor[alias]; + if (existing) { + if (mergeConflicts) { + merge(fns, existing); + } + } else { + visitor[alias] = fns; + } + } + } + + // handle type wrappers + for (let nodeType in visitor) { + if (shouldIgnoreKey(nodeType)) continue; + + var wrapper = typeWrappers[nodeType]; + if (!wrapper) continue; + + // wrap all the functions + var fns = visitor[nodeType]; + for (var type in fns) { + fns[type] = wrapper.wrap(fns[type]); + } + + // clear it from the visitor + delete visitor[nodeType]; + + // merge the visitor if necessary or just put it back in + if (visitor[wrapper.type]) { + merge(visitor[wrapper.type], fns); + } else { + visitor[wrapper.type] = fns; + } + } + + return visitor; +} + +export function verify(visitor) { + if (visitor._verified) return; + + if (typeof visitor === "function") { + throw new Error(messages.get("traverseVerifyRootFunction")); + } + + if (!visitor.enter) visitor.enter = function () { }; + if (!visitor.exit) visitor.exit = function () { }; + if (!visitor.shouldSkip) visitor.shouldSkip = function () { return false; }; + + for (var nodeType in visitor) { + if (shouldIgnoreKey(nodeType)) continue; + + if (!t.VISITOR_KEYS[nodeType]) { + throw new Error(messages.get("traverseVerifyNodeType", nodeType)); + } + + var visitors = visitor[nodeType]; + + if (typeof visitors === "function") { + throw new Error(messages.get("traverseVerifyVisitorFunction", nodeType)); + } else if (typeof visitors === "object") { + for (var visitorKey in visitors) { + if (visitorKey === "enter" || visitorKey === "exit") continue; + throw new Error(messages.get("traverseVerifyVisitorProperty", nodeType, visitorKey)); + } + } + } + + visitor._verified = true; +} + +function shouldIgnoreKey(key) { + // internal/hidden key + if (key[0] === "_") return true; + + // ignore function keys + if (key === "enter" || key === "exit" || key === "shouldSkip") return true; + + // ignore other options + if (key === "blacklist" || key === "noScope") return true; + + return false; +} + +function merge(visitor1, visitor2) { + for (var key in visitor1) { + visitor2[key] = (visitor2[alias] || []).concat(visitor1[key]); + } +} diff --git a/src/babel/traversal/visitors/wrappers.js b/src/babel/traversal/visitors/wrappers.js new file mode 100644 index 0000000000..c57a5147ef --- /dev/null +++ b/src/babel/traversal/visitors/wrappers.js @@ -0,0 +1,10 @@ +export var ReferencedIdentifier = { + type: "Identifier", + wrap(fn) { + return function () { + if (this.isReferencedIdentifier()) { + return fn.apply(this, arguments); + } + }; + } +}