"use strict"; const assert = require("assert"); // TODO(azz): anything that imports from main shouldn't be in a `language-*` dir. const comments = require("../main/comments"); const { getParentExportDeclaration, isExportDeclaration, shouldFlatten, getNextNonSpaceNonCommentCharacter, hasNewline, hasNewlineInRange, getLast, getStringWidth, printString, printNumber, hasIgnoreComment, skipWhitespace, hasNodeIgnoreComment, getPenultimate, startsWithNoLookaheadToken, getIndentSize, matchAncestorTypes, getPreferredQuote } = require("../common/util"); const { isNextLineEmpty, isNextLineEmptyAfterIndex, getNextNonSpaceNonCommentCharacterIndex } = require("../common/util-shared"); const isIdentifierName = require("esutils").keyword.isIdentifierNameES5; const embed = require("./embed"); const clean = require("./clean"); const insertPragma = require("./pragma").insertPragma; const handleComments = require("./comments"); const pathNeedsParens = require("./needs-parens"); const { printHtmlBinding, isVueEventBindingExpression } = require("./html-binding"); const preprocess = require("./preprocess"); const { hasNode, hasFlowAnnotationComment, hasFlowShorthandAnnotationComment } = require("./utils"); const { builders: { concat, join, line, hardline, softline, literalline, group, indent, align, conditionalGroup, fill, ifBreak, breakParent, lineSuffixBoundary, addAlignmentToDoc, dedent }, utils: { willBreak, isLineNext, isEmpty, removeLines }, printer: { printDocToString } } = require("../doc"); let uid = 0; function shouldPrintComma(options, level) { level = level || "es5"; switch (options.trailingComma) { case "all": if (level === "all") { return true; } // fallthrough case "es5": if (level === "es5") { return true; } // fallthrough case "none": default: return false; } } function genericPrint(path, options, printPath, args) { const node = path.getValue(); let needsParens = false; const linesWithoutParens = printPathNoParens(path, options, printPath, args); if (!node || isEmpty(linesWithoutParens)) { return linesWithoutParens; } const parentExportDecl = getParentExportDeclaration(path); const decorators = []; if ( node.type === "ClassMethod" || node.type === "ClassProperty" || node.type === "TSAbstractClassProperty" || node.type === "ClassPrivateProperty" || node.type === "MethodDefinition" || node.type === "TSAbstractMethodDefinition" ) { // their decorators are handled themselves } else if ( node.decorators && node.decorators.length > 0 && // If the parent node is an export declaration and the decorator // was written before the export, the export will be responsible // for printing the decorators. !( parentExportDecl && options.locStart(parentExportDecl, { ignoreDecorators: true }) > options.locStart(node.decorators[0]) ) ) { const shouldBreak = node.type === "ClassExpression" || node.type === "ClassDeclaration" || hasNewlineBetweenOrAfterDecorators(node, options); const separator = shouldBreak ? hardline : line; path.each(decoratorPath => { let decorator = decoratorPath.getValue(); if (decorator.expression) { decorator = decorator.expression; } else { decorator = decorator.callee; } decorators.push(printPath(decoratorPath), separator); }, "decorators"); if (parentExportDecl) { decorators.unshift(hardline); } } else if ( isExportDeclaration(node) && node.declaration && node.declaration.decorators && node.declaration.decorators.length > 0 && // Only print decorators here if they were written before the export, // otherwise they are printed by the node.declaration options.locStart(node, { ignoreDecorators: true }) > options.locStart(node.declaration.decorators[0]) ) { // Export declarations are responsible for printing any decorators // that logically apply to node.declaration. path.each( decoratorPath => { const decorator = decoratorPath.getValue(); const prefix = decorator.type === "Decorator" ? "" : "@"; decorators.push(prefix, printPath(decoratorPath), hardline); }, "declaration", "decorators" ); } else { // Nodes with decorators can't have parentheses, so we can avoid // computing pathNeedsParens() except in this case. needsParens = pathNeedsParens(path, options); } const parts = []; if (needsParens) { parts.unshift("("); } parts.push(linesWithoutParens); if (needsParens) { const node = path.getValue(); if (hasFlowShorthandAnnotationComment(node)) { parts.push(" /*"); parts.push(node.trailingComments[0].value.trimLeft()); parts.push("*/"); node.trailingComments[0].printed = true; } parts.push(")"); } if (decorators.length > 0) { return group(concat(decorators.concat(parts))); } return concat(parts); } function hasNewlineBetweenOrAfterDecorators(node, options) { return ( hasNewlineInRange( options.originalText, options.locStart(node.decorators[0]), options.locEnd(getLast(node.decorators)) ) || hasNewline(options.originalText, options.locEnd(getLast(node.decorators))) ); } function printDecorators(path, options, print) { const node = path.getValue(); return group( concat([ join(line, path.map(print, "decorators")), hasNewlineBetweenOrAfterDecorators(node, options) ? hardline : line ]) ); } function hasPrettierIgnore(path) { return hasIgnoreComment(path) || hasJsxIgnoreComment(path); } function hasJsxIgnoreComment(path) { const node = path.getValue(); const parent = path.getParentNode(); if (!parent || !node || !isJSXNode(node) || !isJSXNode(parent)) { return false; } // Lookup the previous sibling, ignoring any empty JSXText elements const index = parent.children.indexOf(node); let prevSibling = null; for (let i = index; i > 0; i--) { const candidate = parent.children[i - 1]; if (candidate.type === "JSXText" && !isMeaningfulJSXText(candidate)) { continue; } prevSibling = candidate; break; } return ( prevSibling && prevSibling.type === "JSXExpressionContainer" && prevSibling.expression.type === "JSXEmptyExpression" && prevSibling.expression.comments && prevSibling.expression.comments.find( comment => comment.value.trim() === "prettier-ignore" ) ); } /** * The following is the shared logic for * ternary operators, namely ConditionalExpression * and TSConditionalType * @typedef {Object} OperatorOptions * @property {() => Array} beforeParts - Parts to print before the `?`. * @property {(breakClosingParen: boolean) => Array} afterParts - Parts to print after the conditional expression. * @property {boolean} shouldCheckJsx - Whether to check for and print in JSX mode. * @property {string} conditionalNodeType - The type of the conditional expression node, ie "ConditionalExpression" or "TSConditionalType". * @property {string} consequentNodePropertyName - The property at which the consequent node can be found on the main node, eg "consequent". * @property {string} alternateNodePropertyName - The property at which the alternate node can be found on the main node, eg "alternate". * @property {string} testNodePropertyName - The property at which the test node can be found on the main node, eg "test". * @property {boolean} breakNested - Whether to break all nested ternaries when one breaks. * @param {FastPath} path - The path to the ConditionalExpression/TSConditionalType node. * @param {Options} options - Prettier options * @param {Function} print - Print function to call recursively * @param {OperatorOptions} operatorOptions * @returns Doc */ function printTernaryOperator(path, options, print, operatorOptions) { const node = path.getValue(); const testNode = node[operatorOptions.testNodePropertyName]; const consequentNode = node[operatorOptions.consequentNodePropertyName]; const alternateNode = node[operatorOptions.alternateNodePropertyName]; const parts = []; // We print a ConditionalExpression in either "JSX mode" or "normal mode". // See tests/jsx/conditional-expression.js for more info. let jsxMode = false; const parent = path.getParentNode(); let forceNoIndent = parent.type === operatorOptions.conditionalNodeType; // Find the outermost non-ConditionalExpression parent, and the outermost // ConditionalExpression parent. We'll use these to determine if we should // print in JSX mode. let currentParent; let previousParent; let i = 0; do { previousParent = currentParent || node; currentParent = path.getParentNode(i); i++; } while ( currentParent && currentParent.type === operatorOptions.conditionalNodeType ); const firstNonConditionalParent = currentParent || parent; const lastConditionalParent = previousParent; if ( operatorOptions.shouldCheckJsx && (isJSXNode(testNode) || isJSXNode(consequentNode) || isJSXNode(alternateNode) || conditionalExpressionChainContainsJSX(lastConditionalParent)) ) { jsxMode = true; forceNoIndent = true; // Even though they don't need parens, we wrap (almost) everything in // parens when using ?: within JSX, because the parens are analogous to // curly braces in an if statement. const wrap = doc => concat([ ifBreak("(", ""), indent(concat([softline, doc])), softline, ifBreak(")", "") ]); // The only things we don't wrap are: // * Nested conditional expressions in alternates // * null const isNull = node => node.type === "NullLiteral" || (node.type === "Literal" && node.value === null); parts.push( " ? ", isNull(consequentNode) ? path.call(print, operatorOptions.consequentNodePropertyName) : wrap(path.call(print, operatorOptions.consequentNodePropertyName)), " : ", alternateNode.type === operatorOptions.conditionalNodeType || isNull(alternateNode) ? path.call(print, operatorOptions.alternateNodePropertyName) : wrap(path.call(print, operatorOptions.alternateNodePropertyName)) ); } else { // normal mode const part = concat([ line, "? ", consequentNode.type === operatorOptions.conditionalNodeType ? ifBreak("", "(") : "", align(2, path.call(print, operatorOptions.consequentNodePropertyName)), consequentNode.type === operatorOptions.conditionalNodeType ? ifBreak("", ")") : "", line, ": ", alternateNode.type === operatorOptions.conditionalNodeType ? path.call(print, operatorOptions.alternateNodePropertyName) : align(2, path.call(print, operatorOptions.alternateNodePropertyName)) ]); parts.push( parent.type !== operatorOptions.conditionalNodeType || parent[operatorOptions.alternateNodePropertyName] === node ? part : options.useTabs ? dedent(indent(part)) : align(Math.max(0, options.tabWidth - 2), part) ); } // We want a whole chain of ConditionalExpressions to all // break if any of them break. That means we should only group around the // outer-most ConditionalExpression. const maybeGroup = doc => operatorOptions.breakNested ? parent === firstNonConditionalParent ? group(doc) : doc : group(doc); // Break the closing paren to keep the chain right after it: // (a // ? b // : c // ).call() const breakClosingParen = !jsxMode && (parent.type === "MemberExpression" || parent.type === "OptionalMemberExpression") && !parent.computed; return maybeGroup( concat( [].concat( (testDoc => /** * a * ? b * : multiline * test * node * ^^ align(2) * ? d * : e */ parent.type === operatorOptions.conditionalNodeType && parent[operatorOptions.alternateNodePropertyName] === node ? align(2, testDoc) : testDoc)(concat(operatorOptions.beforeParts())), forceNoIndent ? concat(parts) : indent(concat(parts)), operatorOptions.afterParts(breakClosingParen) ) ) ); } function getTypeScriptMappedTypeModifier(tokenNode, keyword) { if (tokenNode.type === "TSPlusToken") { return "+" + keyword; } else if (tokenNode.type === "TSMinusToken") { return "-" + keyword; } return keyword; } function printPathNoParens(path, options, print, args) { const n = path.getValue(); const semi = options.semi ? ";" : ""; if (!n) { return ""; } if (typeof n === "string") { return n; } const htmlBinding = printHtmlBinding(path, options, print); if (htmlBinding) { return htmlBinding; } let parts = []; switch (n.type) { case "JsExpressionRoot": return path.call(print, "node"); case "JsonRoot": return concat([path.call(print, "node"), hardline]); case "File": // Print @babel/parser's InterpreterDirective here so that // leading comments on the `Program` node get printed after the hashbang. if (n.program && n.program.interpreter) { parts.push( path.call( programPath => programPath.call(print, "interpreter"), "program" ) ); } parts.push(path.call(print, "program")); return concat(parts); case "Program": // Babel 6 if (n.directives) { path.each(childPath => { parts.push(print(childPath), semi, hardline); if ( isNextLineEmpty(options.originalText, childPath.getValue(), options) ) { parts.push(hardline); } }, "directives"); } parts.push( path.call(bodyPath => { return printStatementSequence(bodyPath, options, print); }, "body") ); parts.push( comments.printDanglingComments(path, options, /* sameIndent */ true) ); // Only force a trailing newline if there were any contents. if (n.body.length || n.comments) { parts.push(hardline); } return concat(parts); // Babel extension. case "EmptyStatement": return ""; case "ExpressionStatement": // Detect Flow-parsed directives if (n.directive) { return concat([nodeStr(n.expression, options, true), semi]); } if (options.parser === "__vue_event_binding") { const parent = path.getParentNode(); if ( parent.type === "Program" && parent.body.length === 1 && parent.body[0] === n ) { return concat([ path.call(print, "expression"), isVueEventBindingExpression(n.expression) ? ";" : "" ]); } } // Do not append semicolon after the only JSX element in a program return concat([ path.call(print, "expression"), isTheOnlyJSXElementInMarkdown(options, path) ? "" : semi ]); // Babel extension. case "ParenthesizedExpression": return concat(["(", path.call(print, "expression"), ")"]); case "AssignmentExpression": return printAssignment( n.left, path.call(print, "left"), concat([" ", n.operator]), n.right, path.call(print, "right"), options ); case "BinaryExpression": case "LogicalExpression": case "NGPipeExpression": { const parent = path.getParentNode(); const parentParent = path.getParentNode(1); const isInsideParenthesis = n !== parent.body && (parent.type === "IfStatement" || parent.type === "WhileStatement" || parent.type === "DoWhileStatement"); const parts = printBinaryishExpressions( path, print, options, /* isNested */ false, isInsideParenthesis ); // if ( // this.hasPlugin("dynamicImports") && this.lookahead().type === tt.parenLeft // ) { // // looks super weird, we want to break the children if the parent breaks // // if ( // this.hasPlugin("dynamicImports") && // this.lookahead().type === tt.parenLeft // ) { if (isInsideParenthesis) { return concat(parts); } // Break between the parens in unaries or in a member expression, i.e. // // ( // a && // b && // c // ).call() if ( parent.type === "UnaryExpression" || ((parent.type === "MemberExpression" || parent.type === "OptionalMemberExpression") && !parent.computed) ) { return group( concat([indent(concat([softline, concat(parts)])), softline]) ); } // Avoid indenting sub-expressions in some cases where the first sub-expression is already // indented accordingly. We should indent sub-expressions where the first case isn't indented. const shouldNotIndent = parent.type === "ReturnStatement" || (parent.type === "JSXExpressionContainer" && parentParent.type === "JSXAttribute") || (n.type !== "NGPipeExpression" && ((parent.type === "NGRoot" && options.parser === "__ng_binding") || (parent.type === "NGMicrosyntaxExpression" && parentParent.type === "NGMicrosyntax" && parentParent.body.length === 1))) || (n === parent.body && parent.type === "ArrowFunctionExpression") || (n !== parent.body && parent.type === "ForStatement") || (parent.type === "ConditionalExpression" && (parentParent.type !== "ReturnStatement" && parentParent.type !== "CallExpression")); const shouldIndentIfInlining = parent.type === "AssignmentExpression" || parent.type === "VariableDeclarator" || parent.type === "ClassProperty" || parent.type === "TSAbstractClassProperty" || parent.type === "ClassPrivateProperty" || parent.type === "ObjectProperty" || parent.type === "Property"; const samePrecedenceSubExpression = isBinaryish(n.left) && shouldFlatten(n.operator, n.left.operator); if ( shouldNotIndent || (shouldInlineLogicalExpression(n) && !samePrecedenceSubExpression) || (!shouldInlineLogicalExpression(n) && shouldIndentIfInlining) ) { return group(concat(parts)); } if (parts.length === 0) { return ""; } // If the right part is a JSX node, we include it in a separate group to // prevent it breaking the whole chain, so we can print the expression like: // // foo && bar && ( // // // // ) const hasJSX = isJSXNode(n.right); const rest = concat(hasJSX ? parts.slice(1, -1) : parts.slice(1)); const groupId = Symbol("logicalChain-" + ++uid); const chain = group( concat([ // Don't include the initial expression in the indentation // level. The first item is guaranteed to be the first // left-most expression. parts.length > 0 ? parts[0] : "", indent(rest) ]), { id: groupId } ); if (!hasJSX) { return chain; } const jsxPart = getLast(parts); return group( concat([chain, ifBreak(indent(jsxPart), jsxPart, { groupId })]) ); } case "AssignmentPattern": return concat([ path.call(print, "left"), " = ", path.call(print, "right") ]); case "TSTypeAssertionExpression": { const shouldBreakAfterCast = !( n.expression.type === "ArrayExpression" || n.expression.type === "ObjectExpression" ); const castGroup = group( concat([ "<", indent(concat([softline, path.call(print, "typeAnnotation")])), softline, ">" ]) ); const exprContents = concat([ ifBreak("("), indent(concat([softline, path.call(print, "expression")])), softline, ifBreak(")") ]); if (shouldBreakAfterCast) { return conditionalGroup([ concat([castGroup, path.call(print, "expression")]), concat([castGroup, group(exprContents, { shouldBreak: true })]), concat([castGroup, path.call(print, "expression")]) ]); } return group(concat([castGroup, path.call(print, "expression")])); } case "OptionalMemberExpression": case "MemberExpression": { const parent = path.getParentNode(); let firstNonMemberParent; let i = 0; do { firstNonMemberParent = path.getParentNode(i); i++; } while ( firstNonMemberParent && (firstNonMemberParent.type === "MemberExpression" || firstNonMemberParent.type === "OptionalMemberExpression" || firstNonMemberParent.type === "TSNonNullExpression") ); const shouldInline = (firstNonMemberParent && (firstNonMemberParent.type === "NewExpression" || firstNonMemberParent.type === "BindExpression" || (firstNonMemberParent.type === "VariableDeclarator" && firstNonMemberParent.id.type !== "Identifier") || (firstNonMemberParent.type === "AssignmentExpression" && firstNonMemberParent.left.type !== "Identifier"))) || n.computed || (n.object.type === "Identifier" && n.property.type === "Identifier" && parent.type !== "MemberExpression" && parent.type !== "OptionalMemberExpression"); return concat([ path.call(print, "object"), shouldInline ? printMemberLookup(path, options, print) : group( indent( concat([softline, printMemberLookup(path, options, print)]) ) ) ]); } case "MetaProperty": return concat([ path.call(print, "meta"), ".", path.call(print, "property") ]); case "BindExpression": if (n.object) { parts.push(path.call(print, "object")); } parts.push( group( indent( concat([softline, printBindExpressionCallee(path, options, print)]) ) ) ); return concat(parts); case "Identifier": { return concat([ n.name, printOptionalToken(path), printTypeAnnotation(path, options, print) ]); } case "SpreadElement": case "SpreadElementPattern": case "RestProperty": case "SpreadProperty": case "SpreadPropertyPattern": case "RestElement": case "ObjectTypeSpreadProperty": return concat([ "...", path.call(print, "argument"), printTypeAnnotation(path, options, print) ]); case "FunctionDeclaration": case "FunctionExpression": if (isNodeStartingWithDeclare(n, options)) { parts.push("declare "); } parts.push(printFunctionDeclaration(path, print, options)); if (!n.body) { parts.push(semi); } return concat(parts); case "ArrowFunctionExpression": { if (n.async) { parts.push("async "); } if (shouldPrintParamsWithoutParens(path, options)) { parts.push(path.call(print, "params", 0)); } else { parts.push( group( concat([ printFunctionParams( path, print, options, /* expandLast */ args && (args.expandLastArg || args.expandFirstArg), /* printTypeParams */ true ), printReturnType(path, print, options) ]) ) ); } const dangling = comments.printDanglingComments( path, options, /* sameIndent */ true, comment => { const nextCharacter = getNextNonSpaceNonCommentCharacterIndex( options.originalText, comment, options ); return options.originalText.substr(nextCharacter, 2) === "=>"; } ); if (dangling) { parts.push(" ", dangling); } parts.push(" =>"); const body = path.call(bodyPath => print(bodyPath, args), "body"); // We want to always keep these types of nodes on the same line // as the arrow. if ( !hasLeadingOwnLineComment(options.originalText, n.body, options) && (n.body.type === "ArrayExpression" || n.body.type === "ObjectExpression" || n.body.type === "BlockStatement" || isJSXNode(n.body) || isTemplateOnItsOwnLine(n.body, options.originalText, options) || n.body.type === "ArrowFunctionExpression" || n.body.type === "DoExpression") ) { return group(concat([concat(parts), " ", body])); } // We handle sequence expressions as the body of arrows specially, // so that the required parentheses end up on their own lines. if (n.body.type === "SequenceExpression") { return group( concat([ concat(parts), group( concat([" (", indent(concat([softline, body])), softline, ")"]) ) ]) ); } // if the arrow function is expanded as last argument, we are adding a // level of indentation and need to add a softline to align the closing ) // with the opening (, or if it's inside a JSXExpression (e.g. an attribute) // we should align the expression's closing } with the line with the opening {. const shouldAddSoftLine = ((args && args.expandLastArg) || path.getParentNode().type === "JSXExpressionContainer") && !(n.comments && n.comments.length); const printTrailingComma = args && args.expandLastArg && shouldPrintComma(options, "all"); // In order to avoid confusion between // a => a ? a : a // a <= a ? a : a const shouldAddParens = n.body.type === "ConditionalExpression" && !startsWithNoLookaheadToken(n.body, /* forbidFunctionAndClass */ false); return group( concat([ concat(parts), group( concat([ indent( concat([ line, shouldAddParens ? ifBreak("", "(") : "", body, shouldAddParens ? ifBreak("", ")") : "" ]) ), shouldAddSoftLine ? concat([ifBreak(printTrailingComma ? "," : ""), softline]) : "" ]) ) ]) ); } case "MethodDefinition": case "TSAbstractMethodDefinition": if (n.decorators && n.decorators.length !== 0) { parts.push(printDecorators(path, options, print)); } if (n.accessibility) { parts.push(n.accessibility + " "); } if (n.static) { parts.push("static "); } if (n.type === "TSAbstractMethodDefinition") { parts.push("abstract "); } parts.push(printMethod(path, options, print)); return concat(parts); case "YieldExpression": parts.push("yield"); if (n.delegate) { parts.push("*"); } if (n.argument) { parts.push(" ", path.call(print, "argument")); } return concat(parts); case "AwaitExpression": return concat(["await ", path.call(print, "argument")]); case "ImportSpecifier": if (n.importKind) { parts.push(path.call(print, "importKind"), " "); } parts.push(path.call(print, "imported")); if (n.local && n.local.name !== n.imported.name) { parts.push(" as ", path.call(print, "local")); } return concat(parts); case "ExportSpecifier": parts.push(path.call(print, "local")); if (n.exported && n.exported.name !== n.local.name) { parts.push(" as ", path.call(print, "exported")); } return concat(parts); case "ImportNamespaceSpecifier": parts.push("* as "); if (n.local) { parts.push(path.call(print, "local")); } else if (n.id) { parts.push(path.call(print, "id")); } return concat(parts); case "ImportDefaultSpecifier": if (n.local) { return path.call(print, "local"); } return path.call(print, "id"); case "TSExportAssignment": return concat(["export = ", path.call(print, "expression"), semi]); case "ExportDefaultDeclaration": case "ExportNamedDeclaration": return printExportDeclaration(path, options, print); case "ExportAllDeclaration": parts.push("export "); if (n.exportKind === "type") { parts.push("type "); } parts.push("* from ", path.call(print, "source"), semi); return concat(parts); case "ExportNamespaceSpecifier": case "ExportDefaultSpecifier": return path.call(print, "exported"); case "ImportDeclaration": { parts.push("import "); if (n.importKind && n.importKind !== "value") { parts.push(n.importKind + " "); } const standalones = []; const grouped = []; if (n.specifiers && n.specifiers.length > 0) { path.each(specifierPath => { const value = specifierPath.getValue(); if ( value.type === "ImportDefaultSpecifier" || value.type === "ImportNamespaceSpecifier" ) { standalones.push(print(specifierPath)); } else { grouped.push(print(specifierPath)); } }, "specifiers"); if (standalones.length > 0) { parts.push(join(", ", standalones)); } if (standalones.length > 0 && grouped.length > 0) { parts.push(", "); } if ( grouped.length === 1 && standalones.length === 0 && n.specifiers && !n.specifiers.some(node => node.comments) ) { parts.push( concat([ "{", options.bracketSpacing ? " " : "", concat(grouped), options.bracketSpacing ? " " : "", "}" ]) ); } else if (grouped.length >= 1) { parts.push( group( concat([ "{", indent( concat([ options.bracketSpacing ? line : softline, join(concat([",", line]), grouped) ]) ), ifBreak(shouldPrintComma(options) ? "," : ""), options.bracketSpacing ? line : softline, "}" ]) ) ); } parts.push(" from "); } else if ( (n.importKind && n.importKind === "type") || // import {} from 'x' /{\s*}/.test( options.originalText.slice( options.locStart(n), options.locStart(n.source) ) ) ) { parts.push("{} from "); } parts.push(path.call(print, "source"), semi); return concat(parts); } case "Import": return "import"; case "TSModuleBlock": case "BlockStatement": { const naked = path.call(bodyPath => { return printStatementSequence(bodyPath, options, print); }, "body"); const hasContent = n.body.find(node => node.type !== "EmptyStatement"); const hasDirectives = n.directives && n.directives.length > 0; const parent = path.getParentNode(); const parentParent = path.getParentNode(1); if ( !hasContent && !hasDirectives && !hasDanglingComments(n) && (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression" || parent.type === "FunctionDeclaration" || parent.type === "ObjectMethod" || parent.type === "ClassMethod" || parent.type === "ForStatement" || parent.type === "WhileStatement" || parent.type === "DoWhileStatement" || parent.type === "DoExpression" || (parent.type === "CatchClause" && !parentParent.finalizer) || parent.type === "TSModuleDeclaration") ) { return "{}"; } parts.push("{"); // Babel 6 if (hasDirectives) { path.each(childPath => { parts.push(indent(concat([hardline, print(childPath), semi]))); if ( isNextLineEmpty(options.originalText, childPath.getValue(), options) ) { parts.push(hardline); } }, "directives"); } if (hasContent) { parts.push(indent(concat([hardline, naked]))); } parts.push(comments.printDanglingComments(path, options)); parts.push(hardline, "}"); return concat(parts); } case "ReturnStatement": parts.push("return"); if (n.argument) { if (returnArgumentHasLeadingComment(options, n.argument)) { parts.push( concat([ " (", indent(concat([hardline, path.call(print, "argument")])), hardline, ")" ]) ); } else if ( n.argument.type === "LogicalExpression" || n.argument.type === "BinaryExpression" || n.argument.type === "SequenceExpression" ) { parts.push( group( concat([ ifBreak(" (", " "), indent(concat([softline, path.call(print, "argument")])), softline, ifBreak(")") ]) ) ); } else { parts.push(" ", path.call(print, "argument")); } } if (hasDanglingComments(n)) { parts.push( " ", comments.printDanglingComments(path, options, /* sameIndent */ true) ); } parts.push(semi); return concat(parts); case "NewExpression": case "OptionalCallExpression": case "CallExpression": { const isNew = n.type === "NewExpression"; const optional = printOptionalToken(path); if ( // We want to keep CommonJS- and AMD-style require calls, and AMD-style // define calls, as a unit. // e.g. `define(["some/lib", (lib) => {` (!isNew && n.callee.type === "Identifier" && (n.callee.name === "require" || n.callee.name === "define")) || n.callee.type === "Import" || // Template literals as single arguments (n.arguments.length === 1 && isTemplateOnItsOwnLine( n.arguments[0], options.originalText, options )) || // Keep test declarations on a single line // e.g. `it('long name', () => {` (!isNew && isTestCall(n, path.getParentNode())) ) { return concat([ isNew ? "new " : "", path.call(print, "callee"), optional, printFunctionTypeParameters(path, options, print), concat(["(", join(", ", path.map(print, "arguments")), ")"]) ]); } // Inline Flow annotation comments following Identifiers in Call nodes need to // stay with the Identifier. For example: // // foo /*:: */(bar); // // Here, we ensure that such comments stay between the Identifier and the Callee. const isIdentifierWithFlowAnnotation = n.callee.type === "Identifier" && hasFlowAnnotationComment(n.callee.trailingComments); if (isIdentifierWithFlowAnnotation) { n.callee.trailingComments[0].printed = true; } // We detect calls on member lookups and possibly print them in a // special chain format. See `printMemberChain` for more info. if (!isNew && isMemberish(n.callee)) { return printMemberChain(path, options, print); } return concat([ isNew ? "new " : "", path.call(print, "callee"), optional, isIdentifierWithFlowAnnotation ? `/*:: ${n.callee.trailingComments[0].value.substring(2).trim()} */` : "", printFunctionTypeParameters(path, options, print), printArgumentsList(path, options, print) ]); } case "TSInterfaceDeclaration": if (isNodeStartingWithDeclare(n, options)) { parts.push("declare "); } parts.push( n.abstract ? "abstract " : "", printTypeScriptModifiers(path, options, print), "interface ", path.call(print, "id"), n.typeParameters ? path.call(print, "typeParameters") : "", " " ); if (n.heritage.length) { parts.push( group( indent( concat([ softline, "extends ", (n.heritage.length === 1 ? identity : indent)( join(concat([",", line]), path.map(print, "heritage")) ), " " ]) ) ) ); } parts.push(path.call(print, "body")); return concat(parts); case "ObjectTypeInternalSlot": return concat([ n.static ? "static " : "", "[[", path.call(print, "id"), "]]", printOptionalToken(path), n.method ? "" : ": ", path.call(print, "value") ]); case "ObjectExpression": case "ObjectPattern": case "ObjectTypeAnnotation": case "TSInterfaceBody": case "TSTypeLiteral": { let propertiesField; if (n.type === "TSTypeLiteral") { propertiesField = "members"; } else if (n.type === "TSInterfaceBody") { propertiesField = "body"; } else { propertiesField = "properties"; } const isTypeAnnotation = n.type === "ObjectTypeAnnotation"; const fields = []; if (isTypeAnnotation) { fields.push("indexers", "callProperties", "internalSlots"); } fields.push(propertiesField); const firstProperty = fields .map(field => n[field][0]) .sort((a, b) => options.locStart(a) - options.locStart(b))[0]; const parent = path.getParentNode(0); const isFlowInterfaceLikeBody = isTypeAnnotation && parent && (parent.type === "InterfaceDeclaration" || parent.type === "DeclareInterface" || parent.type === "DeclareClass") && path.getName() === "body"; const shouldBreak = n.type === "TSInterfaceBody" || isFlowInterfaceLikeBody || (n.type === "ObjectPattern" && parent.type !== "FunctionDeclaration" && parent.type !== "FunctionExpression" && parent.type !== "ArrowFunctionExpression" && parent.type !== "AssignmentPattern" && parent.type !== "CatchClause" && n.properties.some( property => property.value && (property.value.type === "ObjectPattern" || property.value.type === "ArrayPattern") )) || (n.type !== "ObjectPattern" && firstProperty && hasNewlineInRange( options.originalText, options.locStart(n), options.locStart(firstProperty) )); const separator = isFlowInterfaceLikeBody ? ";" : n.type === "TSInterfaceBody" || n.type === "TSTypeLiteral" ? ifBreak(semi, ";") : ","; const leftBrace = n.exact ? "{|" : "{"; const rightBrace = n.exact ? "|}" : "}"; // Unfortunately, things are grouped together in the ast can be // interleaved in the source code. So we need to reorder them before // printing them. const propsAndLoc = []; fields.forEach(field => { path.each(childPath => { const node = childPath.getValue(); propsAndLoc.push({ node: node, printed: print(childPath), loc: options.locStart(node) }); }, field); }); let separatorParts = []; const props = propsAndLoc .sort((a, b) => a.loc - b.loc) .map(prop => { const result = concat(separatorParts.concat(group(prop.printed))); separatorParts = [separator, line]; if ( (prop.node.type === "TSPropertySignature" || prop.node.type === "TSMethodSignature" || prop.node.type === "TSConstructSignature") && hasNodeIgnoreComment(prop.node) ) { separatorParts.shift(); } if (isNextLineEmpty(options.originalText, prop.node, options)) { separatorParts.push(hardline); } return result; }); if (n.inexact) { props.push(concat(separatorParts.concat(group("...")))); } const lastElem = getLast(n[propertiesField]); const canHaveTrailingSeparator = !( lastElem && (lastElem.type === "RestProperty" || lastElem.type === "RestElement" || hasNodeIgnoreComment(lastElem) || n.inexact) ); let content; if (props.length === 0 && !n.typeAnnotation) { if (!hasDanglingComments(n)) { return concat([leftBrace, rightBrace]); } content = group( concat([ leftBrace, comments.printDanglingComments(path, options), softline, rightBrace, printOptionalToken(path) ]) ); } else { content = concat([ leftBrace, indent( concat([options.bracketSpacing ? line : softline, concat(props)]) ), ifBreak( canHaveTrailingSeparator && (separator !== "," || shouldPrintComma(options)) ? separator : "" ), concat([options.bracketSpacing ? line : softline, rightBrace]), printOptionalToken(path), printTypeAnnotation(path, options, print) ]); } // If we inline the object as first argument of the parent, we don't want // to create another group so that the object breaks before the return // type const parentParentParent = path.getParentNode(2); if ( (n.type === "ObjectPattern" && parent && shouldHugArguments(parent) && parent.params[0] === n) || (shouldHugType(n) && parentParentParent && shouldHugArguments(parentParentParent) && parentParentParent.params[0].typeAnnotation && parentParentParent.params[0].typeAnnotation.typeAnnotation === n) ) { return content; } return group(content, { shouldBreak }); } // Babel 6 case "ObjectProperty": // Non-standard AST node type. case "Property": if (n.method || n.kind === "get" || n.kind === "set") { return printMethod(path, options, print); } if (n.shorthand) { parts.push(path.call(print, "value")); } else { let printedLeft; if (n.computed) { printedLeft = concat(["[", path.call(print, "key"), "]"]); } else { printedLeft = printPropertyKey(path, options, print); } parts.push( printAssignment( n.key, printedLeft, ":", n.value, path.call(print, "value"), options ) ); } return concat(parts); // Babel 6 case "ClassMethod": if (n.decorators && n.decorators.length !== 0) { parts.push(printDecorators(path, options, print)); } if (n.static) { parts.push("static "); } parts = parts.concat(printObjectMethod(path, options, print)); return concat(parts); // Babel 6 case "ObjectMethod": return printObjectMethod(path, options, print); case "Decorator": return concat([ "@", path.call(print, "expression"), path.call(print, "callee") ]); case "ArrayExpression": case "ArrayPattern": if (n.elements.length === 0) { if (!hasDanglingComments(n)) { parts.push("[]"); } else { parts.push( group( concat([ "[", comments.printDanglingComments(path, options), softline, "]" ]) ) ); } } else { const lastElem = getLast(n.elements); const canHaveTrailingComma = !( lastElem && lastElem.type === "RestElement" ); // JavaScript allows you to have empty elements in an array which // changes its length based on the number of commas. The algorithm // is that if the last argument is null, we need to force insert // a comma to ensure JavaScript recognizes it. // [,].length === 1 // [1,].length === 1 // [1,,].length === 2 // // Note that getLast returns null if the array is empty, but // we already check for an empty array just above so we are safe const needsForcedTrailingComma = canHaveTrailingComma && lastElem === null; parts.push( group( concat([ "[", indent( concat([ softline, printArrayItems(path, options, "elements", print) ]) ), needsForcedTrailingComma ? "," : "", ifBreak( canHaveTrailingComma && !needsForcedTrailingComma && shouldPrintComma(options) ? "," : "" ), comments.printDanglingComments( path, options, /* sameIndent */ true ), softline, "]" ]) ) ); } parts.push( printOptionalToken(path), printTypeAnnotation(path, options, print) ); return concat(parts); case "SequenceExpression": { const parent = path.getParentNode(0); 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(p => { if (p.getName() === 0) { parts.push(print(p)); } else { parts.push(",", indent(concat([line, print(p)]))); } }, "expressions"); return group(concat(parts)); } return group( concat([join(concat([",", line]), path.map(print, "expressions"))]) ); } case "ThisExpression": return "this"; case "Super": return "super"; case "NullLiteral": // Babel 6 Literal split return "null"; case "RegExpLiteral": // Babel 6 Literal split return printRegex(n); case "NumericLiteral": // Babel 6 Literal split return printNumber(n.extra.raw); case "BigIntLiteral": return concat([ printNumber( n.extra ? n.extra.rawValue : // TypeScript n.value ), "n" ]); case "BooleanLiteral": // Babel 6 Literal split case "StringLiteral": // Babel 6 Literal split case "Literal": { if (n.regex) { return printRegex(n.regex); } if (typeof n.value === "number") { return printNumber(n.raw); } if (typeof n.value !== "string") { return "" + n.value; } // TypeScript workaround for https://github.com/JamesHenry/typescript-estree/issues/2 // See corresponding workaround in needs-parens.js const grandParent = path.getParentNode(1); const isTypeScriptDirective = options.parser === "typescript" && typeof n.value === "string" && grandParent && (grandParent.type === "Program" || grandParent.type === "BlockStatement"); return nodeStr(n, options, isTypeScriptDirective); } case "Directive": return path.call(print, "value"); // Babel 6 case "DirectiveLiteral": return nodeStr(n, options); case "UnaryExpression": parts.push(n.operator); if (/[a-z]$/.test(n.operator)) { parts.push(" "); } parts.push(path.call(print, "argument")); return concat(parts); case "UpdateExpression": parts.push(path.call(print, "argument"), n.operator); if (n.prefix) { parts.reverse(); } return concat(parts); case "ConditionalExpression": return printTernaryOperator(path, options, print, { beforeParts: () => [path.call(print, "test")], afterParts: breakClosingParen => [breakClosingParen ? softline : ""], shouldCheckJsx: true, conditionalNodeType: "ConditionalExpression", consequentNodePropertyName: "consequent", alternateNodePropertyName: "alternate", testNodePropertyName: "test", breakNested: true }); case "VariableDeclaration": { const printed = path.map(childPath => { return print(childPath); }, "declarations"); // We generally want to terminate all variable declarations with a // semicolon, except when they in the () part of for loops. const parentNode = path.getParentNode(); const isParentForLoop = parentNode.type === "ForStatement" || parentNode.type === "ForInStatement" || parentNode.type === "ForOfStatement" || parentNode.type === "ForAwaitStatement"; const hasValue = n.declarations.some(decl => decl.init); let firstVariable; if (printed.length === 1 && !n.declarations[0].comments) { firstVariable = printed[0]; } else if (printed.length > 0) { // Indent first var to comply with eslint one-var rule firstVariable = indent(printed[0]); } parts = [ isNodeStartingWithDeclare(n, options) ? "declare " : "", n.kind, firstVariable ? concat([" ", firstVariable]) : "", indent( concat( printed .slice(1) .map(p => concat([",", hasValue && !isParentForLoop ? hardline : line, p]) ) ) ) ]; if (!(isParentForLoop && parentNode.body !== n)) { parts.push(semi); } return group(concat(parts)); } case "VariableDeclarator": return printAssignment( n.id, concat([path.call(print, "id"), path.call(print, "typeParameters")]), " =", n.init, n.init && path.call(print, "init"), options ); case "WithStatement": return group( concat([ "with (", path.call(print, "object"), ")", adjustClause(n.body, path.call(print, "body")) ]) ); case "IfStatement": { const con = adjustClause(n.consequent, path.call(print, "consequent")); const opening = group( concat([ "if (", group( concat([ indent(concat([softline, path.call(print, "test")])), softline ]) ), ")", con ]) ); parts.push(opening); if (n.alternate) { const commentOnOwnLine = (hasTrailingComment(n.consequent) && n.consequent.comments.some( comment => comment.trailing && !handleComments.isBlockComment(comment) )) || needsHardlineAfterDanglingComment(n); const elseOnSameLine = n.consequent.type === "BlockStatement" && !commentOnOwnLine; parts.push(elseOnSameLine ? " " : hardline); if (hasDanglingComments(n)) { parts.push( comments.printDanglingComments(path, options, true), commentOnOwnLine ? hardline : " " ); } parts.push( "else", group( adjustClause( n.alternate, path.call(print, "alternate"), n.alternate.type === "IfStatement" ) ) ); } return concat(parts); } case "ForStatement": { const body = adjustClause(n.body, path.call(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 = comments.printDanglingComments( path, options, /* sameLine */ true ); const printedComments = dangling ? concat([dangling, softline]) : ""; if (!n.init && !n.test && !n.update) { return concat([printedComments, group(concat(["for (;;)", body]))]); } return concat([ printedComments, group( concat([ "for (", group( concat([ indent( concat([ softline, path.call(print, "init"), ";", line, path.call(print, "test"), ";", line, path.call(print, "update") ]) ), softline ]) ), ")", body ]) ) ]); } case "WhileStatement": return group( concat([ "while (", group( concat([ indent(concat([softline, path.call(print, "test")])), softline ]) ), ")", adjustClause(n.body, path.call(print, "body")) ]) ); case "ForInStatement": // Note: esprima can't actually parse "for each (". return group( concat([ n.each ? "for each (" : "for (", path.call(print, "left"), " in ", path.call(print, "right"), ")", adjustClause(n.body, path.call(print, "body")) ]) ); case "ForOfStatement": case "ForAwaitStatement": { // Babylon 7 removed ForAwaitStatement in favor of ForOfStatement // with `"await": true`: // https://github.com/estree/estree/pull/138 const isAwait = n.type === "ForAwaitStatement" || n.await; return group( concat([ "for", isAwait ? " await" : "", " (", path.call(print, "left"), " of ", path.call(print, "right"), ")", adjustClause(n.body, path.call(print, "body")) ]) ); } case "DoWhileStatement": { const clause = adjustClause(n.body, path.call(print, "body")); const doBody = group(concat(["do", clause])); parts = [doBody]; if (n.body.type === "BlockStatement") { parts.push(" "); } else { parts.push(hardline); } parts.push("while ("); parts.push( group( concat([ indent(concat([softline, path.call(print, "test")])), softline ]) ), ")", semi ); return concat(parts); } case "DoExpression": return concat(["do ", path.call(print, "body")]); case "BreakStatement": parts.push("break"); if (n.label) { parts.push(" ", path.call(print, "label")); } parts.push(semi); return concat(parts); case "ContinueStatement": parts.push("continue"); if (n.label) { parts.push(" ", path.call(print, "label")); } parts.push(semi); return concat(parts); case "LabeledStatement": if (n.body.type === "EmptyStatement") { return concat([path.call(print, "label"), ":;"]); } return concat([ path.call(print, "label"), ": ", path.call(print, "body") ]); case "TryStatement": return concat([ "try ", path.call(print, "block"), n.handler ? concat([" ", path.call(print, "handler")]) : "", n.finalizer ? concat([" finally ", path.call(print, "finalizer")]) : "" ]); case "CatchClause": if (n.param) { const hasComments = n.param.comments && n.param.comments.some( comment => !handleComments.isBlockComment(comment) || (comment.leading && hasNewline(options.originalText, options.locEnd(comment))) || (comment.trailing && hasNewline(options.originalText, options.locStart(comment), { backwards: true })) ); const param = path.call(print, "param"); return concat([ "catch ", hasComments ? concat(["(", indent(concat([softline, param])), softline, ") "]) : concat(["(", param, ") "]), path.call(print, "body") ]); } return concat(["catch ", path.call(print, "body")]); case "ThrowStatement": return concat(["throw ", path.call(print, "argument"), semi]); // Note: ignoring n.lexical because it has no printing consequences. case "SwitchStatement": return concat([ group( concat([ "switch (", indent(concat([softline, path.call(print, "discriminant")])), softline, ")" ]) ), " {", n.cases.length > 0 ? indent( concat([ hardline, join( hardline, path.map(casePath => { const caseNode = casePath.getValue(); return concat([ casePath.call(print), n.cases.indexOf(caseNode) !== n.cases.length - 1 && isNextLineEmpty(options.originalText, caseNode, options) ? hardline : "" ]); }, "cases") ) ]) ) : "", hardline, "}" ]); case "SwitchCase": { if (n.test) { parts.push("case ", path.call(print, "test"), ":"); } else { parts.push("default:"); } const consequent = n.consequent.filter( node => node.type !== "EmptyStatement" ); if (consequent.length > 0) { const cons = path.call(consequentPath => { return printStatementSequence(consequentPath, options, print); }, "consequent"); parts.push( consequent.length === 1 && consequent[0].type === "BlockStatement" ? concat([" ", cons]) : indent(concat([hardline, cons])) ); } return concat(parts); } // JSX extensions below. case "DebuggerStatement": return concat(["debugger", semi]); case "JSXAttribute": parts.push(path.call(print, "name")); if (n.value) { let res; if (isStringLiteral(n.value)) { const raw = rawText(n.value); // Unescape all quotes so we get an accurate preferred quote let final = raw.replace(/'/g, "'").replace(/"/g, '"'); const quote = getPreferredQuote( final, options.jsxSingleQuote ? "'" : '"' ); const escape = quote === "'" ? "'" : """; final = final.slice(1, -1).replace(new RegExp(quote, "g"), escape); res = concat([quote, final, quote]); } else { res = path.call(print, "value"); } parts.push("=", res); } return concat(parts); case "JSXIdentifier": return "" + n.name; case "JSXNamespacedName": return join(":", [ path.call(print, "namespace"), path.call(print, "name") ]); case "JSXMemberExpression": return join(".", [ path.call(print, "object"), path.call(print, "property") ]); case "TSQualifiedName": return join(".", [path.call(print, "left"), path.call(print, "right")]); case "JSXSpreadAttribute": case "JSXSpreadChild": { return concat([ "{", path.call( p => { const printed = concat(["...", print(p)]); const n = p.getValue(); if (!n.comments || !n.comments.length) { return printed; } return concat([ indent( concat([ softline, comments.printComments(p, () => printed, options) ]) ), softline ]); }, n.type === "JSXSpreadAttribute" ? "argument" : "expression" ), "}" ]); } case "JSXExpressionContainer": { const parent = path.getParentNode(0); const preventInline = parent.type === "JSXAttribute" && n.expression.comments && n.expression.comments.length > 0; const shouldInline = !preventInline && (n.expression.type === "ArrayExpression" || n.expression.type === "ObjectExpression" || n.expression.type === "ArrowFunctionExpression" || n.expression.type === "CallExpression" || n.expression.type === "OptionalCallExpression" || n.expression.type === "FunctionExpression" || n.expression.type === "JSXEmptyExpression" || n.expression.type === "TemplateLiteral" || n.expression.type === "TaggedTemplateExpression" || n.expression.type === "DoExpression" || (isJSXNode(parent) && (n.expression.type === "ConditionalExpression" || isBinaryish(n.expression)))); if (shouldInline) { return group( concat(["{", path.call(print, "expression"), lineSuffixBoundary, "}"]) ); } return group( concat([ "{", indent(concat([softline, path.call(print, "expression")])), softline, lineSuffixBoundary, "}" ]) ); } case "JSXFragment": case "JSXElement": { const elem = comments.printComments( path, () => printJSXElement(path, options, print), options ); return maybeWrapJSXElementInParens(path, elem); } case "JSXOpeningElement": { const n = path.getValue(); const nameHasComments = n.name && n.name.comments && n.name.comments.length > 0; // Don't break self-closing elements with no attributes and no comments if (n.selfClosing && !n.attributes.length && !nameHasComments) { return concat([ "<", path.call(print, "name"), path.call(print, "typeParameters"), " />" ]); } // don't break up opening elements with a single long text attribute if ( n.attributes && n.attributes.length === 1 && n.attributes[0].value && isStringLiteral(n.attributes[0].value) && !n.attributes[0].value.value.includes("\n") && // We should break for the following cases: //
//
!nameHasComments && (!n.attributes[0].comments || !n.attributes[0].comments.length) ) { return group( concat([ "<", path.call(print, "name"), path.call(print, "typeParameters"), " ", concat(path.map(print, "attributes")), n.selfClosing ? " />" : ">" ]) ); } const lastAttrHasTrailingComments = n.attributes.length && hasTrailingComment(getLast(n.attributes)); const bracketSameLine = // Simple tags (no attributes and no comment in tag name) should be // kept unbroken regardless of `jsxBracketSameLine` (!n.attributes.length && !nameHasComments) || (options.jsxBracketSameLine && // We should print the bracket in a new line for the following cases: //
//
(!nameHasComments || n.attributes.length) && !lastAttrHasTrailingComments); // We should print the opening element expanded if any prop value is a // string literal with newlines const shouldBreak = n.attributes && n.attributes.some( attr => attr.value && isStringLiteral(attr.value) && attr.value.value.includes("\n") ); return group( concat([ "<", path.call(print, "name"), path.call(print, "typeParameters"), concat([ indent( concat( path.map(attr => concat([line, print(attr)]), "attributes") ) ), n.selfClosing ? line : bracketSameLine ? ">" : softline ]), n.selfClosing ? "/>" : bracketSameLine ? "" : ">" ]), { shouldBreak } ); } case "JSXClosingElement": return concat([""]); case "JSXOpeningFragment": case "JSXClosingFragment": { const hasComment = n.comments && n.comments.length; const hasOwnLineComment = hasComment && !n.comments.every(handleComments.isBlockComment); const isOpeningFragment = n.type === "JSXOpeningFragment"; return concat([ isOpeningFragment ? "<" : "" ]); } case "JSXText": /* istanbul ignore next */ throw new Error("JSXTest should be handled by JSXElement"); case "JSXEmptyExpression": { const requiresHardline = n.comments && !n.comments.every(handleComments.isBlockComment); return concat([ comments.printDanglingComments( path, options, /* sameIndent */ !requiresHardline ), requiresHardline ? hardline : "" ]); } case "ClassBody": if (!n.comments && n.body.length === 0) { return "{}"; } return concat([ "{", n.body.length > 0 ? indent( concat([ hardline, path.call(bodyPath => { return printStatementSequence(bodyPath, options, print); }, "body") ]) ) : comments.printDanglingComments(path, options), hardline, "}" ]); case "ClassProperty": case "TSAbstractClassProperty": case "ClassPrivateProperty": { if (n.decorators && n.decorators.length !== 0) { parts.push(printDecorators(path, options, print)); } if (n.accessibility) { parts.push(n.accessibility + " "); } if (n.static) { parts.push("static "); } if (n.type === "TSAbstractClassProperty") { parts.push("abstract "); } if (n.readonly) { parts.push("readonly "); } const variance = getFlowVariance(n); if (variance) { parts.push(variance); } if (n.computed) { parts.push("[", path.call(print, "key"), "]"); } else { parts.push(printPropertyKey(path, options, print)); } parts.push(printOptionalToken(path)); parts.push(printTypeAnnotation(path, options, print)); if (n.value) { parts.push( " =", printAssignmentRight( n.key, n.value, path.call(print, "value"), options ) ); } parts.push(semi); return group(concat(parts)); } case "ClassDeclaration": case "ClassExpression": case "TSAbstractClassDeclaration": if (isNodeStartingWithDeclare(n, options)) { parts.push("declare "); } parts.push(concat(printClass(path, options, print))); return concat(parts); case "TSInterfaceHeritage": parts.push(path.call(print, "id")); if (n.typeParameters) { parts.push(path.call(print, "typeParameters")); } return concat(parts); case "TemplateElement": return join(literalline, n.value.raw.split(/\r?\n/g)); case "TemplateLiteral": { const expressions = path.map(print, "expressions"); const parentNode = path.getParentNode(); /** * describe.each`table`(name, fn) * describe.only.each`table`(name, fn) * describe.skip.each`table`(name, fn) * test.each`table`(name, fn) * test.only.each`table`(name, fn) * test.skip.each`table`(name, fn) * * Ref: https://github.com/facebook/jest/pull/6102 */ const jestEachTriggerRegex = /^[xf]?(describe|it|test)$/; if ( parentNode.type === "TaggedTemplateExpression" && parentNode.quasi === n && parentNode.tag.type === "MemberExpression" && parentNode.tag.property.type === "Identifier" && parentNode.tag.property.name === "each" && ((parentNode.tag.object.type === "Identifier" && jestEachTriggerRegex.test(parentNode.tag.object.name)) || (parentNode.tag.object.type === "MemberExpression" && parentNode.tag.object.property.type === "Identifier" && (parentNode.tag.object.property.name === "only" || parentNode.tag.object.property.name === "skip") && parentNode.tag.object.object.type === "Identifier" && jestEachTriggerRegex.test(parentNode.tag.object.object.name))) ) { /** * a | b | expected * ${1} | ${1} | ${2} * ${1} | ${2} | ${3} * ${2} | ${1} | ${3} */ const headerNames = n.quasis[0].value.raw.trim().split(/\s*\|\s*/); if ( headerNames.length > 1 || headerNames.some(headerName => headerName.length !== 0) ) { const stringifiedExpressions = expressions.map( doc => "${" + printDocToString( doc, Object.assign({}, options, { printWidth: Infinity, endOfLine: "lf" }) ).formatted + "}" ); const tableBody = [{ hasLineBreak: false, cells: [] }]; for (let i = 1; i < n.quasis.length; i++) { const row = tableBody[tableBody.length - 1]; const correspondingExpression = stringifiedExpressions[i - 1]; row.cells.push(correspondingExpression); if (correspondingExpression.indexOf("\n") !== -1) { row.hasLineBreak = true; } if (n.quasis[i].value.raw.indexOf("\n") !== -1) { tableBody.push({ hasLineBreak: false, cells: [] }); } } const maxColumnCount = tableBody.reduce( (maxColumnCount, row) => Math.max(maxColumnCount, row.cells.length), headerNames.length ); const maxColumnWidths = Array.from( new Array(maxColumnCount), () => 0 ); const table = [{ cells: headerNames }].concat( tableBody.filter(row => row.cells.length !== 0) ); table .filter(row => !row.hasLineBreak) .forEach(row => { row.cells.forEach((cell, index) => { maxColumnWidths[index] = Math.max( maxColumnWidths[index], getStringWidth(cell) ); }); }); parts.push( "`", indent( concat([ hardline, join( hardline, table.map(row => join( " | ", row.cells.map((cell, index) => row.hasLineBreak ? cell : cell + " ".repeat( maxColumnWidths[index] - getStringWidth(cell) ) ) ) ) ) ]) ), hardline, "`" ); return concat(parts); } } parts.push("`"); path.each(childPath => { const i = childPath.getName(); parts.push(print(childPath)); if (i < expressions.length) { // For a template literal of the following form: // `someQuery { // ${call({ // a, // b, // })} // }` // the expression is on its own line (there is a \n in the previous // quasi literal), therefore we want to indent the JavaScript // expression inside at the beginning of ${ instead of the beginning // of the `. const tabWidth = options.tabWidth; const indentSize = getIndentSize( childPath.getValue().value.raw, tabWidth ); let printed = expressions[i]; if ( (n.expressions[i].comments && n.expressions[i].comments.length) || n.expressions[i].type === "MemberExpression" || n.expressions[i].type === "OptionalMemberExpression" || n.expressions[i].type === "ConditionalExpression" ) { printed = concat([indent(concat([softline, printed])), softline]); } const aligned = addAlignmentToDoc(printed, indentSize, tabWidth); parts.push(group(concat(["${", aligned, lineSuffixBoundary, "}"]))); } }, "quasis"); parts.push("`"); return concat(parts); } // These types are unprintable because they serve as abstract // supertypes for other (printable) types. case "TaggedTemplateExpression": return concat([ path.call(print, "tag"), path.call(print, "typeParameters"), path.call(print, "quasi") ]); case "Node": case "Printable": case "SourceLocation": case "Position": case "Statement": case "Function": case "Pattern": case "Expression": case "Declaration": case "Specifier": case "NamedSpecifier": case "Comment": case "MemberTypeAnnotation": // Flow case "Type": /* istanbul ignore next */ throw new Error("unprintable type: " + JSON.stringify(n.type)); // Type Annotations for Facebook Flow, typically stripped out or // transformed away before printing. case "TypeAnnotation": case "TSTypeAnnotation": if (n.typeAnnotation) { return path.call(print, "typeAnnotation"); } /* istanbul ignore next */ return ""; case "TSTupleType": case "TupleTypeAnnotation": { const typesField = n.type === "TSTupleType" ? "elementTypes" : "types"; return group( concat([ "[", indent( concat([ softline, printArrayItems(path, options, typesField, print) ]) ), // TypeScript doesn't support trailing commas in tuple types n.type === "TSTupleType" ? "" : ifBreak(shouldPrintComma(options) ? "," : ""), comments.printDanglingComments(path, options, /* sameIndent */ true), softline, "]" ]) ); } case "ExistsTypeAnnotation": return "*"; case "EmptyTypeAnnotation": return "empty"; case "AnyTypeAnnotation": return "any"; case "MixedTypeAnnotation": return "mixed"; case "ArrayTypeAnnotation": return concat([path.call(print, "elementType"), "[]"]); case "BooleanTypeAnnotation": return "boolean"; case "BooleanLiteralTypeAnnotation": return "" + n.value; case "DeclareClass": return printFlowDeclaration(path, printClass(path, options, print)); case "DeclareFunction": // For TypeScript the DeclareFunction node shares the AST // structure with FunctionDeclaration if (n.params) { return concat([ "declare ", printFunctionDeclaration(path, print, options), semi ]); } return printFlowDeclaration(path, [ "function ", path.call(print, "id"), n.predicate ? " " : "", path.call(print, "predicate"), semi ]); case "DeclareModule": return printFlowDeclaration(path, [ "module ", path.call(print, "id"), " ", path.call(print, "body") ]); case "DeclareModuleExports": return printFlowDeclaration(path, [ "module.exports", ": ", path.call(print, "typeAnnotation"), semi ]); case "DeclareVariable": return printFlowDeclaration(path, ["var ", path.call(print, "id"), semi]); case "DeclareExportAllDeclaration": return concat(["declare export * from ", path.call(print, "source")]); case "DeclareExportDeclaration": return concat(["declare ", printExportDeclaration(path, options, print)]); case "DeclareOpaqueType": case "OpaqueType": { parts.push( "opaque type ", path.call(print, "id"), path.call(print, "typeParameters") ); if (n.supertype) { parts.push(": ", path.call(print, "supertype")); } if (n.impltype) { parts.push(" = ", path.call(print, "impltype")); } parts.push(semi); if (n.type === "DeclareOpaqueType") { return printFlowDeclaration(path, parts); } return concat(parts); } case "FunctionTypeAnnotation": case "TSFunctionType": { // FunctionTypeAnnotation is ambiguous: // declare function foo(a: B): void; OR // var A: (a: B) => void; const parent = path.getParentNode(0); const parentParent = path.getParentNode(1); const parentParentParent = path.getParentNode(2); let isArrowFunctionTypeAnnotation = n.type === "TSFunctionType" || !( ((parent.type === "ObjectTypeProperty" || parent.type === "ObjectTypeInternalSlot") && !getFlowVariance(parent) && !parent.optional && options.locStart(parent) === options.locStart(n)) || parent.type === "ObjectTypeCallProperty" || (parentParentParent && parentParentParent.type === "DeclareFunction") ); let needsColon = isArrowFunctionTypeAnnotation && (parent.type === "TypeAnnotation" || parent.type === "TSTypeAnnotation"); // Sadly we can't put it inside of FastPath::needsColon because we are // printing ":" as part of the expression and it would put parenthesis // around :( const needsParens = needsColon && isArrowFunctionTypeAnnotation && (parent.type === "TypeAnnotation" || parent.type === "TSTypeAnnotation") && parentParent.type === "ArrowFunctionExpression"; if (isObjectTypePropertyAFunction(parent, options)) { isArrowFunctionTypeAnnotation = true; needsColon = true; } if (needsParens) { parts.push("("); } parts.push( printFunctionParams( path, print, options, /* expandArg */ false, /* printTypeParams */ true ) ); // The returnType is not wrapped in a TypeAnnotation, so the colon // needs to be added separately. if (n.returnType || n.predicate || n.typeAnnotation) { parts.push( isArrowFunctionTypeAnnotation ? " => " : ": ", path.call(print, "returnType"), path.call(print, "predicate"), path.call(print, "typeAnnotation") ); } if (needsParens) { parts.push(")"); } return group(concat(parts)); } case "TSRestType": return concat(["...", path.call(print, "typeAnnotation")]); case "TSOptionalType": return concat([path.call(print, "typeAnnotation"), "?"]); case "FunctionTypeParam": return concat([ path.call(print, "name"), printOptionalToken(path), n.name ? ": " : "", path.call(print, "typeAnnotation") ]); case "GenericTypeAnnotation": return concat([ path.call(print, "id"), path.call(print, "typeParameters") ]); case "DeclareInterface": case "InterfaceDeclaration": case "InterfaceTypeAnnotation": { if ( n.type === "DeclareInterface" || isNodeStartingWithDeclare(n, options) ) { parts.push("declare "); } parts.push("interface"); if (n.type === "DeclareInterface" || n.type === "InterfaceDeclaration") { parts.push( " ", path.call(print, "id"), path.call(print, "typeParameters") ); } if (n["extends"].length > 0) { parts.push( group( indent( concat([ line, "extends ", (n.extends.length === 1 ? identity : indent)( join(concat([",", line]), path.map(print, "extends")) ) ]) ) ) ); } parts.push(" ", path.call(print, "body")); return group(concat(parts)); } case "ClassImplements": case "InterfaceExtends": return concat([ path.call(print, "id"), path.call(print, "typeParameters") ]); case "TSIntersectionType": case "IntersectionTypeAnnotation": { const types = path.map(print, "types"); const result = []; let wasIndented = false; for (let i = 0; i < types.length; ++i) { if (i === 0) { result.push(types[i]); } else if (isObjectType(n.types[i - 1]) && isObjectType(n.types[i])) { // If both are objects, don't indent result.push( concat([" & ", wasIndented ? indent(types[i]) : types[i]]) ); } else if (!isObjectType(n.types[i - 1]) && !isObjectType(n.types[i])) { // If no object is involved, go to the next line if it breaks result.push(indent(concat([" &", line, types[i]]))); } else { // If you go from object to non-object or vis-versa, then inline it if (i > 1) { wasIndented = true; } result.push(" & ", i > 1 ? indent(types[i]) : types[i]); } } return group(concat(result)); } case "TSUnionType": case "UnionTypeAnnotation": { // single-line variation // A | B | C // multi-line variation // | A // | B // | C const parent = path.getParentNode(); const parentParent = path.getParentNode(1); // If there's a leading comment, the parent is doing the indentation const shouldIndent = parent.type !== "TypeParameterInstantiation" && parent.type !== "TSTypeParameterInstantiation" && parent.type !== "GenericTypeAnnotation" && parent.type !== "TSTypeReference" && !(parent.type === "FunctionTypeParam" && !parent.name) && parentParent.type !== "TSTypeAssertionExpression" && !( (parent.type === "TypeAlias" || parent.type === "VariableDeclarator") && hasLeadingOwnLineComment(options.originalText, n, options) ); // { // a: string // } | null | void // should be inlined and not be printed in the multi-line variant const shouldHug = shouldHugType(n); // We want to align the children but without its comment, so it looks like // | child1 // // comment // | child2 const printed = path.map(typePath => { let printedType = typePath.call(print); if (!shouldHug) { printedType = align(2, printedType); } return comments.printComments(typePath, () => printedType, options); }, "types"); if (shouldHug) { return join(" | ", printed); } const shouldAddStartLine = shouldIndent && !hasLeadingOwnLineComment(options.originalText, n, options); const code = concat([ ifBreak(concat([shouldAddStartLine ? line : "", "| "])), join(concat([line, "| "]), printed) ]); let hasParens; if (n.type === "TSUnionType") { const greatGrandParent = path.getParentNode(2); const greatGreatGrandParent = path.getParentNode(3); hasParens = greatGrandParent && greatGrandParent.type === "TSParenthesizedType" && greatGreatGrandParent && (greatGreatGrandParent.type === "TSUnionType" || greatGreatGrandParent.type === "TSIntersectionType"); } else { hasParens = pathNeedsParens(path, options); } if (hasParens) { return group(concat([indent(code), softline])); } return group(shouldIndent ? indent(code) : code); } case "NullableTypeAnnotation": return concat(["?", path.call(print, "typeAnnotation")]); case "TSNullKeyword": case "NullLiteralTypeAnnotation": return "null"; case "ThisTypeAnnotation": return "this"; case "NumberTypeAnnotation": return "number"; case "ObjectTypeCallProperty": if (n.static) { parts.push("static "); } parts.push(path.call(print, "value")); return concat(parts); case "ObjectTypeIndexer": { const variance = getFlowVariance(n); return concat([ variance || "", "[", path.call(print, "id"), n.id ? ": " : "", path.call(print, "key"), "]: ", path.call(print, "value") ]); } case "ObjectTypeProperty": { const variance = getFlowVariance(n); let modifier = ""; if (n.proto) { modifier = "proto "; } else if (n.static) { modifier = "static "; } return concat([ modifier, isGetterOrSetter(n) ? n.kind + " " : "", variance || "", printPropertyKey(path, options, print), printOptionalToken(path), isFunctionNotation(n, options) ? "" : ": ", path.call(print, "value") ]); } case "QualifiedTypeIdentifier": return concat([ path.call(print, "qualification"), ".", path.call(print, "id") ]); case "StringLiteralTypeAnnotation": return nodeStr(n, options); case "NumberLiteralTypeAnnotation": assert.strictEqual(typeof n.value, "number"); if (n.extra != null) { return printNumber(n.extra.raw); } return printNumber(n.raw); case "StringTypeAnnotation": return "string"; case "DeclareTypeAlias": case "TypeAlias": { if ( n.type === "DeclareTypeAlias" || isNodeStartingWithDeclare(n, options) ) { parts.push("declare "); } const printed = printAssignmentRight( n.id, n.right, path.call(print, "right"), options ); parts.push( "type ", path.call(print, "id"), path.call(print, "typeParameters"), " =", printed, semi ); return group(concat(parts)); } case "TypeCastExpression": { const value = path.getValue(); // Flow supports a comment syntax for specifying type annotations: https://flow.org/en/docs/types/comments/. // Unfortunately, its parser doesn't differentiate between comment annotations and regular // annotations when producing an AST. So to preserve parentheses around type casts that use // the comment syntax, we need to hackily read the source itself to see if the code contains // a type annotation comment. // // Note that we're able to use the normal whitespace regex here because the Flow parser has // already deemed this AST node to be a type cast. Only the Babylon parser needs the // non-line-break whitespace regex, which is why hasFlowShorthandAnnotationComment() is // implemented differently. const commentSyntax = value && value.typeAnnotation && value.typeAnnotation.range && options.originalText .substring(value.typeAnnotation.range[0]) .match(/^\/\*\s*:/); return concat([ "(", path.call(print, "expression"), commentSyntax ? " /*" : "", ": ", path.call(print, "typeAnnotation"), commentSyntax ? " */" : "", ")" ]); } case "TypeParameterDeclaration": case "TypeParameterInstantiation": { const value = path.getValue(); const commentStart = value.range ? options.originalText.substring(0, value.range[0]).lastIndexOf("/*") : -1; // As noted in the TypeCastExpression comments above, we're able to use a normal whitespace regex here // because we know for sure that this is a type definition. const commentSyntax = commentStart >= 0 && options.originalText.substring(commentStart).match(/^\/\*\s*::/); if (commentSyntax) { return concat([ "/*:: ", printTypeParameters(path, options, print, "params"), " */" ]); } return printTypeParameters(path, options, print, "params"); } case "TSTypeParameterDeclaration": case "TSTypeParameterInstantiation": return printTypeParameters(path, options, print, "params"); case "TSTypeParameter": case "TypeParameter": { const parent = path.getParentNode(); if (parent.type === "TSMappedType") { parts.push("[", path.call(print, "name")); if (n.constraint) { parts.push(" in ", path.call(print, "constraint")); } parts.push("]"); return concat(parts); } const variance = getFlowVariance(n); if (variance) { parts.push(variance); } parts.push(path.call(print, "name")); if (n.bound) { parts.push(": "); parts.push(path.call(print, "bound")); } if (n.constraint) { parts.push(" extends ", path.call(print, "constraint")); } if (n["default"]) { parts.push(" = ", path.call(print, "default")); } return concat(parts); } case "TypeofTypeAnnotation": return concat(["typeof ", path.call(print, "argument")]); case "VoidTypeAnnotation": return "void"; case "InferredPredicate": return "%checks"; // Unhandled types below. If encountered, nodes of these types should // be either left alone or desugared into AST types that are fully // supported by the pretty-printer. case "DeclaredPredicate": return concat(["%checks(", path.call(print, "value"), ")"]); case "TSAbstractKeyword": return "abstract"; case "TSAnyKeyword": return "any"; case "TSAsyncKeyword": return "async"; case "TSBooleanKeyword": return "boolean"; case "TSBigIntKeyword": return "bigint"; case "TSConstKeyword": return "const"; case "TSDeclareKeyword": return "declare"; case "TSExportKeyword": return "export"; case "TSNeverKeyword": return "never"; case "TSNumberKeyword": return "number"; case "TSObjectKeyword": return "object"; case "TSProtectedKeyword": return "protected"; case "TSPrivateKeyword": return "private"; case "TSPublicKeyword": return "public"; case "TSReadonlyKeyword": return "readonly"; case "TSSymbolKeyword": return "symbol"; case "TSStaticKeyword": return "static"; case "TSStringKeyword": return "string"; case "TSUndefinedKeyword": return "undefined"; case "TSUnknownKeyword": return "unknown"; case "TSVoidKeyword": return "void"; case "TSAsExpression": return concat([ path.call(print, "expression"), " as ", path.call(print, "typeAnnotation") ]); case "TSArrayType": return concat([path.call(print, "elementType"), "[]"]); case "TSPropertySignature": { if (n.export) { parts.push("export "); } if (n.accessibility) { parts.push(n.accessibility + " "); } if (n.static) { parts.push("static "); } if (n.readonly) { parts.push("readonly "); } if (n.computed) { parts.push("["); } parts.push(printPropertyKey(path, options, print)); if (n.computed) { parts.push("]"); } parts.push(printOptionalToken(path)); if (n.typeAnnotation) { parts.push(": "); parts.push(path.call(print, "typeAnnotation")); } // This isn't valid semantically, but it's in the AST so we can print it. if (n.initializer) { parts.push(" = ", path.call(print, "initializer")); } return concat(parts); } case "TSParameterProperty": if (n.accessibility) { parts.push(n.accessibility + " "); } if (n.export) { parts.push("export "); } if (n.static) { parts.push("static "); } if (n.readonly) { parts.push("readonly "); } parts.push(path.call(print, "parameter")); return concat(parts); case "TSTypeReference": return concat([ path.call(print, "typeName"), printTypeParameters(path, options, print, "typeParameters") ]); case "TSTypeQuery": return concat(["typeof ", path.call(print, "exprName")]); case "TSParenthesizedType": { return path.call(print, "typeAnnotation"); } case "TSIndexSignature": { const parent = path.getParentNode(); return concat([ n.export ? "export " : "", n.accessibility ? concat([n.accessibility, " "]) : "", n.static ? "static " : "", n.readonly ? "readonly " : "", "[", path.call(print, "index"), "]: ", path.call(print, "typeAnnotation"), parent.type === "ClassBody" ? semi : "" ]); } case "TSTypePredicate": return concat([ path.call(print, "parameterName"), " is ", path.call(print, "typeAnnotation") ]); case "TSNonNullExpression": return concat([path.call(print, "expression"), "!"]); case "TSThisType": return "this"; case "TSImportType": return concat([ !n.isTypeOf ? "" : "typeof ", "import(", path.call(print, "parameter"), ")", !n.qualifier ? "" : concat([".", path.call(print, "qualifier")]), printTypeParameters(path, options, print, "typeParameters") ]); case "TSLiteralType": return path.call(print, "literal"); case "TSIndexedAccessType": return concat([ path.call(print, "objectType"), "[", path.call(print, "indexType"), "]" ]); case "TSConstructSignature": case "TSConstructorType": case "TSCallSignature": { if (n.type !== "TSCallSignature") { parts.push("new "); } parts.push( group( printFunctionParams( path, print, options, /* expandArg */ false, /* printTypeParams */ true ) ) ); if (n.typeAnnotation) { const isType = n.type === "TSConstructorType"; parts.push(isType ? " => " : ": ", path.call(print, "typeAnnotation")); } return concat(parts); } case "TSTypeOperator": return concat([n.operator, " ", path.call(print, "typeAnnotation")]); case "TSMappedType": return group( concat([ "{", indent( concat([ options.bracketSpacing ? line : softline, n.readonlyToken ? concat([ getTypeScriptMappedTypeModifier( n.readonlyToken, "readonly" ), " " ]) : "", printTypeScriptModifiers(path, options, print), path.call(print, "typeParameter"), n.questionToken ? getTypeScriptMappedTypeModifier(n.questionToken, "?") : "", ": ", path.call(print, "typeAnnotation") ]) ), comments.printDanglingComments(path, options, /* sameIndent */ true), options.bracketSpacing ? line : softline, "}" ]) ); case "TSMethodSignature": parts.push( n.accessibility ? concat([n.accessibility, " "]) : "", n.export ? "export " : "", n.static ? "static " : "", n.readonly ? "readonly " : "", n.computed ? "[" : "", path.call(print, "key"), n.computed ? "]" : "", printOptionalToken(path), printFunctionParams( path, print, options, /* expandArg */ false, /* printTypeParams */ true ) ); if (n.typeAnnotation) { parts.push(": ", path.call(print, "typeAnnotation")); } return group(concat(parts)); case "TSNamespaceExportDeclaration": parts.push("export as namespace ", path.call(print, "name")); if (options.semi) { parts.push(";"); } return group(concat(parts)); case "TSEnumDeclaration": if (isNodeStartingWithDeclare(n, options)) { parts.push("declare "); } if (n.modifiers) { parts.push(printTypeScriptModifiers(path, options, print)); } if (n.const) { parts.push("const "); } parts.push("enum ", path.call(print, "id"), " "); if (n.members.length === 0) { parts.push( group( concat([ "{", comments.printDanglingComments(path, options), softline, "}" ]) ) ); } else { parts.push( group( concat([ "{", indent( concat([ hardline, printArrayItems(path, options, "members", print), shouldPrintComma(options, "es5") ? "," : "" ]) ), comments.printDanglingComments( path, options, /* sameIndent */ true ), hardline, "}" ]) ) ); } return concat(parts); case "TSEnumMember": parts.push(path.call(print, "id")); if (n.initializer) { parts.push(" = ", path.call(print, "initializer")); } return concat(parts); case "TSImportEqualsDeclaration": parts.push( printTypeScriptModifiers(path, options, print), "import ", path.call(print, "name"), " = ", path.call(print, "moduleReference") ); if (options.semi) { parts.push(";"); } return group(concat(parts)); case "TSExternalModuleReference": return concat(["require(", path.call(print, "expression"), ")"]); case "TSModuleDeclaration": { const parent = path.getParentNode(); const isExternalModule = isLiteral(n.id); const parentIsDeclaration = parent.type === "TSModuleDeclaration"; const bodyIsDeclaration = n.body && n.body.type === "TSModuleDeclaration"; if (parentIsDeclaration) { parts.push("."); } else { if (n.declare === true) { parts.push("declare "); } parts.push(printTypeScriptModifiers(path, options, print)); const textBetweenNodeAndItsId = options.originalText.slice( options.locStart(n), options.locStart(n.id) ); // Global declaration looks like this: // (declare)? global { ... } const isGlobalDeclaration = n.id.type === "Identifier" && n.id.name === "global" && !/namespace|module/.test(textBetweenNodeAndItsId); if (!isGlobalDeclaration) { parts.push( isExternalModule || /(^|\s)module(\s|$)/.test(textBetweenNodeAndItsId) ? "module " : "namespace " ); } } parts.push(path.call(print, "id")); if (bodyIsDeclaration) { parts.push(path.call(print, "body")); } else if (n.body) { parts.push(" ", group(path.call(print, "body"))); } else { parts.push(semi); } return concat(parts); } case "PrivateName": return concat(["#", path.call(print, "id")]); case "TSConditionalType": return printTernaryOperator(path, options, print, { beforeParts: () => [ path.call(print, "checkType"), " ", "extends", " ", path.call(print, "extendsType") ], afterParts: () => [], shouldCheckJsx: false, conditionalNodeType: "TSConditionalType", consequentNodePropertyName: "trueType", alternateNodePropertyName: "falseType", testNodePropertyName: "checkType", breakNested: true }); case "TSInferType": return concat(["infer", " ", path.call(print, "typeParameter")]); case "InterpreterDirective": parts.push("#!", n.value, hardline); if (isNextLineEmpty(options.originalText, n, options)) { parts.push(hardline); } return concat(parts); case "NGRoot": return concat( [].concat( path.call(print, "node"), !n.node.comments || n.node.comments.length === 0 ? [] : concat([" //", n.node.comments[0].value.trimRight()]) ) ); case "NGChainedExpression": return group( join( concat([";", line]), path.map( childPath => hasNgSideEffect(childPath) ? print(childPath) : concat(["(", print(childPath), ")"]), "expressions" ) ) ); case "NGEmptyExpression": return ""; case "NGQuotedExpression": return concat([n.prefix, ":", n.value]); case "NGMicrosyntax": return concat( path.map( (childPath, index) => concat([ index === 0 ? "" : isNgForOf(childPath.getValue(), index, n) ? " " : concat([";", line]), print(childPath) ]), "body" ) ); case "NGMicrosyntaxKey": return /^[a-z_$][a-z0-9_$]*(-[a-z_$][a-z0-9_$])*$/i.test(n.name) ? n.name : JSON.stringify(n.name); case "NGMicrosyntaxExpression": return concat([ path.call(print, "expression"), n.alias === null ? "" : concat([" as ", path.call(print, "alias")]) ]); case "NGMicrosyntaxKeyedExpression": { const index = path.getName(); const parentNode = path.getParentNode(); const shouldNotPrintColon = isNgForOf(n, index, parentNode) || (((index === 1 && (n.key.name === "then" || n.key.name === "else")) || (index === 2 && (n.key.name === "else" && parentNode.body[index - 1].type === "NGMicrosyntaxKeyedExpression" && parentNode.body[index - 1].key.name === "then"))) && parentNode.body[0].type === "NGMicrosyntaxExpression"); return concat([ path.call(print, "key"), shouldNotPrintColon ? " " : ": ", path.call(print, "expression") ]); } case "NGMicrosyntaxLet": return concat([ "let ", path.call(print, "key"), n.value === null ? "" : concat([" = ", path.call(print, "value")]) ]); case "NGMicrosyntaxAs": return concat([ path.call(print, "key"), " as ", path.call(print, "alias") ]); default: /* istanbul ignore next */ throw new Error("unknown type: " + JSON.stringify(n.type)); } } function isNgForOf(node, index, parentNode) { return ( node.type === "NGMicrosyntaxKeyedExpression" && node.key.name === "of" && index === 1 && parentNode.body[0].type === "NGMicrosyntaxLet" && parentNode.body[0].value === null ); } /** identify if an angular expression seems to have side effects */ function hasNgSideEffect(path) { return hasNode(path.getValue(), node => { switch (node.type) { case undefined: return false; case "CallExpression": case "OptionalCallExpression": case "AssignmentExpression": return true; } }); } function printStatementSequence(path, options, print) { const printed = []; const bodyNode = path.getNode(); const isClass = bodyNode.type === "ClassBody"; path.map((stmtPath, i) => { const stmt = stmtPath.getValue(); // Just in case the AST has been modified to contain falsy // "statements," it's safer simply to skip them. /* istanbul ignore if */ if (!stmt) { return; } // Skip printing EmptyStatement nodes to avoid leaving stray // semicolons lying around. if (stmt.type === "EmptyStatement") { return; } const stmtPrinted = print(stmtPath); const text = options.originalText; const parts = []; // in no-semi mode, prepend statement with semicolon if it might break ASI // don't prepend the only JSX element in a program with semicolon if ( !options.semi && !isClass && !isTheOnlyJSXElementInMarkdown(options, stmtPath) && stmtNeedsASIProtection(stmtPath, options) ) { if (stmt.comments && stmt.comments.some(comment => comment.leading)) { parts.push(print(stmtPath, { needsSemi: true })); } else { parts.push(";", stmtPrinted); } } else { parts.push(stmtPrinted); } if (!options.semi && isClass) { if (classPropMayCauseASIProblems(stmtPath)) { parts.push(";"); } else if (stmt.type === "ClassProperty") { const nextChild = bodyNode.body[i + 1]; if (classChildNeedsASIProtection(nextChild)) { parts.push(";"); } } } if (isNextLineEmpty(text, stmt, options) && !isLastStatement(stmtPath)) { parts.push(hardline); } printed.push(concat(parts)); }); return join(hardline, printed); } function printPropertyKey(path, options, print) { const node = path.getNode(); const key = node.key; if ( key.type === "Identifier" && !node.computed && options.parser === "json" ) { // a -> "a" return path.call( keyPath => comments.printComments( keyPath, () => JSON.stringify(key.name), options ), "key" ); } if ( isStringLiteral(key) && isIdentifierName(key.value) && !node.computed && options.parser !== "json" && !(options.parser === "typescript" && node.type === "ClassProperty") ) { // 'a' -> a return path.call( keyPath => comments.printComments(keyPath, () => key.value, options), "key" ); } return path.call(print, "key"); } function printMethod(path, options, print) { const node = path.getNode(); const semi = options.semi ? ";" : ""; const kind = node.kind; const parts = []; if (node.type === "ObjectMethod" || node.type === "ClassMethod") { node.value = node; } if (node.value.async) { parts.push("async "); } if (!kind || kind === "init" || kind === "method" || kind === "constructor") { if (node.value.generator) { parts.push("*"); } } else { assert.ok(kind === "get" || kind === "set"); parts.push(kind, " "); } let key = printPropertyKey(path, options, print); if (node.computed) { key = concat(["[", key, "]"]); } parts.push( key, concat( path.call( valuePath => [ printFunctionTypeParameters(valuePath, options, print), group( concat([ printFunctionParams(valuePath, print, options), printReturnType(valuePath, print, options) ]) ) ], "value" ) ) ); if (!node.value.body || node.value.body.length === 0) { parts.push(semi); } else { parts.push(" ", path.call(print, "value", "body")); } return concat(parts); } function couldGroupArg(arg) { return ( (arg.type === "ObjectExpression" && (arg.properties.length > 0 || arg.comments)) || (arg.type === "ArrayExpression" && (arg.elements.length > 0 || arg.comments)) || arg.type === "TSTypeAssertionExpression" || arg.type === "TSAsExpression" || arg.type === "FunctionExpression" || (arg.type === "ArrowFunctionExpression" && !arg.returnType && (arg.body.type === "BlockStatement" || arg.body.type === "ArrowFunctionExpression" || arg.body.type === "ObjectExpression" || arg.body.type === "ArrayExpression" || arg.body.type === "CallExpression" || arg.body.type === "OptionalCallExpression" || arg.body.type === "ConditionalExpression" || isJSXNode(arg.body))) ); } function shouldGroupLastArg(args) { const lastArg = getLast(args); const penultimateArg = getPenultimate(args); return ( !hasLeadingComment(lastArg) && !hasTrailingComment(lastArg) && couldGroupArg(lastArg) && // If the last two arguments are of the same type, // disable last element expansion. (!penultimateArg || penultimateArg.type !== lastArg.type) ); } function shouldGroupFirstArg(args) { if (args.length !== 2) { return false; } const firstArg = args[0]; const secondArg = args[1]; return ( (!firstArg.comments || !firstArg.comments.length) && (firstArg.type === "FunctionExpression" || (firstArg.type === "ArrowFunctionExpression" && firstArg.body.type === "BlockStatement")) && secondArg.type !== "FunctionExpression" && secondArg.type !== "ArrowFunctionExpression" && secondArg.type !== "ConditionalExpression" && !couldGroupArg(secondArg) ); } function isSimpleFlowType(node) { const flowTypeAnnotations = [ "AnyTypeAnnotation", "NullLiteralTypeAnnotation", "GenericTypeAnnotation", "ThisTypeAnnotation", "NumberTypeAnnotation", "VoidTypeAnnotation", "EmptyTypeAnnotation", "MixedTypeAnnotation", "BooleanTypeAnnotation", "BooleanLiteralTypeAnnotation", "StringTypeAnnotation" ]; return ( node && flowTypeAnnotations.indexOf(node.type) !== -1 && !(node.type === "GenericTypeAnnotation" && node.typeParameters) ); } const functionCompositionFunctionNames = new Set([ "pipe", // RxJS, Ramda "pipeP", // Ramda "pipeK", // Ramda "compose", // Ramda, Redux "composeFlipped", // Not from any library, but common in Haskell, so supported "composeP", // Ramda "composeK", // Ramda "flow", // Lodash "flowRight", // Lodash "connect", // Redux "createSelector" // Reselect ]); function isFunctionCompositionFunction(node) { switch (node.type) { case "OptionalMemberExpression": case "MemberExpression": { return isFunctionCompositionFunction(node.property); } case "Identifier": { return functionCompositionFunctionNames.has(node.name); } case "StringLiteral": case "Literal": { return functionCompositionFunctionNames.has(node.value); } } } function printArgumentsList(path, options, print) { const node = path.getValue(); const args = node.arguments; if (args.length === 0) { return concat([ "(", comments.printDanglingComments(path, options, /* sameIndent */ true), ")" ]); } let anyArgEmptyLine = false; let hasEmptyLineFollowingFirstArg = false; const lastArgIndex = args.length - 1; const printedArguments = path.map((argPath, index) => { const arg = argPath.getNode(); const parts = [print(argPath)]; if (index === lastArgIndex) { // do nothing } else if (isNextLineEmpty(options.originalText, arg, options)) { if (index === 0) { hasEmptyLineFollowingFirstArg = true; } anyArgEmptyLine = true; parts.push(",", hardline, hardline); } else { parts.push(",", line); } return concat(parts); }, "arguments"); const maybeTrailingComma = shouldPrintComma(options, "all") ? "," : ""; function allArgsBrokenOut() { return group( concat([ "(", indent(concat([line, concat(printedArguments)])), maybeTrailingComma, line, ")" ]), { shouldBreak: true } ); } // We want to get // pipe( // x => x + 1, // x => x - 1 // ) // here, but not // process.stdout.pipe(socket) if (isFunctionCompositionFunction(node.callee) && args.length > 1) { return allArgsBrokenOut(); } const shouldGroupFirst = shouldGroupFirstArg(args); const shouldGroupLast = shouldGroupLastArg(args); if (shouldGroupFirst || shouldGroupLast) { const shouldBreak = (shouldGroupFirst ? printedArguments.slice(1).some(willBreak) : printedArguments.slice(0, -1).some(willBreak)) || anyArgEmptyLine; // We want to print the last argument with a special flag let printedExpanded; let i = 0; path.each(argPath => { if (shouldGroupFirst && i === 0) { printedExpanded = [ concat([ argPath.call(p => print(p, { expandFirstArg: true })), printedArguments.length > 1 ? "," : "", hasEmptyLineFollowingFirstArg ? hardline : line, hasEmptyLineFollowingFirstArg ? hardline : "" ]) ].concat(printedArguments.slice(1)); } if (shouldGroupLast && i === args.length - 1) { printedExpanded = printedArguments .slice(0, -1) .concat(argPath.call(p => print(p, { expandLastArg: true }))); } i++; }, "arguments"); const somePrintedArgumentsWillBreak = printedArguments.some(willBreak); return concat([ somePrintedArgumentsWillBreak ? breakParent : "", conditionalGroup( [ concat([ ifBreak( indent(concat(["(", softline, concat(printedExpanded)])), concat(["(", concat(printedExpanded)]) ), somePrintedArgumentsWillBreak ? concat([ifBreak(maybeTrailingComma), softline]) : "", ")" ]), shouldGroupFirst ? concat([ "(", group(printedExpanded[0], { shouldBreak: true }), concat(printedExpanded.slice(1)), ")" ]) : concat([ "(", concat(printedArguments.slice(0, -1)), group(getLast(printedExpanded), { shouldBreak: true }), ")" ]), allArgsBrokenOut() ], { shouldBreak } ) ]); } return group( concat([ "(", indent(concat([softline, concat(printedArguments)])), ifBreak(shouldPrintComma(options, "all") ? "," : ""), softline, ")" ]), { shouldBreak: printedArguments.some(willBreak) || anyArgEmptyLine } ); } function printTypeAnnotation(path, options, print) { const node = path.getValue(); if (!node.typeAnnotation) { return ""; } const parentNode = path.getParentNode(); const isDefinite = node.definite || (parentNode && parentNode.type === "VariableDeclarator" && parentNode.definite); const isFunctionDeclarationIdentifier = parentNode.type === "DeclareFunction" && parentNode.id === node; if ( isFlowAnnotationComment(options.originalText, node.typeAnnotation, options) ) { return concat([" /*: ", path.call(print, "typeAnnotation"), " */"]); } return concat([ isFunctionDeclarationIdentifier ? "" : isDefinite ? "!: " : ": ", path.call(print, "typeAnnotation") ]); } function printFunctionTypeParameters(path, options, print) { const fun = path.getValue(); if (fun.typeArguments) { return path.call(print, "typeArguments"); } if (fun.typeParameters) { return path.call(print, "typeParameters"); } return ""; } function printFunctionParams(path, print, options, expandArg, printTypeParams) { const fun = path.getValue(); const paramsField = fun.parameters ? "parameters" : "params"; const typeParams = printTypeParams ? printFunctionTypeParameters(path, options, print) : ""; let printed = []; if (fun[paramsField]) { printed = path.map(print, paramsField); } if (fun.rest) { printed.push(concat(["...", path.call(print, "rest")])); } if (printed.length === 0) { return concat([ typeParams, "(", comments.printDanglingComments( path, options, /* sameIndent */ true, comment => getNextNonSpaceNonCommentCharacter( options.originalText, comment, options.locEnd ) === ")" ), ")" ]); } const lastParam = getLast(fun[paramsField]); // If the parent is a call with the first/last argument expansion and this is the // params of the first/last argument, we dont want the arguments to break and instead // want the whole expression to be on a new line. // // Good: Bad: // verylongcall( verylongcall(( // (a, b) => { a, // } b, // }) ) => { // }) if ( expandArg && !(fun[paramsField] && fun[paramsField].some(n => n.comments)) ) { return group( concat([ removeLines(typeParams), "(", join(", ", printed.map(removeLines)), ")" ]) ); } // Single object destructuring should hug // // function({ // a, // b, // c // }) {} if (shouldHugArguments(fun)) { return concat([typeParams, "(", join(", ", printed), ")"]); } const parent = path.getParentNode(); // don't break in specs, eg; `it("should maintain parens around done even when long", (done) => {})` if (isTestCall(parent)) { return concat([typeParams, "(", join(", ", printed), ")"]); } const isFlowShorthandWithOneArg = (isObjectTypePropertyAFunction(parent, options) || isTypeAnnotationAFunction(parent, options) || parent.type === "TypeAlias" || parent.type === "UnionTypeAnnotation" || parent.type === "TSUnionType" || parent.type === "IntersectionTypeAnnotation" || (parent.type === "FunctionTypeAnnotation" && parent.returnType === fun)) && fun[paramsField].length === 1 && fun[paramsField][0].name === null && fun[paramsField][0].typeAnnotation && fun.typeParameters === null && isSimpleFlowType(fun[paramsField][0].typeAnnotation) && !fun.rest; if (isFlowShorthandWithOneArg) { if (options.arrowParens === "always") { return concat(["(", concat(printed), ")"]); } return concat(printed); } const canHaveTrailingComma = !(lastParam && lastParam.type === "RestElement") && !fun.rest; return concat([ typeParams, "(", indent(concat([softline, join(concat([",", line]), printed)])), ifBreak( canHaveTrailingComma && shouldPrintComma(options, "all") ? "," : "" ), softline, ")" ]); } function shouldPrintParamsWithoutParens(path, options) { if (options.arrowParens === "always") { return false; } if (options.arrowParens === "avoid") { const node = path.getValue(); return canPrintParamsWithoutParens(node); } // Fallback default; should be unreachable return false; } function canPrintParamsWithoutParens(node) { return ( node.params.length === 1 && !node.rest && !node.typeParameters && !hasDanglingComments(node) && node.params[0].type === "Identifier" && !node.params[0].typeAnnotation && !node.params[0].comments && !node.params[0].optional && !node.predicate && !node.returnType ); } function printFunctionDeclaration(path, print, options) { const n = path.getValue(); const parts = []; if (n.async) { parts.push("async "); } parts.push("function"); if (n.generator) { parts.push("*"); } if (n.id) { parts.push(" ", path.call(print, "id")); } parts.push( printFunctionTypeParameters(path, options, print), group( concat([ printFunctionParams(path, print, options), printReturnType(path, print, options) ]) ), n.body ? " " : "", path.call(print, "body") ); return concat(parts); } function printObjectMethod(path, options, print) { const objMethod = path.getValue(); const parts = []; if (objMethod.async) { parts.push("async "); } if (objMethod.generator) { parts.push("*"); } if ( objMethod.method || objMethod.kind === "get" || objMethod.kind === "set" ) { return printMethod(path, options, print); } const key = printPropertyKey(path, options, print); if (objMethod.computed) { parts.push("[", key, "]"); } else { parts.push(key); } parts.push( printFunctionTypeParameters(path, options, print), group( concat([ printFunctionParams(path, print, options), printReturnType(path, print, options) ]) ), " ", path.call(print, "body") ); return concat(parts); } function printReturnType(path, print, options) { const n = path.getValue(); const returnType = path.call(print, "returnType"); if ( n.returnType && isFlowAnnotationComment(options.originalText, n.returnType, options) ) { return concat([" /*: ", returnType, " */"]); } const parts = [returnType]; // prepend colon to TypeScript type annotation if (n.returnType && n.returnType.typeAnnotation) { parts.unshift(": "); } if (n.predicate) { // The return type will already add the colon, but otherwise we // need to do it ourselves parts.push(n.returnType ? " " : ": ", path.call(print, "predicate")); } return concat(parts); } function printExportDeclaration(path, options, print) { const decl = path.getValue(); const semi = options.semi ? ";" : ""; const parts = ["export "]; const isDefault = decl["default"] || decl.type === "ExportDefaultDeclaration"; if (isDefault) { parts.push("default "); } parts.push( comments.printDanglingComments(path, options, /* sameIndent */ true) ); if (needsHardlineAfterDanglingComment(decl)) { parts.push(hardline); } if (decl.declaration) { parts.push(path.call(print, "declaration")); if ( isDefault && (decl.declaration.type !== "ClassDeclaration" && decl.declaration.type !== "FunctionDeclaration" && decl.declaration.type !== "TSAbstractClassDeclaration" && decl.declaration.type !== "TSInterfaceDeclaration" && decl.declaration.type !== "DeclareClass" && decl.declaration.type !== "DeclareFunction") ) { parts.push(semi); } } else { if (decl.specifiers && decl.specifiers.length > 0) { const specifiers = []; const defaultSpecifiers = []; const namespaceSpecifiers = []; path.each(specifierPath => { const specifierType = path.getValue().type; if (specifierType === "ExportSpecifier") { specifiers.push(print(specifierPath)); } else if (specifierType === "ExportDefaultSpecifier") { defaultSpecifiers.push(print(specifierPath)); } else if (specifierType === "ExportNamespaceSpecifier") { namespaceSpecifiers.push(concat(["* as ", print(specifierPath)])); } }, "specifiers"); const isNamespaceFollowed = namespaceSpecifiers.length !== 0 && specifiers.length !== 0; const isDefaultFollowed = defaultSpecifiers.length !== 0 && (namespaceSpecifiers.length !== 0 || specifiers.length !== 0); parts.push( decl.exportKind === "type" ? "type " : "", concat(defaultSpecifiers), concat([isDefaultFollowed ? ", " : ""]), concat(namespaceSpecifiers), concat([isNamespaceFollowed ? ", " : ""]), specifiers.length !== 0 ? group( concat([ "{", indent( concat([ options.bracketSpacing ? line : softline, join(concat([",", line]), specifiers) ]) ), ifBreak(shouldPrintComma(options) ? "," : ""), options.bracketSpacing ? line : softline, "}" ]) ) : "" ); } else { parts.push("{}"); } if (decl.source) { parts.push(" from ", path.call(print, "source")); } parts.push(semi); } return concat(parts); } function printFlowDeclaration(path, parts) { const parentExportDecl = getParentExportDeclaration(path); if (parentExportDecl) { assert.strictEqual(parentExportDecl.type, "DeclareExportDeclaration"); } else { // If the parent node has type DeclareExportDeclaration, then it // will be responsible for printing the "declare" token. Otherwise // it needs to be printed with this non-exported declaration node. parts.unshift("declare "); } return concat(parts); } function getFlowVariance(path) { if (!path.variance) { return null; } // Babylon 7.0 currently uses variance node type, and flow should // follow suit soon: // https://github.com/babel/babel/issues/4722 const variance = path.variance.kind || path.variance; switch (variance) { case "plus": return "+"; case "minus": return "-"; default: /* istanbul ignore next */ return variance; } } function printTypeScriptModifiers(path, options, print) { const n = path.getValue(); if (!n.modifiers || !n.modifiers.length) { return ""; } return concat([join(" ", path.map(print, "modifiers")), " "]); } function printTypeParameters(path, options, print, paramsKey) { const n = path.getValue(); if (!n[paramsKey]) { return ""; } // for TypeParameterDeclaration typeParameters is a single node if (!Array.isArray(n[paramsKey])) { return path.call(print, paramsKey); } const grandparent = path.getNode(2); const isParameterInTestCall = grandparent != null && isTestCall(grandparent); const shouldInline = isParameterInTestCall || n[paramsKey].length === 0 || (n[paramsKey].length === 1 && (shouldHugType(n[paramsKey][0]) || (n[paramsKey][0].type === "GenericTypeAnnotation" && shouldHugType(n[paramsKey][0].id)) || (n[paramsKey][0].type === "TSTypeReference" && shouldHugType(n[paramsKey][0].typeName)) || n[paramsKey][0].type === "NullableTypeAnnotation")); if (shouldInline) { return concat(["<", join(", ", path.map(print, paramsKey)), ">"]); } return group( concat([ "<", indent( concat([ softline, join(concat([",", line]), path.map(print, paramsKey)) ]) ), ifBreak( options.parser !== "typescript" && shouldPrintComma(options, "all") ? "," : "" ), softline, ">" ]) ); } function printClass(path, options, print) { const n = path.getValue(); const parts = []; if (n.type === "TSAbstractClassDeclaration") { parts.push("abstract "); } parts.push("class"); if (n.id) { parts.push(" ", path.call(print, "id")); } parts.push(path.call(print, "typeParameters")); const partsGroup = []; if (n.superClass) { const printed = concat([ "extends ", path.call(print, "superClass"), path.call(print, "superTypeParameters") ]); // Keep old behaviour of extends in same line // If there is only on extends and there are not comments if ( (!n.implements || n.implements.length === 0) && (!n.superClass.comments || n.superClass.comments.length === 0) ) { parts.push( concat([ " ", path.call( superClass => comments.printComments(superClass, () => printed, options), "superClass" ) ]) ); } else { partsGroup.push( group( concat([ line, path.call( superClass => comments.printComments(superClass, () => printed, options), "superClass" ) ]) ) ); } } else if (n.extends && n.extends.length > 0) { parts.push(" extends ", join(", ", path.map(print, "extends"))); } if (n["mixins"] && n["mixins"].length > 0) { partsGroup.push( line, "mixins ", group(indent(join(concat([",", line]), path.map(print, "mixins")))) ); } if (n["implements"] && n["implements"].length > 0) { partsGroup.push( line, "implements", group( indent( concat([ line, join(concat([",", line]), path.map(print, "implements")) ]) ) ) ); } if (partsGroup.length > 0) { parts.push(group(indent(concat(partsGroup)))); } if ( n.body && n.body.comments && hasLeadingOwnLineComment(options.originalText, n.body, options) ) { parts.push(hardline); } else { parts.push(" "); } parts.push(path.call(print, "body")); return parts; } function printOptionalToken(path) { const node = path.getValue(); if (!node.optional) { return ""; } if ( node.type === "OptionalCallExpression" || (node.type === "OptionalMemberExpression" && node.computed) ) { return "?."; } return "?"; } function printMemberLookup(path, options, print) { const property = path.call(print, "property"); const n = path.getValue(); const optional = printOptionalToken(path); if (!n.computed) { return concat([optional, ".", property]); } if (!n.property || isNumericLiteral(n.property)) { return concat([optional, "[", property, "]"]); } return group( concat([optional, "[", indent(concat([softline, property])), softline, "]"]) ); } function printBindExpressionCallee(path, options, print) { return concat(["::", path.call(print, "callee")]); } // We detect calls on member expressions specially to format a // common pattern better. The pattern we are looking for is this: // // arr // .map(x => x + 1) // .filter(x => x > 10) // .some(x => x % 2) // // The way it is structured in the AST is via a nested sequence of // MemberExpression and CallExpression. We need to traverse the AST // and make groups out of it to print it in the desired way. function printMemberChain(path, options, print) { // The first phase is to linearize the AST by traversing it down. // // a().b() // has the following AST structure: // CallExpression(MemberExpression(CallExpression(Identifier))) // and we transform it into // [Identifier, CallExpression, MemberExpression, CallExpression] const printedNodes = []; // Here we try to retain one typed empty line after each call expression or // the first group whether it is in parentheses or not function shouldInsertEmptyLineAfter(node) { const originalText = options.originalText; const nextCharIndex = getNextNonSpaceNonCommentCharacterIndex( originalText, node, options ); const nextChar = originalText.charAt(nextCharIndex); // if it is cut off by a parenthesis, we only account for one typed empty // line after that parenthesis if (nextChar == ")") { return isNextLineEmptyAfterIndex( originalText, nextCharIndex + 1, options ); } return isNextLineEmpty(originalText, node, options); } function rec(path) { const node = path.getValue(); if ( (node.type === "CallExpression" || node.type === "OptionalCallExpression") && (isMemberish(node.callee) || node.callee.type === "CallExpression" || node.callee.type === "OptionalCallExpression") ) { printedNodes.unshift({ node: node, printed: concat([ comments.printComments( path, () => concat([ printOptionalToken(path), printFunctionTypeParameters(path, options, print), printArgumentsList(path, options, print) ]), options ), shouldInsertEmptyLineAfter(node) ? hardline : "" ]) }); path.call(callee => rec(callee), "callee"); } else if (isMemberish(node)) { printedNodes.unshift({ node: node, needsParens: pathNeedsParens(path, options), printed: comments.printComments( path, () => node.type === "OptionalMemberExpression" || node.type === "MemberExpression" ? printMemberLookup(path, options, print) : printBindExpressionCallee(path, options, print), options ) }); path.call(object => rec(object), "object"); } else if (node.type === "TSNonNullExpression") { printedNodes.unshift({ node: node, printed: comments.printComments(path, () => "!", options) }); path.call(expression => rec(expression), "expression"); } else { printedNodes.unshift({ node: node, printed: path.call(print) }); } } // Note: the comments of the root node have already been printed, so we // need to extract this first call without printing them as they would // if handled inside of the recursive call. const node = path.getValue(); printedNodes.unshift({ node, printed: concat([ printOptionalToken(path), printFunctionTypeParameters(path, options, print), printArgumentsList(path, options, print) ]) }); path.call(callee => rec(callee), "callee"); // Once we have a linear list of printed nodes, we want to create groups out // of it. // // a().b.c().d().e // will be grouped as // [ // [Identifier, CallExpression], // [MemberExpression, MemberExpression, CallExpression], // [MemberExpression, CallExpression], // [MemberExpression], // ] // so that we can print it as // a() // .b.c() // .d() // .e // The first group is the first node followed by // - as many CallExpression as possible // < fn()()() >.something() // - as many array acessors as possible // < fn()[0][1][2] >.something() // - then, as many MemberExpression as possible but the last one // < this.items >.something() const groups = []; let currentGroup = [printedNodes[0]]; let i = 1; for (; i < printedNodes.length; ++i) { if ( printedNodes[i].node.type === "TSNonNullExpression" || printedNodes[i].node.type === "OptionalCallExpression" || printedNodes[i].node.type === "CallExpression" || ((printedNodes[i].node.type === "MemberExpression" || printedNodes[i].node.type === "OptionalMemberExpression") && printedNodes[i].node.computed && isNumericLiteral(printedNodes[i].node.property)) ) { currentGroup.push(printedNodes[i]); } else { break; } } if ( printedNodes[0].node.type !== "CallExpression" && printedNodes[0].node.type !== "OptionalCallExpression" ) { for (; i + 1 < printedNodes.length; ++i) { if ( isMemberish(printedNodes[i].node) && isMemberish(printedNodes[i + 1].node) ) { currentGroup.push(printedNodes[i]); } else { break; } } } groups.push(currentGroup); currentGroup = []; // Then, each following group is a sequence of MemberExpression followed by // a sequence of CallExpression. To compute it, we keep adding things to the // group until we has seen a CallExpression in the past and reach a // MemberExpression let hasSeenCallExpression = false; for (; i < printedNodes.length; ++i) { if (hasSeenCallExpression && isMemberish(printedNodes[i].node)) { // [0] should be appended at the end of the group instead of the // beginning of the next one if ( printedNodes[i].node.computed && isNumericLiteral(printedNodes[i].node.property) ) { currentGroup.push(printedNodes[i]); continue; } groups.push(currentGroup); currentGroup = []; hasSeenCallExpression = false; } if ( printedNodes[i].node.type === "CallExpression" || printedNodes[i].node.type === "OptionalCallExpression" ) { hasSeenCallExpression = true; } currentGroup.push(printedNodes[i]); if ( printedNodes[i].node.comments && printedNodes[i].node.comments.some(comment => comment.trailing) ) { groups.push(currentGroup); currentGroup = []; hasSeenCallExpression = false; } } if (currentGroup.length > 0) { groups.push(currentGroup); } // There are cases like Object.keys(), Observable.of(), _.values() where // they are the subject of all the chained calls and therefore should // be kept on the same line: // // Object.keys(items) // .filter(x => x) // .map(x => x) // // In order to detect those cases, we use an heuristic: if the first // node is an identifier with the name starting with a capital // letter or just a sequence of _$. The rationale is that they are // likely to be factories. function isFactory(name) { return /^[A-Z]|^[_$]+$/.test(name); } // In case the Identifier is shorter than tab width, we can keep the // first call in a single line, if it's an ExpressionStatement. // // d3.scaleLinear() // .domain([0, 100]) // .range([0, width]); // function isShort(name) { return name.length <= options.tabWidth; } function shouldNotWrap(groups) { const parent = path.getParentNode(); const isExpression = parent && parent.type === "ExpressionStatement"; const hasComputed = groups[1].length && groups[1][0].node.computed; if (groups[0].length === 1) { const firstNode = groups[0][0].node; return ( firstNode.type === "ThisExpression" || (firstNode.type === "Identifier" && (isFactory(firstNode.name) || (isExpression && isShort(firstNode.name)) || hasComputed)) ); } const lastNode = getLast(groups[0]).node; return ( (lastNode.type === "MemberExpression" || lastNode.type === "OptionalMemberExpression") && lastNode.property.type === "Identifier" && (isFactory(lastNode.property.name) || hasComputed) ); } const shouldMerge = groups.length >= 2 && !groups[1][0].node.comments && shouldNotWrap(groups); function printGroup(printedGroup) { const result = []; for (let i = 0; i < printedGroup.length; i++) { // Checks if the next node (i.e. the parent node) needs parens // and print accordingly if (printedGroup[i + 1] && printedGroup[i + 1].needsParens) { result.push( "(", printedGroup[i].printed, printedGroup[i + 1].printed, ")" ); i++; } else { result.push(printedGroup[i].printed); } } return concat(result); } function printIndentedGroup(groups) { if (groups.length === 0) { return ""; } return indent( group(concat([hardline, join(hardline, groups.map(printGroup))])) ); } const printedGroups = groups.map(printGroup); const oneLine = concat(printedGroups); const cutoff = shouldMerge ? 3 : 2; const flatGroups = groups .slice(0, cutoff) .reduce((res, group) => res.concat(group), []); const hasComment = flatGroups.slice(1, -1).some(node => hasLeadingComment(node.node)) || flatGroups.slice(0, -1).some(node => hasTrailingComment(node.node)) || (groups[cutoff] && hasLeadingComment(groups[cutoff][0].node)); // If we only have a single `.`, we shouldn't do anything fancy and just // render everything concatenated together. if (groups.length <= cutoff && !hasComment) { return group(oneLine); } // Find out the last node in the first group and check if it has an // empty line after const lastNodeBeforeIndent = getLast( shouldMerge ? groups.slice(1, 2)[0] : groups[0] ).node; const shouldHaveEmptyLineBeforeIndent = lastNodeBeforeIndent.type !== "CallExpression" && lastNodeBeforeIndent.type !== "OptionalCallExpression" && shouldInsertEmptyLineAfter(lastNodeBeforeIndent); const expanded = concat([ printGroup(groups[0]), shouldMerge ? concat(groups.slice(1, 2).map(printGroup)) : "", shouldHaveEmptyLineBeforeIndent ? hardline : "", printIndentedGroup(groups.slice(shouldMerge ? 2 : 1)) ]); const callExpressions = printedNodes .map(({ node }) => node) .filter(isCallOrOptionalCallExpression); // We don't want to print in one line if there's: // * A comment. // * 3 or more chained calls. // * Any group but the last one has a hard line. // If the last group is a function it's okay to inline if it fits. if ( hasComment || callExpressions.length >= 3 || printedGroups.slice(0, -1).some(willBreak) || /** * scopes.filter(scope => scope.value !== '').map((scope, i) => { * // multi line content * }) */ (((lastGroupDoc, lastGroupNode) => isCallOrOptionalCallExpression(lastGroupNode) && willBreak(lastGroupDoc))( getLast(printedGroups), getLast(getLast(groups)).node ) && callExpressions .slice(0, -1) .some(n => n.arguments.some(isFunctionOrArrowExpression))) ) { return group(expanded); } return concat([ // We only need to check `oneLine` because if `expanded` is chosen // that means that the parent group has already been broken // naturally willBreak(oneLine) || shouldHaveEmptyLineBeforeIndent ? breakParent : "", conditionalGroup([oneLine, expanded]) ]); } function isCallOrOptionalCallExpression(node) { return ( node.type === "CallExpression" || node.type === "OptionalCallExpression" ); } function isJSXNode(node) { return node.type === "JSXElement" || node.type === "JSXFragment"; } function isEmptyJSXElement(node) { if (node.children.length === 0) { return true; } if (node.children.length > 1) { return false; } // if there is one text child and does not contain any meaningful text // we can treat the element as empty. const child = node.children[0]; return isLiteral(child) && !isMeaningfulJSXText(child); } // Only space, newline, carriage return, and tab are treated as whitespace // inside JSX. const jsxWhitespaceChars = " \n\r\t"; const containsNonJsxWhitespaceRegex = new RegExp( "[^" + jsxWhitespaceChars + "]" ); const matchJsxWhitespaceRegex = new RegExp("([" + jsxWhitespaceChars + "]+)"); // Meaningful if it contains non-whitespace characters, // or it contains whitespace without a new line. function isMeaningfulJSXText(node) { return ( isLiteral(node) && (containsNonJsxWhitespaceRegex.test(rawText(node)) || !/\n/.test(rawText(node))) ); } function conditionalExpressionChainContainsJSX(node) { return Boolean(getConditionalChainContents(node).find(isJSXNode)); } // If we have nested conditional expressions, we want to print them in JSX mode // if there's at least one JSXElement somewhere in the tree. // // A conditional expression chain like this should be printed in normal mode, // because there aren't JSXElements anywhere in it: // // isA ? "A" : isB ? "B" : isC ? "C" : "Unknown"; // // But a conditional expression chain like this should be printed in JSX mode, // because there is a JSXElement in the last ConditionalExpression: // // isA ? "A" : isB ? "B" : isC ? "C" : Unknown; // // This type of ConditionalExpression chain is structured like this in the AST: // // ConditionalExpression { // test: ..., // consequent: ..., // alternate: ConditionalExpression { // test: ..., // consequent: ..., // alternate: ConditionalExpression { // test: ..., // consequent: ..., // alternate: ..., // } // } // } // // We want to traverse over that shape and convert it into a flat structure so // that we can find if there's a JSXElement somewhere inside. function getConditionalChainContents(node) { // Given this code: // // // Using a ConditionalExpression as the consequent is uncommon, but should // // be handled. // A ? B : C ? D : E ? F ? G : H : I // // which has this AST: // // ConditionalExpression { // test: Identifier(A), // consequent: Identifier(B), // alternate: ConditionalExpression { // test: Identifier(C), // consequent: Identifier(D), // alternate: ConditionalExpression { // test: Identifier(E), // consequent: ConditionalExpression { // test: Identifier(F), // consequent: Identifier(G), // alternate: Identifier(H), // }, // alternate: Identifier(I), // } // } // } // // we should return this Array: // // [ // Identifier(A), // Identifier(B), // Identifier(C), // Identifier(D), // Identifier(E), // Identifier(F), // Identifier(G), // Identifier(H), // Identifier(I) // ]; // // This loses the information about whether each node was the test, // consequent, or alternate, but we don't care about that here- we are only // flattening this structure to find if there's any JSXElements inside. const nonConditionalExpressions = []; function recurse(node) { if (node.type === "ConditionalExpression") { recurse(node.test); recurse(node.consequent); recurse(node.alternate); } else { nonConditionalExpressions.push(node); } } recurse(node); return nonConditionalExpressions; } // Detect an expression node representing `{" "}` function isJSXWhitespaceExpression(node) { return ( node.type === "JSXExpressionContainer" && isLiteral(node.expression) && node.expression.value === " " && !node.expression.comments ); } function separatorNoWhitespace( isFacebookTranslationTag, child, childNode, nextNode ) { if (isFacebookTranslationTag) { return ""; } if ( (childNode.type === "JSXElement" && !childNode.closingElement) || (nextNode && (nextNode.type === "JSXElement" && !nextNode.closingElement)) ) { return child.length === 1 ? softline : hardline; } return softline; } function separatorWithWhitespace( isFacebookTranslationTag, child, childNode, nextNode ) { if (isFacebookTranslationTag) { return hardline; } if (child.length === 1) { return (childNode.type === "JSXElement" && !childNode.closingElement) || (nextNode && nextNode.type === "JSXElement" && !nextNode.closingElement) ? hardline : softline; } return hardline; } // JSX Children are strange, mostly for two reasons: // 1. JSX reads newlines into string values, instead of skipping them like JS // 2. up to one whitespace between elements within a line is significant, // but not between lines. // // Leading, trailing, and lone whitespace all need to // turn themselves into the rather ugly `{' '}` when breaking. // // We print JSX using the `fill` doc primitive. // This requires that we give it an array of alternating // content and whitespace elements. // To ensure this we add dummy `""` content elements as needed. function printJSXChildren( path, options, print, jsxWhitespace, isFacebookTranslationTag ) { const n = path.getValue(); const children = []; // using `map` instead of `each` because it provides `i` path.map((childPath, i) => { const child = childPath.getValue(); if (isLiteral(child)) { const text = rawText(child); // Contains a non-whitespace character if (isMeaningfulJSXText(child)) { const words = text.split(matchJsxWhitespaceRegex); // Starts with whitespace if (words[0] === "") { children.push(""); words.shift(); if (/\n/.test(words[0])) { const next = n.children[i + 1]; children.push( separatorWithWhitespace( isFacebookTranslationTag, words[1], child, next ) ); } else { children.push(jsxWhitespace); } words.shift(); } let endWhitespace; // Ends with whitespace if (getLast(words) === "") { words.pop(); endWhitespace = words.pop(); } // This was whitespace only without a new line. if (words.length === 0) { return; } words.forEach((word, i) => { if (i % 2 === 1) { children.push(line); } else { children.push(word); } }); if (endWhitespace !== undefined) { if (/\n/.test(endWhitespace)) { const next = n.children[i + 1]; children.push( separatorWithWhitespace( isFacebookTranslationTag, getLast(children), child, next ) ); } else { children.push(jsxWhitespace); } } else { const next = n.children[i + 1]; children.push( separatorNoWhitespace( isFacebookTranslationTag, getLast(children), child, next ) ); } } else if (/\n/.test(text)) { // Keep (up to one) blank line between tags/expressions/text. // Note: We don't keep blank lines between text elements. if (text.match(/\n/g).length > 1) { children.push(""); children.push(hardline); } } else { children.push(""); children.push(jsxWhitespace); } } else { const printedChild = print(childPath); children.push(printedChild); const next = n.children[i + 1]; const directlyFollowedByMeaningfulText = next && isMeaningfulJSXText(next); if (directlyFollowedByMeaningfulText) { const firstWord = rawText(next) .trim() .split(matchJsxWhitespaceRegex)[0]; children.push( separatorNoWhitespace( isFacebookTranslationTag, firstWord, child, next ) ); } else { children.push(hardline); } } }, "children"); return children; } // JSX expands children from the inside-out, instead of the outside-in. // This is both to break children before attributes, // and to ensure that when children break, their parents do as well. // // Any element that is written without any newlines and fits on a single line // is left that way. // Not only that, any user-written-line containing multiple JSX siblings // should also be kept on one line if possible, // so each user-written-line is wrapped in its own group. // // Elements that contain newlines or don't fit on a single line (recursively) // are fully-split, using hardline and shouldBreak: true. // // To support that case properly, all leading and trailing spaces // are stripped from the list of children, and replaced with a single hardline. function printJSXElement(path, options, print) { const n = path.getValue(); // Turn
into
if (n.type === "JSXElement" && isEmptyJSXElement(n)) { n.openingElement.selfClosing = true; return path.call(print, "openingElement"); } const openingLines = n.type === "JSXElement" ? path.call(print, "openingElement") : path.call(print, "openingFragment"); const closingLines = n.type === "JSXElement" ? path.call(print, "closingElement") : path.call(print, "closingFragment"); if ( n.children.length === 1 && n.children[0].type === "JSXExpressionContainer" && (n.children[0].expression.type === "TemplateLiteral" || n.children[0].expression.type === "TaggedTemplateExpression") ) { return concat([ openingLines, concat(path.map(print, "children")), closingLines ]); } // Convert `{" "}` to text nodes containing a space. // This makes it easy to turn them into `jsxWhitespace` which // can then print as either a space or `{" "}` when breaking. n.children = n.children.map(child => { if (isJSXWhitespaceExpression(child)) { return { type: "JSXText", value: " ", raw: " " }; } return child; }); const containsTag = n.children.filter(isJSXNode).length > 0; const containsMultipleExpressions = n.children.filter(child => child.type === "JSXExpressionContainer").length > 1; const containsMultipleAttributes = n.type === "JSXElement" && n.openingElement.attributes.length > 1; // Record any breaks. Should never go from true to false, only false to true. let forcedBreak = willBreak(openingLines) || containsTag || containsMultipleAttributes || containsMultipleExpressions; const rawJsxWhitespace = options.singleQuote ? "{' '}" : '{" "}'; const jsxWhitespace = ifBreak(concat([rawJsxWhitespace, softline]), " "); const isFacebookTranslationTag = n.openingElement && n.openingElement.name && n.openingElement.name.name === "fbt"; const children = printJSXChildren( path, options, print, jsxWhitespace, isFacebookTranslationTag ); const containsText = n.children.filter(child => isMeaningfulJSXText(child)).length > 0; // We can end up we multiple whitespace elements with empty string // content between them. // We need to remove empty whitespace and softlines before JSX whitespace // to get the correct output. for (let i = children.length - 2; i >= 0; i--) { const isPairOfEmptyStrings = children[i] === "" && children[i + 1] === ""; const isPairOfHardlines = children[i] === hardline && children[i + 1] === "" && children[i + 2] === hardline; const isLineFollowedByJSXWhitespace = (children[i] === softline || children[i] === hardline) && children[i + 1] === "" && children[i + 2] === jsxWhitespace; const isJSXWhitespaceFollowedByLine = children[i] === jsxWhitespace && children[i + 1] === "" && (children[i + 2] === softline || children[i + 2] === hardline); const isDoubleJSXWhitespace = children[i] === jsxWhitespace && children[i + 1] === "" && children[i + 2] === jsxWhitespace; const isPairOfHardOrSoftLines = (children[i] === softline && children[i + 1] === "" && children[i + 2] === hardline) || (children[i] === hardline && children[i + 1] === "" && children[i + 2] === softline); if ( (isPairOfHardlines && containsText) || isPairOfEmptyStrings || isLineFollowedByJSXWhitespace || isDoubleJSXWhitespace || isPairOfHardOrSoftLines ) { children.splice(i, 2); } else if (isJSXWhitespaceFollowedByLine) { children.splice(i + 1, 2); } } // Trim trailing lines (or empty strings) while ( children.length && (isLineNext(getLast(children)) || isEmpty(getLast(children))) ) { children.pop(); } // Trim leading lines (or empty strings) while ( children.length && (isLineNext(children[0]) || isEmpty(children[0])) && (isLineNext(children[1]) || isEmpty(children[1])) ) { children.shift(); children.shift(); } // Tweak how we format children if outputting this element over multiple lines. // Also detect whether we will force this element to output over multiple lines. const multilineChildren = []; children.forEach((child, i) => { // There are a number of situations where we need to ensure we display // whitespace as `{" "}` when outputting this element over multiple lines. if (child === jsxWhitespace) { if (i === 1 && children[i - 1] === "") { if (children.length === 2) { // Solitary whitespace multilineChildren.push(rawJsxWhitespace); return; } // Leading whitespace multilineChildren.push(concat([rawJsxWhitespace, hardline])); return; } else if (i === children.length - 1) { // Trailing whitespace multilineChildren.push(rawJsxWhitespace); return; } else if (children[i - 1] === "" && children[i - 2] === hardline) { // Whitespace after line break multilineChildren.push(rawJsxWhitespace); return; } } multilineChildren.push(child); if (willBreak(child)) { forcedBreak = true; } }); // If there is text we use `fill` to fit as much onto each line as possible. // When there is no text (just tags and expressions) we use `group` // to output each on a separate line. const content = containsText ? fill(multilineChildren) : group(concat(multilineChildren), { shouldBreak: true }); const multiLineElem = group( concat([ openingLines, indent(concat([hardline, content])), hardline, closingLines ]) ); if (forcedBreak) { return multiLineElem; } return conditionalGroup([ group(concat([openingLines, concat(children), closingLines])), multiLineElem ]); } function maybeWrapJSXElementInParens(path, elem) { const parent = path.getParentNode(); if (!parent) { return elem; } const NO_WRAP_PARENTS = { ArrayExpression: true, JSXAttribute: true, JSXElement: true, JSXExpressionContainer: true, JSXFragment: true, ExpressionStatement: true, CallExpression: true, OptionalCallExpression: true, ConditionalExpression: true, JsExpressionRoot: true }; if (NO_WRAP_PARENTS[parent.type]) { return elem; } const shouldBreak = matchAncestorTypes(path, [ "ArrowFunctionExpression", "CallExpression", "JSXExpressionContainer" ]); return group( concat([ ifBreak("("), indent(concat([softline, elem])), softline, ifBreak(")") ]), { shouldBreak } ); } function isBinaryish(node) { return ( node.type === "BinaryExpression" || node.type === "LogicalExpression" || node.type === "NGPipeExpression" ); } function isMemberish(node) { return ( node.type === "MemberExpression" || node.type === "OptionalMemberExpression" || (node.type === "BindExpression" && node.object) ); } function shouldInlineLogicalExpression(node) { if (node.type !== "LogicalExpression") { return false; } if ( node.right.type === "ObjectExpression" && node.right.properties.length !== 0 ) { return true; } if ( node.right.type === "ArrayExpression" && node.right.elements.length !== 0 ) { return true; } if (isJSXNode(node.right)) { return true; } return false; } // For binary expressions to be consistent, we need to group // subsequent operators with the same precedence level under a single // group. Otherwise they will be nested such that some of them break // onto new lines but not all. Operators with the same precedence // level should either all break or not. Because we group them by // precedence level and the AST is structured based on precedence // level, things are naturally broken up correctly, i.e. `&&` is // broken before `+`. function printBinaryishExpressions( path, print, options, isNested, isInsideParenthesis ) { let parts = []; const node = path.getValue(); // We treat BinaryExpression and LogicalExpression nodes the same. if (isBinaryish(node)) { // Put all operators with the same precedence level in the same // group. The reason we only need to do this with the `left` // expression is because given an expression like `1 + 2 - 3`, it // is always parsed like `((1 + 2) - 3)`, meaning the `left` side // is where the rest of the expression will exist. Binary // expressions on the right side mean they have a difference // precedence level and should be treated as a separate group, so // print them normally. (This doesn't hold for the `**` operator, // which is unique in that it is right-associative.) if (shouldFlatten(node.operator, node.left.operator)) { // Flatten them out by recursively calling this function. parts = parts.concat( path.call( left => printBinaryishExpressions( left, print, options, /* isNested */ true, isInsideParenthesis ), "left" ) ); } else { parts.push(path.call(print, "left")); } const shouldInline = shouldInlineLogicalExpression(node); const lineBeforeOperator = (node.operator === "|>" || node.type === "NGPipeExpression" || (node.operator === "|" && options.parser === "__vue_expression")) && !hasLeadingOwnLineComment(options.originalText, node.right, options); const operator = node.type === "NGPipeExpression" ? "|" : node.operator; const rightSuffix = node.type === "NGPipeExpression" && node.arguments.length !== 0 ? group( indent( concat([ softline, ": ", join( concat([softline, ":", ifBreak(" ")]), path.map(print, "arguments").map(arg => align(2, group(arg))) ) ]) ) ) : ""; const right = shouldInline ? concat([operator, " ", path.call(print, "right"), rightSuffix]) : concat([ lineBeforeOperator ? softline : "", operator, lineBeforeOperator ? " " : line, path.call(print, "right"), rightSuffix ]); // If there's only a single binary expression, we want to create a group // in order to avoid having a small right part like -1 be on its own line. const parent = path.getParentNode(); const shouldGroup = !(isInsideParenthesis && node.type === "LogicalExpression") && parent.type !== node.type && node.left.type !== node.type && node.right.type !== node.type; parts.push(" ", shouldGroup ? group(right) : right); // The root comments are already printed, but we need to manually print // the other ones since we don't call the normal print on BinaryExpression, // only for the left and right parts if (isNested && node.comments) { parts = comments.printComments(path, () => concat(parts), options); } } else { // Our stopping case. Simply print the node normally. parts.push(path.call(print)); } return parts; } function printAssignmentRight(leftNode, rightNode, printedRight, options) { if (hasLeadingOwnLineComment(options.originalText, rightNode, options)) { return indent(concat([hardline, printedRight])); } const canBreak = (isBinaryish(rightNode) && !shouldInlineLogicalExpression(rightNode)) || (rightNode.type === "ConditionalExpression" && isBinaryish(rightNode.test) && !shouldInlineLogicalExpression(rightNode.test)) || rightNode.type === "StringLiteralTypeAnnotation" || (rightNode.type === "ClassExpression" && rightNode.decorators && rightNode.decorators.length) || ((leftNode.type === "Identifier" || isStringLiteral(leftNode) || leftNode.type === "MemberExpression") && (isStringLiteral(rightNode) || isMemberExpressionChain(rightNode)) && // do not put values on a separate line from the key in json options.parser !== "json" && options.parser !== "json5"); if (canBreak) { return group(indent(concat([line, printedRight]))); } return concat([" ", printedRight]); } function printAssignment( leftNode, printedLeft, operator, rightNode, printedRight, options ) { if (!rightNode) { return printedLeft; } const printed = printAssignmentRight( leftNode, rightNode, printedRight, options ); return group(concat([printedLeft, operator, printed])); } function adjustClause(node, clause, forceSpace) { if (node.type === "EmptyStatement") { return ";"; } if (node.type === "BlockStatement" || forceSpace) { return concat([" ", clause]); } return indent(concat([line, clause])); } function nodeStr(node, options, isFlowOrTypeScriptDirectiveLiteral) { const raw = rawText(node); const isDirectiveLiteral = isFlowOrTypeScriptDirectiveLiteral || node.type === "DirectiveLiteral"; return printString(raw, options, isDirectiveLiteral); } function printRegex(node) { const flags = node.flags .split("") .sort() .join(""); return `/${node.pattern}/${flags}`; } function isLastStatement(path) { const parent = path.getParentNode(); if (!parent) { return true; } const node = path.getValue(); const body = (parent.body || parent.consequent).filter( stmt => stmt.type !== "EmptyStatement" ); return body && body[body.length - 1] === node; } function hasLeadingComment(node) { return node.comments && node.comments.some(comment => comment.leading); } function hasTrailingComment(node) { return node.comments && node.comments.some(comment => comment.trailing); } function hasLeadingOwnLineComment(text, node, options) { if (isJSXNode(node)) { return hasNodeIgnoreComment(node); } const res = node.comments && node.comments.some( comment => comment.leading && hasNewline(text, options.locEnd(comment)) ); return res; } function hasNakedLeftSide(node) { return ( node.type === "AssignmentExpression" || node.type === "BinaryExpression" || node.type === "LogicalExpression" || node.type === "NGPipeExpression" || node.type === "ConditionalExpression" || node.type === "CallExpression" || node.type === "OptionalCallExpression" || node.type === "MemberExpression" || node.type === "OptionalMemberExpression" || node.type === "SequenceExpression" || node.type === "TaggedTemplateExpression" || node.type === "BindExpression" || (node.type === "UpdateExpression" && !node.prefix) || node.type === "TSNonNullExpression" ); } function isFlowAnnotationComment(text, typeAnnotation, options) { const start = options.locStart(typeAnnotation); const end = skipWhitespace(text, options.locEnd(typeAnnotation)); return text.substr(start, 2) === "/*" && text.substr(end, 2) === "*/"; } function getLeftSide(node) { if (node.expressions) { return node.expressions[0]; } return ( node.left || node.test || node.callee || node.object || node.tag || node.argument || node.expression ); } function getLeftSidePathName(path, node) { if (node.expressions) { return ["expressions", 0]; } if (node.left) { return ["left"]; } if (node.test) { return ["test"]; } if (node.object) { return ["object"]; } if (node.callee) { return ["callee"]; } if (node.tag) { return ["tag"]; } if (node.argument) { return ["argument"]; } if (node.expression) { return ["expression"]; } throw new Error("Unexpected node has no left side", node); } function exprNeedsASIProtection(path, options) { const node = path.getValue(); const maybeASIProblem = pathNeedsParens(path, options) || node.type === "ParenthesizedExpression" || node.type === "TypeCastExpression" || (node.type === "ArrowFunctionExpression" && !shouldPrintParamsWithoutParens(path, options)) || node.type === "ArrayExpression" || node.type === "ArrayPattern" || (node.type === "UnaryExpression" && node.prefix && (node.operator === "+" || node.operator === "-")) || node.type === "TemplateLiteral" || node.type === "TemplateElement" || isJSXNode(node) || (node.type === "BindExpression" && !node.object) || node.type === "RegExpLiteral" || (node.type === "Literal" && node.pattern) || (node.type === "Literal" && node.regex); if (maybeASIProblem) { return true; } if (!hasNakedLeftSide(node)) { return false; } return path.call.apply( path, [childPath => exprNeedsASIProtection(childPath, options)].concat( getLeftSidePathName(path, node) ) ); } function stmtNeedsASIProtection(path, options) { const node = path.getNode(); if (node.type !== "ExpressionStatement") { return false; } return path.call( childPath => exprNeedsASIProtection(childPath, options), "expression" ); } function classPropMayCauseASIProblems(path) { const node = path.getNode(); if (node.type !== "ClassProperty") { return false; } const name = node.key && node.key.name; // this isn't actually possible yet with most parsers available today // so isn't properly tested yet. if ( (name === "static" || name === "get" || name === "set") && !node.value && !node.typeAnnotation ) { return true; } } function classChildNeedsASIProtection(node) { if (!node) { return; } if ( node.static || node.accessibility // TypeScript ) { return false; } if (!node.computed) { const name = node.key && node.key.name; if (name === "in" || name === "instanceof") { return true; } } switch (node.type) { case "ClassProperty": case "TSAbstractClassProperty": return node.computed; case "MethodDefinition": // Flow case "TSAbstractMethodDefinition": // TypeScript case "ClassMethod": { // Babylon const isAsync = node.value ? node.value.async : node.async; const isGenerator = node.value ? node.value.generator : node.generator; if (isAsync || node.kind === "get" || node.kind === "set") { return false; } if (node.computed || isGenerator) { return true; } return false; } default: /* istanbul ignore next */ return false; } } // This recurses the return argument, looking for the first token // (the leftmost leaf node) and, if it (or its parents) has any // leadingComments, returns true (so it can be wrapped in parens). function returnArgumentHasLeadingComment(options, argument) { if (hasLeadingOwnLineComment(options.originalText, argument, options)) { return true; } if (hasNakedLeftSide(argument)) { let leftMost = argument; let newLeftMost; while ((newLeftMost = getLeftSide(leftMost))) { leftMost = newLeftMost; if (hasLeadingOwnLineComment(options.originalText, leftMost, options)) { return true; } } } return false; } function isMemberExpressionChain(node) { if ( node.type !== "MemberExpression" && node.type !== "OptionalMemberExpression" ) { return false; } if (node.object.type === "Identifier") { return true; } return isMemberExpressionChain(node.object); } // Hack to differentiate between the following two which have the same ast // type T = { method: () => void }; // type T = { method(): void }; function isObjectTypePropertyAFunction(node, options) { return ( (node.type === "ObjectTypeProperty" || node.type === "ObjectTypeInternalSlot") && node.value.type === "FunctionTypeAnnotation" && !node.static && !isFunctionNotation(node, options) ); } // TODO: This is a bad hack and we need a better way to distinguish between // arrow functions and otherwise function isFunctionNotation(node, options) { return isGetterOrSetter(node) || sameLocStart(node, node.value, options); } function isGetterOrSetter(node) { return node.kind === "get" || node.kind === "set"; } function sameLocStart(nodeA, nodeB, options) { return options.locStart(nodeA) === options.locStart(nodeB); } // Hack to differentiate between the following two which have the same ast // declare function f(a): void; // var f: (a) => void; function isTypeAnnotationAFunction(node, options) { return ( (node.type === "TypeAnnotation" || node.type === "TSTypeAnnotation") && node.typeAnnotation.type === "FunctionTypeAnnotation" && !node.static && !sameLocStart(node, node.typeAnnotation, options) ); } function isNodeStartingWithDeclare(node, options) { if (!(options.parser === "flow" || options.parser === "typescript")) { return false; } return ( options.originalText .slice(0, options.locStart(node)) .match(/declare[ \t]*$/) || options.originalText .slice(node.range[0], node.range[1]) .startsWith("declare ") ); } function shouldHugType(node) { if (isSimpleFlowType(node) || isObjectType(node)) { return true; } if (node.type === "UnionTypeAnnotation" || node.type === "TSUnionType") { const voidCount = node.types.filter( n => n.type === "VoidTypeAnnotation" || n.type === "TSVoidKeyword" || n.type === "NullLiteralTypeAnnotation" || n.type === "TSNullKeyword" ).length; const objectCount = node.types.filter( n => n.type === "ObjectTypeAnnotation" || n.type === "TSTypeLiteral" || // This is a bit aggressive but captures Array<{x}> n.type === "GenericTypeAnnotation" || n.type === "TSTypeReference" ).length; if (node.types.length - 1 === voidCount && objectCount > 0) { return true; } } return false; } function shouldHugArguments(fun) { return ( fun && fun.params && fun.params.length === 1 && !fun.params[0].comments && (fun.params[0].type === "ObjectPattern" || fun.params[0].type === "ArrayPattern" || (fun.params[0].type === "Identifier" && fun.params[0].typeAnnotation && (fun.params[0].typeAnnotation.type === "TypeAnnotation" || fun.params[0].typeAnnotation.type === "TSTypeAnnotation") && isObjectType(fun.params[0].typeAnnotation.typeAnnotation)) || (fun.params[0].type === "FunctionTypeParam" && isObjectType(fun.params[0].typeAnnotation)) || (fun.params[0].type === "AssignmentPattern" && (fun.params[0].left.type === "ObjectPattern" || fun.params[0].left.type === "ArrayPattern") && (fun.params[0].right.type === "Identifier" || (fun.params[0].right.type === "ObjectExpression" && fun.params[0].right.properties.length === 0) || (fun.params[0].right.type === "ArrayExpression" && fun.params[0].right.elements.length === 0)))) && !fun.rest ); } function templateLiteralHasNewLines(template) { return template.quasis.some(quasi => quasi.value.raw.includes("\n")); } function isTemplateOnItsOwnLine(n, text, options) { return ( ((n.type === "TemplateLiteral" && templateLiteralHasNewLines(n)) || (n.type === "TaggedTemplateExpression" && templateLiteralHasNewLines(n.quasi))) && !hasNewline(text, options.locStart(n), { backwards: true }) ); } function printArrayItems(path, options, printPath, print) { const printedElements = []; let separatorParts = []; path.each(childPath => { printedElements.push(concat(separatorParts)); printedElements.push(group(print(childPath))); separatorParts = [",", line]; if ( childPath.getValue() && isNextLineEmpty(options.originalText, childPath.getValue(), options) ) { separatorParts.push(softline); } }, printPath); return concat(printedElements); } function hasDanglingComments(node) { return ( node.comments && node.comments.some(comment => !comment.leading && !comment.trailing) ); } function needsHardlineAfterDanglingComment(node) { if (!node.comments) { return false; } const lastDanglingComment = getLast( node.comments.filter(comment => !comment.leading && !comment.trailing) ); return ( lastDanglingComment && !handleComments.isBlockComment(lastDanglingComment) ); } function isLiteral(node) { return ( node.type === "BooleanLiteral" || node.type === "DirectiveLiteral" || node.type === "Literal" || node.type === "NullLiteral" || node.type === "NumericLiteral" || node.type === "RegExpLiteral" || node.type === "StringLiteral" || node.type === "TemplateLiteral" || node.type === "TSTypeLiteral" || node.type === "JSXText" ); } function isNumericLiteral(node) { return ( node.type === "NumericLiteral" || (node.type === "Literal" && typeof node.value === "number") ); } function isStringLiteral(node) { return ( node.type === "StringLiteral" || (node.type === "Literal" && typeof node.value === "string") ); } function isObjectType(n) { return n.type === "ObjectTypeAnnotation" || n.type === "TSTypeLiteral"; } const unitTestRe = /^(skip|[fx]?(it|describe|test))$/; // eg; `describe("some string", (done) => {})` function isTestCall(n, parent) { if (n.type !== "CallExpression") { return false; } if (n.arguments.length === 1) { if (isAngularTestWrapper(n) && parent && isTestCall(parent)) { return isFunctionOrArrowExpression(n.arguments[0]); } if (isUnitTestSetUp(n)) { return isAngularTestWrapper(n.arguments[0]); } } else if (n.arguments.length === 2 || n.arguments.length === 3) { if ( ((n.callee.type === "Identifier" && unitTestRe.test(n.callee.name)) || isSkipOrOnlyBlock(n)) && (isTemplateLiteral(n.arguments[0]) || isStringLiteral(n.arguments[0])) ) { // it("name", () => { ... }, 2500) if (n.arguments[2] && !isNumericLiteral(n.arguments[2])) { return false; } return ( (n.arguments.length === 2 ? isFunctionOrArrowExpression(n.arguments[1]) : isFunctionOrArrowExpressionWithBody(n.arguments[1]) && n.arguments[1].params.length <= 1) || isAngularTestWrapper(n.arguments[1]) ); } } return false; } function isSkipOrOnlyBlock(node) { return ( (node.callee.type === "MemberExpression" || node.callee.type === "OptionalMemberExpression") && node.callee.object.type === "Identifier" && node.callee.property.type === "Identifier" && unitTestRe.test(node.callee.object.name) && (node.callee.property.name === "only" || node.callee.property.name === "skip") ); } function isTemplateLiteral(node) { return node.type === "TemplateLiteral"; } // `inject` is used in AngularJS 1.x, `async` in Angular 2+ // example: https://docs.angularjs.org/guide/unit-testing#using-beforeall- function isAngularTestWrapper(node) { return ( (node.type === "CallExpression" || node.type === "OptionalCallExpression") && node.callee.type === "Identifier" && (node.callee.name === "async" || node.callee.name === "inject" || node.callee.name === "fakeAsync") ); } function isFunctionOrArrowExpression(node) { return ( node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" ); } function isFunctionOrArrowExpressionWithBody(node) { return ( node.type === "FunctionExpression" || (node.type === "ArrowFunctionExpression" && node.body.type === "BlockStatement") ); } function isUnitTestSetUp(n) { const unitTestSetUpRe = /^(before|after)(Each|All)$/; return ( n.callee.type === "Identifier" && unitTestSetUpRe.test(n.callee.name) && n.arguments.length === 1 ); } function isTheOnlyJSXElementInMarkdown(options, path) { if (options.parentParser !== "markdown" && options.parentParser !== "mdx") { return false; } const node = path.getNode(); if (!node.expression || !isJSXNode(node.expression)) { return false; } const parent = path.getParentNode(); return parent.type === "Program" && parent.body.length == 1; } function willPrintOwnComments(path) { const node = path.getValue(); const parent = path.getParentNode(); return ( ((node && (isJSXNode(node) || hasFlowShorthandAnnotationComment(node) || (parent && parent.type === "CallExpression" && (hasFlowAnnotationComment(node.leadingComments) || hasFlowAnnotationComment(node.trailingComments))))) || (parent && (parent.type === "JSXSpreadAttribute" || parent.type === "JSXSpreadChild" || parent.type === "UnionTypeAnnotation" || parent.type === "TSUnionType" || ((parent.type === "ClassDeclaration" || parent.type === "ClassExpression") && parent.superClass === node)))) && !hasIgnoreComment(path) ); } function canAttachComment(node) { return ( node.type && node.type !== "CommentBlock" && node.type !== "CommentLine" && node.type !== "Line" && node.type !== "Block" && node.type !== "EmptyStatement" && node.type !== "TemplateElement" && node.type !== "Import" && !(node.callee && node.callee.type === "Import") ); } function printComment(commentPath, options) { const comment = commentPath.getValue(); switch (comment.type) { case "CommentBlock": case "Block": { if (isIndentableBlockComment(comment)) { const printed = printIndentableBlockComment(comment); // We need to prevent an edge case of a previous trailing comment // printed as a `lineSuffix` which causes the comments to be // interleaved. See https://github.com/prettier/prettier/issues/4412 if ( comment.trailing && !hasNewline(options.originalText, options.locStart(comment), { backwards: true }) ) { return concat([hardline, printed]); } return printed; } const isInsideFlowComment = options.originalText.substr(options.locEnd(comment) - 3, 3) === "*-/"; return "/*" + comment.value + (isInsideFlowComment ? "*-/" : "*/"); } case "CommentLine": case "Line": // Print shebangs with the proper comment characters if ( options.originalText.slice(options.locStart(comment)).startsWith("#!") ) { return "#!" + comment.value.trimRight(); } return "//" + comment.value.trimRight(); default: throw new Error("Not a comment: " + JSON.stringify(comment)); } } function isIndentableBlockComment(comment) { // If the comment has multiple lines and every line starts with a star // we can fix the indentation of each line. The stars in the `/*` and // `*/` delimiters are not included in the comment value, so add them // back first. const lines = `*${comment.value}*`.split("\n"); return lines.length > 1 && lines.every(line => line.trim()[0] === "*"); } function printIndentableBlockComment(comment) { const lines = comment.value.split("\n"); return concat([ "/*", join( hardline, lines.map((line, index) => index === 0 ? line.trimRight() : " " + (index < lines.length - 1 ? line.trim() : line.trimLeft()) ) ), "*/" ]); } function rawText(node) { return node.extra ? node.extra.raw : node.raw; } function identity(x) { return x; } module.exports = { preprocess, print: genericPrint, embed, insertPragma, massageAstNode: clean, hasPrettierIgnore, willPrintOwnComments, canAttachComment, printComment, isBlockComment: handleComments.isBlockComment, handleComments: { ownLine: handleComments.handleOwnLineComment, endOfLine: handleComments.handleEndOfLineComment, remaining: handleComments.handleRemainingComment } };