Add placeholders support to @babel/types and @babel/generator (#9542)

This commit is contained in:
Nicolò Ribaudo 2019-03-07 11:47:39 +01:00 committed by GitHub
parent 095e5c0e09
commit 15dfce33df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 302 additions and 20 deletions

View File

@ -82,3 +82,13 @@ export function DirectiveLiteral(node: Object) {
export function InterpreterDirective(node: Object) { export function InterpreterDirective(node: Object) {
this.token(`#!${node.value}\n`); this.token(`#!${node.value}\n`);
} }
export function Placeholder(node: Object) {
this.token("%%");
this.print(node.name);
this.token("%%");
if (node.expectedNode === "Statement") {
this.semicolon();
}
}

View File

@ -0,0 +1,8 @@
var %%a%% = %%b%%
%%c%%
class %%d%% {}
class A %%e%%
function %%f%%(...%%g%%) %%h%%

View File

@ -0,0 +1,3 @@
{
"plugins": ["placeholders"]
}

View File

@ -0,0 +1,8 @@
var %%a%% = %%b%%;
%%c%%;
class %%d%% {}
class A %%e%%
function %%f%%(...%%g%%) %%h%%

View File

@ -1,14 +1,37 @@
"use strict"; "use strict";
const definitions = require("../../lib/definitions"); 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) { function addIsHelper(type, aliasKeys, deprecated) {
const targetType = JSON.stringify(type); const targetType = JSON.stringify(type);
let aliasSource = ""; let aliasSource = "";
if (aliasKeys) { if (aliasKeys) {
aliasSource = aliasSource = " || " + joinComparisons(aliasKeys, "nodeType");
" || " + }
aliasKeys.map(JSON.stringify).join(" === nodeType || ") +
" === 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 { return `export function is${type}(node: ?Object, opts?: Object): boolean {
@ -16,7 +39,7 @@ function addIsHelper(type, aliasKeys, deprecated) {
if (!node) return false; if (!node) return false;
const nodeType = node.type; const nodeType = node.type;
if (nodeType === ${targetType}${aliasSource}) { if (nodeType === ${targetType}${aliasSource}${placeholderSource}) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
} else { } else {

View File

@ -660,6 +660,9 @@ export function assertJSXClosingFragment(
export function assertNoop(node: Object, opts?: Object = {}): void { export function assertNoop(node: Object, opts?: Object = {}): void {
assert("Noop", node, opts); assert("Noop", node, opts);
} }
export function assertPlaceholder(node: Object, opts?: Object = {}): void {
assert("Placeholder", node, opts);
}
export function assertArgumentPlaceholder( export function assertArgumentPlaceholder(
node: Object, node: Object,
opts?: Object = {}, opts?: Object = {},

View File

@ -596,6 +596,10 @@ export function Noop(...args: Array<any>): Object {
return builder("Noop", ...args); return builder("Noop", ...args);
} }
export { Noop as noop }; export { Noop as noop };
export function Placeholder(...args: Array<any>): Object {
return builder("Placeholder", ...args);
}
export { Placeholder as placeholder };
export function ArgumentPlaceholder(...args: Array<any>): Object { export function ArgumentPlaceholder(...args: Array<any>): Object {
return builder("ArgumentPlaceholder", ...args); return builder("ArgumentPlaceholder", ...args);
} }

View File

@ -15,6 +15,11 @@ import {
BUILDER_KEYS, BUILDER_KEYS,
DEPRECATED_KEYS, DEPRECATED_KEYS,
} from "./utils"; } 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 // We do this here, because at this point the visitor keys should be ready and setup
toFastProperties(VISITOR_KEYS); toFastProperties(VISITOR_KEYS);
@ -24,6 +29,9 @@ toFastProperties(NODE_FIELDS);
toFastProperties(BUILDER_KEYS); toFastProperties(BUILDER_KEYS);
toFastProperties(DEPRECATED_KEYS); toFastProperties(DEPRECATED_KEYS);
toFastProperties(PLACEHOLDERS_ALIAS);
toFastProperties(PLACEHOLDERS_FLIPPED_ALIAS);
const TYPES: Array<string> = Object.keys(VISITOR_KEYS) const TYPES: Array<string> = Object.keys(VISITOR_KEYS)
.concat(Object.keys(FLIPPED_ALIAS_KEYS)) .concat(Object.keys(FLIPPED_ALIAS_KEYS))
.concat(Object.keys(DEPRECATED_KEYS)); .concat(Object.keys(DEPRECATED_KEYS));
@ -35,5 +43,8 @@ export {
NODE_FIELDS, NODE_FIELDS,
BUILDER_KEYS, BUILDER_KEYS,
DEPRECATED_KEYS, DEPRECATED_KEYS,
PLACEHOLDERS,
PLACEHOLDERS_ALIAS,
PLACEHOLDERS_FLIPPED_ALIAS,
TYPES, TYPES,
}; };

View File

@ -1,6 +1,21 @@
// @flow // @flow
import defineType from "./utils"; import defineType, { assertNodeType, assertOneOf } from "./utils";
import { PLACEHOLDERS } from "./placeholders";
defineType("Noop", { defineType("Noop", {
visitor: [], visitor: [],
}); });
defineType("Placeholder", {
visitor: [],
builder: ["expectedNode", "name"],
// aliases: [], defined in placeholders.js
fields: {
name: {
validate: assertNodeType("Identifier"),
},
expectedNode: {
validate: assertOneOf(...PLACEHOLDERS),
},
},
});

View File

@ -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<string> } = {
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<string> } = {};
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);
});
});

