diff --git a/packages/babel-generator/src/generators/base.js b/packages/babel-generator/src/generators/base.js index cb98733e55..dd45014377 100644 --- a/packages/babel-generator/src/generators/base.js +++ b/packages/babel-generator/src/generators/base.js @@ -82,3 +82,13 @@ export function DirectiveLiteral(node: Object) { export function InterpreterDirective(node: Object) { this.token(`#!${node.value}\n`); } + +export function Placeholder(node: Object) { + this.token("%%"); + this.print(node.name); + this.token("%%"); + + if (node.expectedNode === "Statement") { + this.semicolon(); + } +} diff --git a/packages/babel-generator/test/fixtures/misc/placeholders/input.js b/packages/babel-generator/test/fixtures/misc/placeholders/input.js new file mode 100644 index 0000000000..42eb25727b --- /dev/null +++ b/packages/babel-generator/test/fixtures/misc/placeholders/input.js @@ -0,0 +1,8 @@ +var %%a%% = %%b%% + +%%c%% + +class %%d%% {} +class A %%e%% + +function %%f%%(...%%g%%) %%h%% diff --git a/packages/babel-generator/test/fixtures/misc/placeholders/options.json b/packages/babel-generator/test/fixtures/misc/placeholders/options.json new file mode 100644 index 0000000000..92404e5012 --- /dev/null +++ b/packages/babel-generator/test/fixtures/misc/placeholders/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["placeholders"] +} diff --git a/packages/babel-generator/test/fixtures/misc/placeholders/output.js b/packages/babel-generator/test/fixtures/misc/placeholders/output.js new file mode 100644 index 0000000000..82ad54ecc6 --- /dev/null +++ b/packages/babel-generator/test/fixtures/misc/placeholders/output.js @@ -0,0 +1,8 @@ +var %%a%% = %%b%%; +%%c%%; + +class %%d%% {} + +class A %%e%% + +function %%f%%(...%%g%%) %%h%% \ No newline at end of file diff --git a/packages/babel-types/scripts/generators/generateValidators.js b/packages/babel-types/scripts/generators/generateValidators.js index 1e1ed321be..1455f99e5b 100644 --- a/packages/babel-types/scripts/generators/generateValidators.js +++ b/packages/babel-types/scripts/generators/generateValidators.js @@ -1,14 +1,37 @@ "use strict"; const definitions = require("../../lib/definitions"); +const has = Function.call.bind(Object.prototype.hasOwnProperty); + +function joinComparisons(leftArr, right) { + return ( + leftArr.map(JSON.stringify).join(` === ${right} || `) + ` === ${right}` + ); +} + function addIsHelper(type, aliasKeys, deprecated) { const targetType = JSON.stringify(type); let aliasSource = ""; if (aliasKeys) { - aliasSource = - " || " + - aliasKeys.map(JSON.stringify).join(" === nodeType || ") + - " === nodeType"; + aliasSource = " || " + joinComparisons(aliasKeys, "nodeType"); + } + + let placeholderSource = ""; + const placeholderTypes = []; + if ( + definitions.PLACEHOLDERS.includes(type) && + has(definitions.FLIPPED_ALIAS_KEYS, type) + ) { + placeholderTypes.push(type); + } + if (has(definitions.PLACEHOLDERS_FLIPPED_ALIAS, type)) { + placeholderTypes.push(...definitions.PLACEHOLDERS_FLIPPED_ALIAS[type]); + } + if (placeholderTypes.length > 0) { + placeholderSource = + ' || nodeType === "Placeholder" && (' + + joinComparisons(placeholderTypes, "node.expectedNode") + + ")"; } return `export function is${type}(node: ?Object, opts?: Object): boolean { @@ -16,7 +39,7 @@ function addIsHelper(type, aliasKeys, deprecated) { if (!node) return false; const nodeType = node.type; - if (nodeType === ${targetType}${aliasSource}) { + if (nodeType === ${targetType}${aliasSource}${placeholderSource}) { if (typeof opts === "undefined") { return true; } else { diff --git a/packages/babel-types/src/asserts/generated/index.js b/packages/babel-types/src/asserts/generated/index.js index 75fcbd6a8d..3a4e81d347 100644 --- a/packages/babel-types/src/asserts/generated/index.js +++ b/packages/babel-types/src/asserts/generated/index.js @@ -660,6 +660,9 @@ export function assertJSXClosingFragment( export function assertNoop(node: Object, opts?: Object = {}): void { assert("Noop", node, opts); } +export function assertPlaceholder(node: Object, opts?: Object = {}): void { + assert("Placeholder", node, opts); +} export function assertArgumentPlaceholder( node: Object, opts?: Object = {}, diff --git a/packages/babel-types/src/builders/generated/index.js b/packages/babel-types/src/builders/generated/index.js index e7af718d9c..e4ca375762 100644 --- a/packages/babel-types/src/builders/generated/index.js +++ b/packages/babel-types/src/builders/generated/index.js @@ -596,6 +596,10 @@ export function Noop(...args: Array): Object { return builder("Noop", ...args); } export { Noop as noop }; +export function Placeholder(...args: Array): Object { + return builder("Placeholder", ...args); +} +export { Placeholder as placeholder }; export function ArgumentPlaceholder(...args: Array): Object { return builder("ArgumentPlaceholder", ...args); } diff --git a/packages/babel-types/src/definitions/index.js b/packages/babel-types/src/definitions/index.js index 6bab249930..d6e1763ebd 100644 --- a/packages/babel-types/src/definitions/index.js +++ b/packages/babel-types/src/definitions/index.js @@ -15,6 +15,11 @@ import { BUILDER_KEYS, DEPRECATED_KEYS, } from "./utils"; +import { + PLACEHOLDERS, + PLACEHOLDERS_ALIAS, + PLACEHOLDERS_FLIPPED_ALIAS, +} from "./placeholders"; // We do this here, because at this point the visitor keys should be ready and setup toFastProperties(VISITOR_KEYS); @@ -24,6 +29,9 @@ toFastProperties(NODE_FIELDS); toFastProperties(BUILDER_KEYS); toFastProperties(DEPRECATED_KEYS); +toFastProperties(PLACEHOLDERS_ALIAS); +toFastProperties(PLACEHOLDERS_FLIPPED_ALIAS); + const TYPES: Array = Object.keys(VISITOR_KEYS) .concat(Object.keys(FLIPPED_ALIAS_KEYS)) .concat(Object.keys(DEPRECATED_KEYS)); @@ -35,5 +43,8 @@ export { NODE_FIELDS, BUILDER_KEYS, DEPRECATED_KEYS, + PLACEHOLDERS, + PLACEHOLDERS_ALIAS, + PLACEHOLDERS_FLIPPED_ALIAS, TYPES, }; diff --git a/packages/babel-types/src/definitions/misc.js b/packages/babel-types/src/definitions/misc.js index 6bc22babc0..e319235b2c 100644 --- a/packages/babel-types/src/definitions/misc.js +++ b/packages/babel-types/src/definitions/misc.js @@ -1,6 +1,21 @@ // @flow -import defineType from "./utils"; +import defineType, { assertNodeType, assertOneOf } from "./utils"; +import { PLACEHOLDERS } from "./placeholders"; defineType("Noop", { visitor: [], }); + +defineType("Placeholder", { + visitor: [], + builder: ["expectedNode", "name"], + // aliases: [], defined in placeholders.js + fields: { + name: { + validate: assertNodeType("Identifier"), + }, + expectedNode: { + validate: assertOneOf(...PLACEHOLDERS), + }, + }, +}); diff --git a/packages/babel-types/src/definitions/placeholders.js b/packages/babel-types/src/definitions/placeholders.js new file mode 100644 index 0000000000..ba5e13cc1c --- /dev/null +++ b/packages/babel-types/src/definitions/placeholders.js @@ -0,0 +1,33 @@ +import { ALIAS_KEYS } from "./utils"; + +export const PLACEHOLDERS = [ + "Identifier", + "StringLiteral", + "Expression", + "Statement", + "Declaration", + "BlockStatement", + "ClassBody", + "Pattern", +]; + +export const PLACEHOLDERS_ALIAS: { [string]: Array } = { + Declaration: ["Statement"], + Pattern: ["PatternLike", "LVal"], +}; + +for (const type of PLACEHOLDERS) { + const alias = ALIAS_KEYS[type]; + if (alias && alias.length) PLACEHOLDERS_ALIAS[type] = alias; +} + +export const PLACEHOLDERS_FLIPPED_ALIAS: { [string]: Array } = {}; + +Object.keys(PLACEHOLDERS_ALIAS).forEach(type => { + PLACEHOLDERS_ALIAS[type].forEach(alias => { + if (!Object.hasOwnProperty.call(PLACEHOLDERS_FLIPPED_ALIAS, alias)) { + PLACEHOLDERS_FLIPPED_ALIAS[alias] = []; + } + PLACEHOLDERS_FLIPPED_ALIAS[alias].push(type); + }); +}); diff --git a/packages/babel-types/src/index.js b/packages/babel-types/src/index.js index 56d36125a9..d3311dfc9f 100644 --- a/packages/babel-types/src/index.js +++ b/packages/babel-types/src/index.js @@ -100,6 +100,7 @@ export { default as isImmutable } from "./validators/isImmutable"; export { default as isLet } from "./validators/isLet"; export { default as isNode } from "./validators/isNode"; export { default as isNodesEquivalent } from "./validators/isNodesEquivalent"; +export { default as isPlaceholderType } from "./validators/isPlaceholderType"; export { default as isReferenced } from "./validators/isReferenced"; export { default as isScope } from "./validators/isScope"; export { default as isSpecifierDefault } from "./validators/isSpecifierDefault"; diff --git a/packages/babel-types/src/validators/generated/index.js b/packages/babel-types/src/validators/generated/index.js index 969c6cdc2d..890999e5d0 100644 --- a/packages/babel-types/src/validators/generated/index.js +++ b/packages/babel-types/src/validators/generated/index.js @@ -2093,6 +2093,20 @@ export function isNoop(node: ?Object, opts?: Object): boolean { return false; } +export function isPlaceholder(node: ?Object, opts?: Object): boolean { + if (!node) return false; + + const nodeType = node.type; + if (nodeType === "Placeholder") { + if (typeof opts === "undefined") { + return true; + } else { + return shallowEqual(node, opts); + } + } + + return false; +} export function isArgumentPlaceholder(node: ?Object, opts?: Object): boolean { if (!node) return false; @@ -3280,7 +3294,11 @@ export function isExpression(node: ?Object, opts?: Object): boolean { "BigIntLiteral" === nodeType || "TSAsExpression" === nodeType || "TSTypeAssertion" === nodeType || - "TSNonNullExpression" === nodeType + "TSNonNullExpression" === nodeType || + (nodeType === "Placeholder" && + ("Expression" === node.expectedNode || + "Identifier" === node.expectedNode || + "StringLiteral" === node.expectedNode)) ) { if (typeof opts === "undefined") { return true; @@ -3331,7 +3349,8 @@ export function isScopable(node: ?Object, opts?: Object): boolean { "ClassExpression" === nodeType || "ForOfStatement" === nodeType || "ClassMethod" === nodeType || - "ClassPrivateMethod" === nodeType + "ClassPrivateMethod" === nodeType || + (nodeType === "Placeholder" && "BlockStatement" === node.expectedNode) ) { if (typeof opts === "undefined") { return true; @@ -3362,7 +3381,8 @@ export function isBlockParent(node: ?Object, opts?: Object): boolean { "ArrowFunctionExpression" === nodeType || "ForOfStatement" === nodeType || "ClassMethod" === nodeType || - "ClassPrivateMethod" === nodeType + "ClassPrivateMethod" === nodeType || + (nodeType === "Placeholder" && "BlockStatement" === node.expectedNode) ) { if (typeof opts === "undefined") { return true; @@ -3380,7 +3400,8 @@ export function isBlock(node: ?Object, opts?: Object): boolean { if ( nodeType === "Block" || "BlockStatement" === nodeType || - "Program" === nodeType + "Program" === nodeType || + (nodeType === "Placeholder" && "BlockStatement" === node.expectedNode) ) { if (typeof opts === "undefined") { return true; @@ -3442,7 +3463,11 @@ export function isStatement(node: ?Object, opts?: Object): boolean { "TSModuleDeclaration" === nodeType || "TSImportEqualsDeclaration" === nodeType || "TSExportAssignment" === nodeType || - "TSNamespaceExportDeclaration" === nodeType + "TSNamespaceExportDeclaration" === nodeType || + (nodeType === "Placeholder" && + ("Statement" === node.expectedNode || + "Declaration" === node.expectedNode || + "BlockStatement" === node.expectedNode)) ) { if (typeof opts === "undefined") { return true; @@ -3667,7 +3692,8 @@ export function isPureish(node: ?Object, opts?: Object): boolean { "ArrowFunctionExpression" === nodeType || "ClassDeclaration" === nodeType || "ClassExpression" === nodeType || - "BigIntLiteral" === nodeType + "BigIntLiteral" === nodeType || + (nodeType === "Placeholder" && "StringLiteral" === node.expectedNode) ) { if (typeof opts === "undefined") { return true; @@ -3708,7 +3734,8 @@ export function isDeclaration(node: ?Object, opts?: Object): boolean { "TSInterfaceDeclaration" === nodeType || "TSTypeAliasDeclaration" === nodeType || "TSEnumDeclaration" === nodeType || - "TSModuleDeclaration" === nodeType + "TSModuleDeclaration" === nodeType || + (nodeType === "Placeholder" && "Declaration" === node.expectedNode) ) { if (typeof opts === "undefined") { return true; @@ -3729,7 +3756,9 @@ export function isPatternLike(node: ?Object, opts?: Object): boolean { "RestElement" === nodeType || "AssignmentPattern" === nodeType || "ArrayPattern" === nodeType || - "ObjectPattern" === nodeType + "ObjectPattern" === nodeType || + (nodeType === "Placeholder" && + ("Pattern" === node.expectedNode || "Identifier" === node.expectedNode)) ) { if (typeof opts === "undefined") { return true; @@ -3752,7 +3781,9 @@ export function isLVal(node: ?Object, opts?: Object): boolean { "AssignmentPattern" === nodeType || "ArrayPattern" === nodeType || "ObjectPattern" === nodeType || - "TSParameterProperty" === nodeType + "TSParameterProperty" === nodeType || + (nodeType === "Placeholder" && + ("Pattern" === node.expectedNode || "Identifier" === node.expectedNode)) ) { if (typeof opts === "undefined") { return true; @@ -3770,7 +3801,8 @@ export function isTSEntityName(node: ?Object, opts?: Object): boolean { if ( nodeType === "TSEntityName" || "Identifier" === nodeType || - "TSQualifiedName" === nodeType + "TSQualifiedName" === nodeType || + (nodeType === "Placeholder" && "Identifier" === node.expectedNode) ) { if (typeof opts === "undefined") { return true; @@ -3793,7 +3825,8 @@ export function isLiteral(node: ?Object, opts?: Object): boolean { "BooleanLiteral" === nodeType || "RegExpLiteral" === nodeType || "TemplateLiteral" === nodeType || - "BigIntLiteral" === nodeType + "BigIntLiteral" === nodeType || + (nodeType === "Placeholder" && "StringLiteral" === node.expectedNode) ) { if (typeof opts === "undefined") { return true; @@ -3824,7 +3857,8 @@ export function isImmutable(node: ?Object, opts?: Object): boolean { "JSXFragment" === nodeType || "JSXOpeningFragment" === nodeType || "JSXClosingFragment" === nodeType || - "BigIntLiteral" === nodeType + "BigIntLiteral" === nodeType || + (nodeType === "Placeholder" && "StringLiteral" === node.expectedNode) ) { if (typeof opts === "undefined") { return true; @@ -3940,7 +3974,8 @@ export function isPattern(node: ?Object, opts?: Object): boolean { nodeType === "Pattern" || "AssignmentPattern" === nodeType || "ArrayPattern" === nodeType || - "ObjectPattern" === nodeType + "ObjectPattern" === nodeType || + (nodeType === "Placeholder" && "Pattern" === node.expectedNode) ) { if (typeof opts === "undefined") { return true; diff --git a/packages/babel-types/src/validators/is.js b/packages/babel-types/src/validators/is.js index 25cd7c735f..50cf945ff8 100644 --- a/packages/babel-types/src/validators/is.js +++ b/packages/babel-types/src/validators/is.js @@ -1,6 +1,8 @@ // @flow import shallowEqual from "../utils/shallowEqual"; import isType from "./isType"; +import isPlaceholderType from "./isPlaceholderType"; +import { FLIPPED_ALIAS_KEYS } from "../definitions"; /** * Returns whether `node` is of given `type`. @@ -11,7 +13,21 @@ export default function is(type: string, node: Object, opts?: Object): boolean { if (!node) return false; const matches = isType(node.type, type); - if (!matches) return false; + if (!matches) { + if (!opts && node.type === "Placeholder" && type in FLIPPED_ALIAS_KEYS) { + // We can only return true if the placeholder doesn't replace a real node, + // but it replaces a category of nodes (an alias). + // + // t.is("Identifier", node) gives some guarantees about node's shape, so we + // can't say that Placeholder(expectedNode: "Identifier") is an identifier + // because it doesn't have the same properties. + // On the other hand, t.is("Expression", node) doesn't say anything about + // the shape of node because Expression can be many different nodes: we can, + // and should, safely report expression placeholders as Expressions. + return isPlaceholderType(node.expectedNode, type); + } + return false; + } if (typeof opts === "undefined") { return true; diff --git a/packages/babel-types/src/validators/isPlaceholderType.js b/packages/babel-types/src/validators/isPlaceholderType.js new file mode 100644 index 0000000000..fa775b7ed8 --- /dev/null +++ b/packages/babel-types/src/validators/isPlaceholderType.js @@ -0,0 +1,21 @@ +// @flow +import { PLACEHOLDERS_ALIAS } from "../definitions"; + +/** + * Test if a `placeholderType` is a `targetType` or if `targetType` is an alias of `placeholderType`. + */ +export default function isPlaceholderType( + placeholderType: ?string, + targetType: string, +): boolean { + if (placeholderType === targetType) return true; + + const aliases: ?Array = PLACEHOLDERS_ALIAS[placeholderType]; + if (aliases) { + for (const alias of aliases) { + if (targetType === alias) return true; + } + } + + return false; +} diff --git a/packages/babel-types/test/validators.js b/packages/babel-types/test/validators.js index 3b4ec0d933..f08dedab1a 100644 --- a/packages/babel-types/test/validators.js +++ b/packages/babel-types/test/validators.js @@ -216,4 +216,95 @@ describe("validators", function() { expect(t.isType(undefined, "Expression")).toBe(false); }); }); + + describe("placeholders", function() { + describe("isPlaceholderType", function() { + describe("when placeholderType is a specific node type", function() { + const placeholder = "Identifier"; + + it("returns true if targetType is placeholderType", function() { + expect(t.isPlaceholderType(placeholder, "Identifier")).toBe(true); + }); + it("returns true if targetType an alias for placeholderType", function() { + expect(t.isPlaceholderType(placeholder, "Expression")).toBe(true); + }); + it("returns false for unrelated types", function() { + expect(t.isPlaceholderType(placeholder, "String")).toBe(false); + }); + }); + + describe("when placeholderType is a generic alias type", function() { + const placeholder = "Pattern"; + + it("returns true if targetType is placeholderType", function() { + expect(t.isPlaceholderType(placeholder, "Pattern")).toBe(true); + }); + it("returns true if targetType an alias for placeholderType", function() { + expect(t.isPlaceholderType(placeholder, "LVal")).toBe(true); + }); + it("returns false for unrelated types", function() { + expect(t.isPlaceholderType(placeholder, "Expression")).toBe(false); + }); + it("returns false if targetType is aliased by placeholderType", function() { + // i.e. a Pattern might not be an Identifier + expect(t.isPlaceholderType(placeholder, "Identifier")).toBe(false); + }); + }); + }); + + describe("is", function() { + describe("when the placeholder matches a specific node", function() { + const identifier = t.placeholder("Identifier", t.identifier("foo")); + + it("returns false if targetType is expectedNode", function() { + expect(t.is("Identifier", identifier)).toBe(false); + }); + it("returns true if targetType is an alias", function() { + expect(t.is("LVal", identifier)).toBe(true); + }); + }); + + describe("when the placeholder matches a generic alias", function() { + const pattern = t.placeholder("Pattern", t.identifier("bar")); + + it("returns false if targetType is aliased as expectedNode", function() { + // i.e. a Pattern might not be an Identifier + expect(t.is("Identifier", pattern)).toBe(false); + }); + it("returns true if targetType is expectedNode", function() { + expect(t.is("Pattern", pattern)).toBe(true); + }); + it("returns true if targetType is an alias for expectedNode", function() { + expect(t.is("LVal", pattern)).toBe(true); + }); + }); + }); + + describe("is[Type]", function() { + describe("when the placeholder matches a specific node", function() { + const identifier = t.placeholder("Identifier", t.identifier("foo")); + + it("returns false if targetType is expectedNode", function() { + expect(t.isIdentifier(identifier)).toBe(false); + }); + it("returns true if targetType is an alias", function() { + expect(t.isLVal(identifier)).toBe(true); + }); + }); + + describe("when the placeholder matches a generic alias", function() { + const pattern = t.placeholder("Pattern", t.identifier("bar")); + + it("returns false if targetType is aliased as expectedNode", function() { + expect(t.isIdentifier(pattern)).toBe(false); + }); + it("returns true if targetType is expectedNode", function() { + expect(t.isPattern(pattern)).toBe(true); + }); + it("returns true if targetType is an alias for expectedNode", function() { + expect(t.isLVal(pattern)).toBe(true); + }); + }); + }); + }); });