Files
test-repo/src/language-js/printer-estree.js
T

751 lines
21 KiB
JavaScript

// TODO(azz): anything that imports from main shouldn't be in a `language-*` dir.
import { printDanglingComments } from "../main/comments/print.js";
import printIgnored from "../main/print-ignored.js";
import hasNewline from "../utils/has-newline.js";
import {
join,
line,
hardline,
softline,
group,
indent,
} from "../document/builders.js";
import { replaceEndOfLine, inheritLabel } from "../document/utils.js";
import UnexpectedNodeError from "../utils/unexpected-node-error.js";
import isNonEmptyArray from "../utils/is-non-empty-array.js";
import pathNeedsParens from "./needs-parens.js";
import {
hasComment,
CommentCheckFlags,
isNextLineEmpty,
needsHardlineAfterDanglingComment,
isCallExpression,
isMemberExpression,
isArrayOrTupleExpression,
isObjectOrRecordExpression,
startsWithNoLookaheadToken,
createTypeCheckFunction,
} from "./utils/index.js";
import { locStart, locEnd } from "./loc.js";
import isBlockComment from "./utils/is-block-comment.js";
import isIgnored from "./utils/is-ignored.js";
import { printHtmlBinding } from "./print/html-binding.js";
import { printAngular } from "./print/angular.js";
import { printJsx } from "./print/jsx.js";
import { printFlow } from "./print/flow.js";
import { printTypescript } from "./print/typescript.js";
import {
printOptionalToken,
printBindExpressionCallee,
adjustClause,
printRestSpread,
printDefiniteToken,
printDeclareToken,
} from "./print/misc.js";
import {
printImportDeclaration,
printExportDeclaration,
printModuleSpecifier,
} from "./print/module.js";
import { printTernary } from "./print/ternary.js";
import {
printTaggedTemplateLiteral,
printTemplateLiteral,
} from "./print/template-literal.js";
import { printArray } from "./print/array.js";
import { printObject } from "./print/object.js";
import {
printClass,
printClassMethod,
printClassProperty,
printClassBody,
} from "./print/class.js";
import { printProperty } from "./print/property.js";
import {
printFunction,
printMethod,
printReturnStatement,
printThrowStatement,
} from "./print/function.js";
import { printArrowFunction } from "./print/arrow-function.js";
import { printCallExpression } from "./print/call-expression.js";
import {
printVariableDeclarator,
printAssignmentExpression,
} from "./print/assignment.js";
import { printBinaryishExpression } from "./print/binaryish.js";
import { printStatementSequence } from "./print/statement.js";
import { printMemberExpression } from "./print/member.js";
import { printBlock, printBlockBody } from "./print/block.js";
import { printLiteral } from "./print/literal.js";
import { printDecorators } from "./print/decorators.js";
import { printTypeAnnotationProperty } from "./print/type-annotation.js";
import { shouldPrintLeadingSemicolon } from "./print/semicolon.js";
import { printExpressionStatement } from "./print/expression-statement.js";
/**
* @typedef {import("../common/ast-path.js").default} AstPath
* @typedef {import("../document/builders.js").Doc} Doc
*/
// Their decorators are handled themselves, and they don't need parentheses or leading semicolons
const shouldPrintDirectly = createTypeCheckFunction([
"ClassMethod",
"ClassPrivateMethod",
"ClassProperty",
"ClassAccessorProperty",
"AccessorProperty",
"TSAbstractAccessorProperty",
"PropertyDefinition",
"TSAbstractPropertyDefinition",
"ClassPrivateProperty",
"MethodDefinition",
"TSAbstractMethodDefinition",
"TSDeclareMethod",
]);
/**
* @param {AstPath} path
* @param {*} options
* @param {*} print
* @param {*} [args]
* @returns {Doc}
*/
function genericPrint(path, options, print, args) {
const doc = printPathNoParens(path, options, print, args);
if (!doc) {
return "";
}
const { node } = path;
if (shouldPrintDirectly(node)) {
return doc;
}
const hasDecorators = isNonEmptyArray(node.decorators);
const decoratorsDoc = printDecorators(path, options, print);
const isClassExpression = node.type === "ClassExpression";
// Nodes (except `ClassExpression`) with decorators can't have parentheses and don't need leading semicolons
if (hasDecorators && !isClassExpression) {
return inheritLabel(doc, (doc) => group([decoratorsDoc, doc]));
}
const needsParens = pathNeedsParens(path, options);
const needsSemi = shouldPrintLeadingSemicolon(path, options);
if (!decoratorsDoc && !needsParens && !needsSemi) {
return doc;
}
return inheritLabel(doc, (doc) => [
needsSemi ? ";" : "",
needsParens ? "(" : "",
needsParens && isClassExpression && hasDecorators
? [indent([line, decoratorsDoc, doc]), line]
: [decoratorsDoc, doc],
needsParens ? ")" : "",
]);
}
/**
* @param {AstPath} path
* @param {*} options
* @param {*} print
* @param {*} [args]
* @returns {Doc}
*/
function printPathNoParens(path, options, print, args) {
if (isIgnored(path)) {
return printIgnored(path, options);
}
for (const printer of [
printLiteral,
printHtmlBinding,
printAngular,
printJsx,
printFlow,
printTypescript,
]) {
const printed = printer(path, options, print);
if (printed !== undefined) {
return printed;
}
}
const { node } = path;
const semi = options.semi ? ";" : "";
/** @type{Doc[]} */
let parts = [];
switch (node.type) {
case "JsExpressionRoot":
return print("node");
case "JsonRoot":
return [print("node"), hardline];
case "File":
return print("program");
case "Program":
return printBlockBody(path, options, print);
// Babel extension.
case "EmptyStatement":
return "";
case "ExpressionStatement":
return printExpressionStatement(path, options, print);
case "ChainExpression":
return print("expression");
// Babel non-standard node. Used for Closure-style type casts. See postprocess.js.
case "ParenthesizedExpression": {
const shouldHug =
!hasComment(node.expression) &&
(isObjectOrRecordExpression(node.expression) ||
isArrayOrTupleExpression(node.expression));
if (shouldHug) {
return ["(", print("expression"), ")"];
}
return group([
"(",
indent([softline, print("expression")]),
softline,
")",
]);
}
case "AssignmentExpression":
return printAssignmentExpression(path, options, print);
case "VariableDeclarator":
return printVariableDeclarator(path, options, print);
case "BinaryExpression":
case "LogicalExpression":
return printBinaryishExpression(path, options, print);
case "AssignmentPattern":
return [print("left"), " = ", print("right")];
case "OptionalMemberExpression":
case "MemberExpression":
return printMemberExpression(path, options, print);
case "MetaProperty":
return [print("meta"), ".", print("property")];
case "BindExpression":
if (node.object) {
parts.push(print("object"));
}
parts.push(
group(
indent([softline, printBindExpressionCallee(path, options, print)])
)
);
return parts;
case "Identifier":
return [
node.name,
printOptionalToken(path),
printDefiniteToken(path),
printTypeAnnotationProperty(path, print),
];
case "V8IntrinsicIdentifier":
return ["%", node.name];
case "SpreadElement":
case "SpreadElementPattern":
case "SpreadPropertyPattern":
case "RestElement":
return printRestSpread(path, print);
case "FunctionDeclaration":
case "FunctionExpression":
return printFunction(path, print, options, args);
case "ArrowFunctionExpression":
return printArrowFunction(path, options, print, args);
case "YieldExpression":
parts.push("yield");
if (node.delegate) {
parts.push("*");
}
if (node.argument) {
parts.push(" ", print("argument"));
}
return parts;
case "AwaitExpression":
parts.push("await");
if (node.argument) {
parts.push(" ", print("argument"));
const { parent } = path;
if (
(isCallExpression(parent) && parent.callee === node) ||
(isMemberExpression(parent) && parent.object === node)
) {
parts = [indent([softline, ...parts]), softline];
// avoid printing `await (await` on one line
const parentAwaitOrBlock = path.findAncestor(
(node) =>
node.type === "AwaitExpression" || node.type === "BlockStatement"
);
if (
parentAwaitOrBlock?.type !== "AwaitExpression" ||
!startsWithNoLookaheadToken(
parentAwaitOrBlock.argument,
(leftmostNode) => leftmostNode === node
)
) {
return group(parts);
}
}
}
return parts;
case "ExportDefaultDeclaration":
case "ExportNamedDeclaration":
case "ExportAllDeclaration":
return printExportDeclaration(path, options, print);
case "ImportDeclaration":
return printImportDeclaration(path, options, print);
case "ImportSpecifier":
case "ExportSpecifier":
case "ImportNamespaceSpecifier":
case "ExportNamespaceSpecifier":
case "ImportDefaultSpecifier":
case "ExportDefaultSpecifier":
return printModuleSpecifier(path, options, print);
case "ImportAttribute":
return [print("key"), ": ", print("value")];
case "Import":
return "import";
case "BlockStatement":
case "StaticBlock":
return printBlock(path, options, print);
case "ClassBody":
return printClassBody(path, options, print);
case "ThrowStatement":
return printThrowStatement(path, options, print);
case "ReturnStatement":
return printReturnStatement(path, options, print);
case "NewExpression":
case "ImportExpression":
case "OptionalCallExpression":
case "CallExpression":
return printCallExpression(path, options, print);
case "ObjectExpression":
case "ObjectPattern":
case "RecordExpression":
return printObject(path, options, print);
// Babel 6
case "ObjectProperty": // Non-standard AST node type.
case "Property":
if (node.method || node.kind === "get" || node.kind === "set") {
return printMethod(path, options, print);
}
return printProperty(path, options, print);
case "ObjectMethod":
return printMethod(path, options, print);
case "Decorator":
return ["@", print("expression")];
case "ArrayExpression":
case "ArrayPattern":
case "TupleExpression":
return printArray(path, options, print);
case "SequenceExpression": {
const { parent } = path;
if (
parent.type === "ExpressionStatement" ||
parent.type === "ForStatement"
) {
// For ExpressionStatements and for-loop heads, which are among
// the few places a SequenceExpression appears unparenthesized, we want
// to indent expressions after the first.
const parts = [];
path.each(({ isFirst }) => {
if (isFirst) {
parts.push(print());
} else {
parts.push(",", indent([line, print()]));
}
}, "expressions");
return group(parts);
}
return group(join([",", line], path.map(print, "expressions")));
}
case "ThisExpression":
return "this";
case "Super":
return "super";
case "Directive":
return [print("value"), semi]; // Babel 6
case "UnaryExpression":
parts.push(node.operator);
if (/[a-z]$/.test(node.operator)) {
parts.push(" ");
}
if (hasComment(node.argument)) {
parts.push(
group(["(", indent([softline, print("argument")]), softline, ")"])
);
} else {
parts.push(print("argument"));
}
return parts;
case "UpdateExpression":
parts.push(print("argument"), node.operator);
if (node.prefix) {
parts.reverse();
}
return parts;
case "ConditionalExpression":
return printTernary(path, options, print);
case "VariableDeclaration": {
const printed = path.map(print, "declarations");
// We generally want to terminate all variable declarations with a
// semicolon, except when they in the () part of for loops.
const parentNode = path.parent;
const isParentForLoop =
parentNode.type === "ForStatement" ||
parentNode.type === "ForInStatement" ||
parentNode.type === "ForOfStatement";
const hasValue = node.declarations.some((decl) => decl.init);
let firstVariable;
if (printed.length === 1 && !hasComment(node.declarations[0])) {
firstVariable = printed[0];
} else if (printed.length > 0) {
// Indent first var to comply with eslint one-var rule
firstVariable = indent(printed[0]);
}
parts = [
printDeclareToken(path),
node.kind,
firstVariable ? [" ", firstVariable] : "",
indent(
printed
.slice(1)
.map((p) => [
",",
hasValue && !isParentForLoop ? hardline : line,
p,
])
),
];
if (!(isParentForLoop && parentNode.body !== node)) {
parts.push(semi);
}
return group(parts);
}
case "WithStatement":
return group([
"with (",
print("object"),
")",
adjustClause(node.body, print("body")),
]);
case "IfStatement": {
const con = adjustClause(node.consequent, print("consequent"));
const opening = group([
"if (",
group([indent([softline, print("test")]), softline]),
")",
con,
]);
parts.push(opening);
if (node.alternate) {
const commentOnOwnLine =
hasComment(
node.consequent,
CommentCheckFlags.Trailing | CommentCheckFlags.Line
) || needsHardlineAfterDanglingComment(node);
const elseOnSameLine =
node.consequent.type === "BlockStatement" && !commentOnOwnLine;
parts.push(elseOnSameLine ? " " : hardline);
if (hasComment(node, CommentCheckFlags.Dangling)) {
parts.push(
printDanglingComments(path, options),
commentOnOwnLine ? hardline : " "
);
}
parts.push(
"else",
group(
adjustClause(
node.alternate,
print("alternate"),
node.alternate.type === "IfStatement"
)
)
);
}
return parts;
}
case "ForStatement": {
const body = adjustClause(node.body, print("body"));
// We want to keep dangling comments above the loop to stay consistent.
// Any comment positioned between the for statement and the parentheses
// is going to be printed before the statement.
const dangling = printDanglingComments(path, options);
const printedComments = dangling ? [dangling, softline] : "";
if (!node.init && !node.test && !node.update) {
return [printedComments, group(["for (;;)", body])];
}
return [
printedComments,
group([
"for (",
group([
indent([
softline,
print("init"),
";",
line,
print("test"),
";",
line,
print("update"),
]),
softline,
]),
")",
body,
]),
];
}
case "WhileStatement":
return group([
"while (",
group([indent([softline, print("test")]), softline]),
")",
adjustClause(node.body, print("body")),
]);
case "ForInStatement":
return group([
"for (",
print("left"),
" in ",
print("right"),
")",
adjustClause(node.body, print("body")),
]);
case "ForOfStatement":
return group([
"for",
node.await ? " await" : "",
" (",
print("left"),
" of ",
print("right"),
")",
adjustClause(node.body, print("body")),
]);
case "DoWhileStatement": {
const clause = adjustClause(node.body, print("body"));
const doBody = group(["do", clause]);
parts = [doBody];
if (node.body.type === "BlockStatement") {
parts.push(" ");
} else {
parts.push(hardline);
}
parts.push(
"while (",
group([indent([softline, print("test")]), softline]),
")",
semi
);
return parts;
}
case "DoExpression":
return [node.async ? "async " : "", "do ", print("body")];
case "BreakStatement":
case "ContinueStatement":
parts.push(node.type === "BreakStatement" ? "break" : "continue");
if (node.label) {
parts.push(" ", print("label"));
}
parts.push(semi);
return parts;
case "LabeledStatement":
if (node.body.type === "EmptyStatement") {
return [print("label"), ":;"];
}
return [print("label"), ": ", print("body")];
case "TryStatement":
return [
"try ",
print("block"),
node.handler ? [" ", print("handler")] : "",
node.finalizer ? [" finally ", print("finalizer")] : "",
];
case "CatchClause":
if (node.param) {
const parameterHasComments = hasComment(
node.param,
(comment) =>
!isBlockComment(comment) ||
(comment.leading &&
hasNewline(options.originalText, locEnd(comment))) ||
(comment.trailing &&
hasNewline(options.originalText, locStart(comment), {
backwards: true,
}))
);
const param = print("param");
return [
"catch ",
parameterHasComments
? ["(", indent([softline, param]), softline, ") "]
: ["(", param, ") "],
print("body"),
];
}
return ["catch ", print("body")];
// Note: ignoring n.lexical because it has no printing consequences.
case "SwitchStatement":
return [
group([
"switch (",
indent([softline, print("discriminant")]),
softline,
")",
]),
" {",
node.cases.length > 0
? indent([
hardline,
join(
hardline,
path.map(
({ node, isLast }) => [
print(),
!isLast && isNextLineEmpty(node, options) ? hardline : "",
],
"cases"
)
),
])
: "",
hardline,
"}",
];
case "SwitchCase": {
if (node.test) {
parts.push("case ", print("test"), ":");
} else {
parts.push("default:");
}
if (hasComment(node, CommentCheckFlags.Dangling)) {
parts.push(" ", printDanglingComments(path, options));
}
const consequent = node.consequent.filter(
(node) => node.type !== "EmptyStatement"
);
if (consequent.length > 0) {
const cons = printStatementSequence(path, options, print, "consequent");
parts.push(
consequent.length === 1 && consequent[0].type === "BlockStatement"
? [" ", cons]
: indent([hardline, cons])
);
}
return parts;
}
// JSX extensions below.
case "DebuggerStatement":
return ["debugger", semi];
case "ClassDeclaration":
case "ClassExpression":
return printClass(path, options, print);
case "ClassMethod":
case "ClassPrivateMethod":
case "MethodDefinition":
return printClassMethod(path, options, print);
case "ClassProperty":
case "PropertyDefinition":
case "ClassPrivateProperty":
case "ClassAccessorProperty":
case "AccessorProperty":
return printClassProperty(path, options, print);
case "TemplateElement":
return replaceEndOfLine(node.value.raw);
case "TemplateLiteral":
return printTemplateLiteral(path, print, options);
case "TaggedTemplateExpression":
return printTaggedTemplateLiteral(print);
case "PrivateIdentifier":
return ["#", node.name];
case "PrivateName":
return ["#", print("id")];
// For hack-style pipeline
case "TopicReference":
return "%";
case "ArgumentPlaceholder":
return "?";
case "ModuleExpression": {
parts.push("module {");
const printed = print("body");
if (printed) {
parts.push(indent([hardline, printed]), hardline);
}
parts.push("}");
return parts;
}
case "InterpreterDirective": // Printed as comment
default:
/* c8 ignore next */
throw new UnexpectedNodeError(node, "ESTree");
}
}
export const experimentalFeatures = {
// TODO: Make this default behavior
avoidAstMutation: true,
};
export { genericPrint as print };
export * from "./comments/printer-methods.js";
export { default as embed } from "./embed/index.js";
export { default as massageAstNode } from "./clean.js";
export { insertPragma } from "./pragma.js";
export { default as getVisitorKeys } from "./traverse/get-visitor-keys.js";