View File

@ -100,6 +100,7 @@ export { default as isImmutable } from "./validators/isImmutable";
export { default as isLet } from "./validators/isLet"; export { default as isLet } from "./validators/isLet";
export { default as isNode } from "./validators/isNode"; export { default as isNode } from "./validators/isNode";
export { default as isNodesEquivalent } from "./validators/isNodesEquivalent"; export { default as isNodesEquivalent } from "./validators/isNodesEquivalent";
export { default as isPlaceholderType } from "./validators/isPlaceholderType";
export { default as isReferenced } from "./validators/isReferenced"; export { default as isReferenced } from "./validators/isReferenced";
export { default as isScope } from "./validators/isScope"; export { default as isScope } from "./validators/isScope";
export { default as isSpecifierDefault } from "./validators/isSpecifierDefault"; export { default as isSpecifierDefault } from "./validators/isSpecifierDefault";

View File

@ -2093,6 +2093,20 @@ export function isNoop(node: ?Object, opts?: Object): boolean {
return false; 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 { export function isArgumentPlaceholder(node: ?Object, opts?: Object): boolean {
if (!node) return false; if (!node) return false;
@ -3280,7 +3294,11 @@ export function isExpression(node: ?Object, opts?: Object): boolean {
"BigIntLiteral" === nodeType || "BigIntLiteral" === nodeType ||
"TSAsExpression" === nodeType || "TSAsExpression" === nodeType ||
"TSTypeAssertion" === nodeType || "TSTypeAssertion" === nodeType ||
"TSNonNullExpression" === nodeType "TSNonNullExpression" === nodeType ||
(nodeType === "Placeholder" &&
("Expression" === node.expectedNode ||
"Identifier" === node.expectedNode ||
"StringLiteral" === node.expectedNode))
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
@ -3331,7 +3349,8 @@ export function isScopable(node: ?Object, opts?: Object): boolean {
"ClassExpression" === nodeType || "ClassExpression" === nodeType ||
"ForOfStatement" === nodeType || "ForOfStatement" === nodeType ||
"ClassMethod" === nodeType || "ClassMethod" === nodeType ||
"ClassPrivateMethod" === nodeType "ClassPrivateMethod" === nodeType ||
(nodeType === "Placeholder" && "BlockStatement" === node.expectedNode)
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
@ -3362,7 +3381,8 @@ export function isBlockParent(node: ?Object, opts?: Object): boolean {
"ArrowFunctionExpression" === nodeType || "ArrowFunctionExpression" === nodeType ||
"ForOfStatement" === nodeType || "ForOfStatement" === nodeType ||
"ClassMethod" === nodeType || "ClassMethod" === nodeType ||
"ClassPrivateMethod" === nodeType "ClassPrivateMethod" === nodeType ||
(nodeType === "Placeholder" && "BlockStatement" === node.expectedNode)
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
@ -3380,7 +3400,8 @@ export function isBlock(node: ?Object, opts?: Object): boolean {
if ( if (
nodeType === "Block" || nodeType === "Block" ||
"BlockStatement" === nodeType || "BlockStatement" === nodeType ||
"Program" === nodeType "Program" === nodeType ||
(nodeType === "Placeholder" && "BlockStatement" === node.expectedNode)
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
@ -3442,7 +3463,11 @@ export function isStatement(node: ?Object, opts?: Object): boolean {
"TSModuleDeclaration" === nodeType || "TSModuleDeclaration" === nodeType ||
"TSImportEqualsDeclaration" === nodeType || "TSImportEqualsDeclaration" === nodeType ||
"TSExportAssignment" === nodeType || "TSExportAssignment" === nodeType ||
"TSNamespaceExportDeclaration" === nodeType "TSNamespaceExportDeclaration" === nodeType ||
(nodeType === "Placeholder" &&
("Statement" === node.expectedNode ||
"Declaration" === node.expectedNode ||
"BlockStatement" === node.expectedNode))
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
@ -3667,7 +3692,8 @@ export function isPureish(node: ?Object, opts?: Object): boolean {
"ArrowFunctionExpression" === nodeType || "ArrowFunctionExpression" === nodeType ||
"ClassDeclaration" === nodeType || "ClassDeclaration" === nodeType ||
"ClassExpression" === nodeType || "ClassExpression" === nodeType ||
"BigIntLiteral" === nodeType "BigIntLiteral" === nodeType ||
(nodeType === "Placeholder" && "StringLiteral" === node.expectedNode)
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
@ -3708,7 +3734,8 @@ export function isDeclaration(node: ?Object, opts?: Object): boolean {
"TSInterfaceDeclaration" === nodeType || "TSInterfaceDeclaration" === nodeType ||
"TSTypeAliasDeclaration" === nodeType || "TSTypeAliasDeclaration" === nodeType ||
"TSEnumDeclaration" === nodeType || "TSEnumDeclaration" === nodeType ||
"TSModuleDeclaration" === nodeType "TSModuleDeclaration" === nodeType ||
(nodeType === "Placeholder" && "Declaration" === node.expectedNode)
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
@ -3729,7 +3756,9 @@ export function isPatternLike(node: ?Object, opts?: Object): boolean {
"RestElement" === nodeType || "RestElement" === nodeType ||
"AssignmentPattern" === nodeType || "AssignmentPattern" === nodeType ||
"ArrayPattern" === nodeType || "ArrayPattern" === nodeType ||
"ObjectPattern" === nodeType "ObjectPattern" === nodeType ||
(nodeType === "Placeholder" &&
("Pattern" === node.expectedNode || "Identifier" === node.expectedNode))
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
@ -3752,7 +3781,9 @@ export function isLVal(node: ?Object, opts?: Object): boolean {
"AssignmentPattern" === nodeType || "AssignmentPattern" === nodeType ||
"ArrayPattern" === nodeType || "ArrayPattern" === nodeType ||
"ObjectPattern" === nodeType || "ObjectPattern" === nodeType ||
"TSParameterProperty" === nodeType "TSParameterProperty" === nodeType ||
(nodeType === "Placeholder" &&
("Pattern" === node.expectedNode || "Identifier" === node.expectedNode))
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
@ -3770,7 +3801,8 @@ export function isTSEntityName(node: ?Object, opts?: Object): boolean {
if ( if (
nodeType === "TSEntityName" || nodeType === "TSEntityName" ||
"Identifier" === nodeType || "Identifier" === nodeType ||
"TSQualifiedName" === nodeType "TSQualifiedName" === nodeType ||
(nodeType === "Placeholder" && "Identifier" === node.expectedNode)
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
@ -3793,7 +3825,8 @@ export function isLiteral(node: ?Object, opts?: Object): boolean {
"BooleanLiteral" === nodeType || "BooleanLiteral" === nodeType ||
"RegExpLiteral" === nodeType || "RegExpLiteral" === nodeType ||
"TemplateLiteral" === nodeType || "TemplateLiteral" === nodeType ||
"BigIntLiteral" === nodeType "BigIntLiteral" === nodeType ||
(nodeType === "Placeholder" && "StringLiteral" === node.expectedNode)
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
@ -3824,7 +3857,8 @@ export function isImmutable(node: ?Object, opts?: Object): boolean {
"JSXFragment" === nodeType || "JSXFragment" === nodeType ||
"JSXOpeningFragment" === nodeType || "JSXOpeningFragment" === nodeType ||
"JSXClosingFragment" === nodeType || "JSXClosingFragment" === nodeType ||
"BigIntLiteral" === nodeType "BigIntLiteral" === nodeType ||
(nodeType === "Placeholder" && "StringLiteral" === node.expectedNode)
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;
@ -3940,7 +3974,8 @@ export function isPattern(node: ?Object, opts?: Object): boolean {
nodeType === "Pattern" || nodeType === "Pattern" ||
"AssignmentPattern" === nodeType || "AssignmentPattern" === nodeType ||
"ArrayPattern" === nodeType || "ArrayPattern" === nodeType ||
"ObjectPattern" === nodeType "ObjectPattern" === nodeType ||
(nodeType === "Placeholder" && "Pattern" === node.expectedNode)
) { ) {
if (typeof opts === "undefined") { if (typeof opts === "undefined") {
return true; return true;

View File

@ -1,6 +1,8 @@
// @flow // @flow
import shallowEqual from "../utils/shallowEqual"; import shallowEqual from "../utils/shallowEqual";
import isType from "./isType"; import isType from "./isType";
import isPlaceholderType from "./isPlaceholderType";
import { FLIPPED_ALIAS_KEYS } from "../definitions";
/** /**
* Returns whether `node` is of given `type`. * 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; if (!node) return false;
const matches = isType(node.type, type); 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") { if (typeof opts === "undefined") {
return true; return true;

View File

@ -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<string> = PLACEHOLDERS_ALIAS[placeholderType];
if (aliases) {
for (const alias of aliases) {
if (targetType === alias) return true;
}
}
return false;
}

View File

@ -216,4 +216,95 @@ describe("validators", function() {
expect(t.isType(undefined, "Expression")).toBe(false); 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);
});
});
});
});
}); });