From 32dd447271db23b6d51f28f7053c7f9ebae46e56 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Thu, 3 Oct 2019 23:45:03 +0900 Subject: [PATCH] Refactor: Move functions unrelated to printing from printer-estree.js to utils.js (#6562) --- src/language-js/printer-estree.js | 839 ++--------------------------- src/language-js/utils.js | 848 +++++++++++++++++++++++++++++- 2 files changed, 893 insertions(+), 794 deletions(-) diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js index 86ebc3d72..47aafc252 100644 --- a/src/language-js/printer-estree.js +++ b/src/language-js/printer-estree.js @@ -16,7 +16,6 @@ const { printString, printNumber, hasIgnoreComment, - skipWhitespace, hasNodeIgnoreComment, getPenultimate, startsWithNoLookaheadToken, @@ -29,7 +28,6 @@ const { 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; @@ -41,12 +39,57 @@ const { } = require("./html-binding"); const preprocess = require("./preprocess"); const { - getLeftSide, + classChildNeedsASIProtection, + classPropMayCauseASIProblems, + conditionalExpressionChainContainsJSX, + getFlowVariance, getLeftSidePathName, - hasNakedLeftSide, - hasNode, + getTypeScriptMappedTypeModifier, + hasDanglingComments, hasFlowAnnotationComment, - hasFlowShorthandAnnotationComment + hasFlowShorthandAnnotationComment, + hasLeadingComment, + hasLeadingOwnLineComment, + hasNakedLeftSide, + hasNewlineBetweenOrAfterDecorators, + hasNgSideEffect, + hasPrettierIgnore, + hasTrailingComment, + identity, + isBinaryish, + isCallOrOptionalCallExpression, + isEmptyJSXElement, + isFlowAnnotationComment, + isFunctionCompositionArgs, + isFunctionNotation, + isFunctionOrArrowExpression, + isGetterOrSetter, + isJestEachTemplateLiteral, + isJSXNode, + isJSXWhitespaceExpression, + isLastStatement, + isLiteral, + isLongCurriedCallExpression, + isMeaningfulJSXText, + isMemberExpressionChain, + isMemberish, + isNgForOf, + isNodeStartingWithDeclare, + isNumericLiteral, + isObjectType, + isObjectTypePropertyAFunction, + isSimpleFlowType, + isSimpleTemplateLiteral, + isStringLiteral, + isStringPropSafeToCoerceToIdentifier, + isTemplateOnItsOwnLine, + isTestCall, + isTheOnlyJSXElementInMarkdown, + isTypeAnnotationAFunction, + matchJsxWhitespaceRegex, + needsHardlineAfterDanglingComment, + rawText, + returnArgumentHasLeadingComment } = require("./utils"); const needsQuoteProps = new WeakMap(); @@ -202,17 +245,6 @@ function genericPrint(path, options, printPath, args) { 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( @@ -223,40 +255,6 @@ function printDecorators(path, options, print) { ); } -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 @@ -419,15 +417,6 @@ function printTernaryOperator(path, options, print, operatorOptions) { ); } -function getTypeScriptMappedTypeModifier(tokenNode, keyword) { - if (tokenNode === "+") { - return "+" + keyword; - } else if (tokenNode === "-") { - return "-" + keyword; - } - return keyword; -} - function printPathNoParens(path, options, print, args) { const n = path.getValue(); const semi = options.semi ? ";" : ""; @@ -3560,30 +3549,6 @@ function printPathNoParens(path, options, print, args) { } } -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 = []; @@ -3825,57 +3790,6 @@ function shouldGroupFirstArg(args) { ); } -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) - ); -} - -function isJestEachTemplateLiteral(node, parentNode) { - /** - * 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)$/; - return ( - parentNode.type === "TaggedTemplateExpression" && - parentNode.quasi === node && - 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))) - ); -} - function printJestEachTemplateLiteral(node, expressions, options) { /** * a | b | expected @@ -3966,97 +3880,6 @@ function printJestEachTemplateLiteral(node, expressions, options) { } } -/** @param node {import("estree").TemplateLiteral} */ -function isSimpleTemplateLiteral(node) { - if (node.expressions.length === 0) { - return false; - } - - return node.expressions.every(expr => { - // Disallow comments since printDocToString can't print them here - if (expr.comments) { - return false; - } - - // Allow `x` and `this` - if (expr.type === "Identifier" || expr.type === "ThisExpression") { - return true; - } - - // Allow `a.b.c`, `a.b[c]`, and `this.x.y` - if ( - (expr.type === "MemberExpression" || - expr.type === "OptionalMemberExpression") && - (expr.property.type === "Identifier" || expr.property.type === "Literal") - ) { - let ancestor = expr; - while ( - ancestor.type === "MemberExpression" || - ancestor.type === "OptionalMemberExpression" - ) { - ancestor = ancestor.object; - if (ancestor.comments) { - return false; - } - } - - if ( - ancestor.type === "Identifier" || - ancestor.type === "ThisExpression" - ) { - return true; - } - - return false; - } - - return false; - }); -} - -// Logic to check for args with multiple anonymous functions. For instance, -// the following call should be split on multiple lines for readability: -// source.pipe(map((x) => x + x), filter((x) => x % 2 === 0)) -function isFunctionCompositionArgs(args) { - if (args.length <= 1) { - return false; - } - let count = 0; - for (const arg of args) { - if (isFunctionOrArrowExpression(arg)) { - count += 1; - if (count > 1) { - return true; - } - } else if (isCallOrOptionalCallExpression(arg)) { - for (const childArg of arg.arguments) { - if (isFunctionOrArrowExpression(childArg)) { - return true; - } - } - } - } - return false; -} - -// Logic to determine if a call is a “long curried function call”. -// See https://github.com/prettier/prettier/issues/1420. -// -// `connect(a, b, c)(d)` -// In the above call expression, the second call is the parent node and the -// first call is the current node. -function isLongCurriedCallExpression(path) { - const node = path.getValue(); - const parent = path.getParentNode(); - return ( - isCallOrOptionalCallExpression(node) && - isCallOrOptionalCallExpression(parent) && - parent.callee === node && - node.arguments.length > parent.arguments.length && - parent.arguments.length > 0 - ); -} - function printArgumentsList(path, options, print) { const node = path.getValue(); const args = node.arguments; @@ -4683,27 +4506,6 @@ function printFlowDeclaration(path, parts) { return concat(parts); } -function getFlowVariance(path) { - if (!path.variance) { - return null; - } - - // Babel 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) { @@ -5275,153 +5077,6 @@ function printMemberChain(path, options, print) { ]); } -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, @@ -5852,22 +5507,6 @@ function maybeWrapJSXElementInParens(path, elem) { ); } -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; @@ -6080,45 +5719,6 @@ function printRegex(node) { 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 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 exprNeedsASIProtection(path, options) { const node = path.getValue(); @@ -6170,159 +5770,6 @@ function stmtNeedsASIProtection(path, options) { ); } -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": - case "ClassPrivateMethod": { - // Babel - 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; @@ -6381,19 +5828,6 @@ function shouldHugArguments(fun) { ); } -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 = []; @@ -6414,173 +5848,6 @@ function printArrayItems(path, options, printPath, print) { 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 isStringPropSafeToCoerceToIdentifier(node, options) { - return ( - isStringLiteral(node.key) && - isIdentifierName(node.key.value) && - options.parser !== "json" && - !(options.parser === "typescript" && node.type === "ClassProperty") - ); -} - -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 /*, options */) { const node = path.getValue(); const parent = path.getParentNode(); @@ -6685,14 +5952,6 @@ function printIndentableBlockComment(comment) { ]); } -function rawText(node) { - return node.extra ? node.extra.raw : node.raw; -} - -function identity(x) { - return x; -} - module.exports = { preprocess, print: genericPrint, diff --git a/src/language-js/utils.js b/src/language-js/utils.js index 5f60e04ae..60f7cbc05 100644 --- a/src/language-js/utils.js +++ b/src/language-js/utils.js @@ -1,5 +1,16 @@ "use strict"; +const { + getLast, + hasNewline, + hasNewlineInRange, + hasIgnoreComment, + hasNodeIgnoreComment, + skipWhitespace +} = require("../common/util"); +const isIdentifierName = require("esutils").keyword.isIdentifierNameES5; +const handleComments = require("./comments"); + // We match any whitespace except line terminators because // Flow annotation comments cannot be split across lines. For example: // @@ -107,11 +118,840 @@ function getLeftSidePathName(path, node) { throw new Error("Unexpected node has no left side", node); } +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"; +} + +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 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 isJSXNode(node) { + return node.type === "JSXElement" || node.type === "JSXFragment"; +} + +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; +} + +// Detect an expression node representing `{" "}` +function isJSXWhitespaceExpression(node) { + return ( + node.type === "JSXExpressionContainer" && + isLiteral(node.expression) && + node.expression.value === " " && + !node.expression.comments + ); +} + +function isMemberExpressionChain(node) { + if ( + node.type !== "MemberExpression" && + node.type !== "OptionalMemberExpression" + ) { + return false; + } + if (node.object.type === "Identifier") { + return true; + } + return isMemberExpressionChain(node.object); +} + +function isGetterOrSetter(node) { + return node.kind === "get" || node.kind === "set"; +} + +function sameLocStart(nodeA, nodeB, options) { + return options.locStart(nodeA) === options.locStart(nodeB); +} + +// 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); +} + +// 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) + ); +} + +// 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 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 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 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 unitTestRe = /^(skip|[fx]?(it|describe|test))$/; + +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 isUnitTestSetUp(n) { + const unitTestSetUpRe = /^(before|after)(Each|All)$/; + return ( + n.callee.type === "Identifier" && + unitTestSetUpRe.test(n.callee.name) && + n.arguments.length === 1 + ); +} + +// 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 hasLeadingComment(node) { + return node.comments && node.comments.some(comment => comment.leading); +} + +function hasTrailingComment(node) { + return node.comments && node.comments.some(comment => comment.trailing); +} + +function isCallOrOptionalCallExpression(node) { + return ( + node.type === "CallExpression" || node.type === "OptionalCallExpression" + ); +} + +function hasDanglingComments(node) { + return ( + node.comments && + node.comments.some(comment => !comment.leading && !comment.trailing) + ); +} + +/** 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 isNgForOf(node, index, parentNode) { + return ( + node.type === "NGMicrosyntaxKeyedExpression" && + node.key.name === "of" && + index === 1 && + parentNode.body[0].type === "NGMicrosyntaxLet" && + parentNode.body[0].value === null + ); +} + +/** @param node {import("estree").TemplateLiteral} */ +function isSimpleTemplateLiteral(node) { + if (node.expressions.length === 0) { + return false; + } + + return node.expressions.every(expr => { + // Disallow comments since printDocToString can't print them here + if (expr.comments) { + return false; + } + + // Allow `x` and `this` + if (expr.type === "Identifier" || expr.type === "ThisExpression") { + return true; + } + + // Allow `a.b.c`, `a.b[c]`, and `this.x.y` + if ( + (expr.type === "MemberExpression" || + expr.type === "OptionalMemberExpression") && + (expr.property.type === "Identifier" || expr.property.type === "Literal") + ) { + let ancestor = expr; + while ( + ancestor.type === "MemberExpression" || + ancestor.type === "OptionalMemberExpression" + ) { + ancestor = ancestor.object; + if (ancestor.comments) { + return false; + } + } + + if ( + ancestor.type === "Identifier" || + ancestor.type === "ThisExpression" + ) { + return true; + } + + return false; + } + + return false; + }); +} + +function getFlowVariance(path) { + if (!path.variance) { + return null; + } + + // Babel 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 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": + case "ClassPrivateMethod": { + // Babel + 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; + } +} + +function getTypeScriptMappedTypeModifier(tokenNode, keyword) { + if (tokenNode === "+") { + return "+" + keyword; + } else if (tokenNode === "-") { + return "-" + keyword; + } + return keyword; +} + +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))) + ); +} + +// Only space, newline, carriage return, and tab are treated as whitespace +// inside JSX. +const jsxWhitespaceChars = " \n\r\t"; +const matchJsxWhitespaceRegex = new RegExp("([" + jsxWhitespaceChars + "]+)"); +const containsNonJsxWhitespaceRegex = 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 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" + ) + ); +} + +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); +} + +function hasPrettierIgnore(path) { + return hasIgnoreComment(path) || hasJsxIgnoreComment(path); +} + +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 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 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; +} + +// 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 isStringPropSafeToCoerceToIdentifier(node, options) { + return ( + isStringLiteral(node.key) && + isIdentifierName(node.key.value) && + options.parser !== "json" && + !(options.parser === "typescript" && node.type === "ClassProperty") + ); +} + +function isJestEachTemplateLiteral(node, parentNode) { + /** + * 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)$/; + return ( + parentNode.type === "TaggedTemplateExpression" && + parentNode.quasi === node && + 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))) + ); +} + +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 needsHardlineAfterDanglingComment(node) { + if (!node.comments) { + return false; + } + const lastDanglingComment = getLast( + node.comments.filter(comment => !comment.leading && !comment.trailing) + ); + return ( + lastDanglingComment && !handleComments.isBlockComment(lastDanglingComment) + ); +} + +// 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; +} + +function conditionalExpressionChainContainsJSX(node) { + return Boolean(getConditionalChainContents(node).find(isJSXNode)); +} + +// Logic to check for args with multiple anonymous functions. For instance, +// the following call should be split on multiple lines for readability: +// source.pipe(map((x) => x + x), filter((x) => x % 2 === 0)) +function isFunctionCompositionArgs(args) { + if (args.length <= 1) { + return false; + } + let count = 0; + for (const arg of args) { + if (isFunctionOrArrowExpression(arg)) { + count += 1; + if (count > 1) { + return true; + } + } else if (isCallOrOptionalCallExpression(arg)) { + for (const childArg of arg.arguments) { + if (isFunctionOrArrowExpression(childArg)) { + return true; + } + } + } + } + return false; +} + +// Logic to determine if a call is a “long curried function call”. +// See https://github.com/prettier/prettier/issues/1420. +// +// `connect(a, b, c)(d)` +// In the above call expression, the second call is the parent node and the +// first call is the current node. +function isLongCurriedCallExpression(path) { + const node = path.getValue(); + const parent = path.getParentNode(); + return ( + isCallOrOptionalCallExpression(node) && + isCallOrOptionalCallExpression(parent) && + parent.callee === node && + node.arguments.length > parent.arguments.length && + parent.arguments.length > 0 + ); +} + +function rawText(node) { + return node.extra ? node.extra.raw : node.raw; +} + +function identity(x) { + return x; +} + module.exports = { - getLeftSide, + classChildNeedsASIProtection, + classPropMayCauseASIProblems, + conditionalExpressionChainContainsJSX, + getFlowVariance, getLeftSidePathName, - hasNakedLeftSide, - hasNode, + getTypeScriptMappedTypeModifier, + hasDanglingComments, + hasFlowAnnotationComment, hasFlowShorthandAnnotationComment, - hasFlowAnnotationComment + hasLeadingComment, + hasLeadingOwnLineComment, + hasNakedLeftSide, + hasNewlineBetweenOrAfterDecorators, + hasNgSideEffect, + hasPrettierIgnore, + hasTrailingComment, + identity, + isBinaryish, + isCallOrOptionalCallExpression, + isEmptyJSXElement, + isFlowAnnotationComment, + isFunctionCompositionArgs, + isFunctionNotation, + isFunctionOrArrowExpression, + isGetterOrSetter, + isJestEachTemplateLiteral, + isJSXNode, + isJSXWhitespaceExpression, + isLastStatement, + isLiteral, + isLongCurriedCallExpression, + isMeaningfulJSXText, + isMemberExpressionChain, + isMemberish, + isNgForOf, + isNodeStartingWithDeclare, + isNumericLiteral, + isObjectType, + isObjectTypePropertyAFunction, + isSimpleFlowType, + isSimpleTemplateLiteral, + isStringLiteral, + isStringPropSafeToCoerceToIdentifier, + isTemplateOnItsOwnLine, + isTestCall, + isTheOnlyJSXElementInMarkdown, + isTypeAnnotationAFunction, + matchJsxWhitespaceRegex, + needsHardlineAfterDanglingComment, + rawText, + returnArgumentHasLeadingComment };