Refactor: Move functions unrelated to printing from printer-estree.js to utils.js (#6562)

master
Sosuke Suzuki 2019-10-03 23:45:03 +09:00 committed by Evilebot Tnawi
parent 6d4d6a7599
commit 32dd447271
2 changed files with 893 additions and 794 deletions

View File

@ -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" : <span className="warning">Unknown</span>;
//
// 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,

View File

@ -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" : <span className="warning">Unknown</span>;
//
// 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
};