diff --git a/changelog_unreleased/javascript/16116.md b/changelog_unreleased/javascript/16116.md new file mode 100644 index 000000000..31673dfa1 --- /dev/null +++ b/changelog_unreleased/javascript/16116.md @@ -0,0 +1,19 @@ +#### Allow linebreaks in member expressions in template interpolations (#16116 by @bakkot) + +When there is already a linebreak in a template interpolation, allow it to stay there even if it is a member expression. Note that (as of [#15209](https://github.com/prettier/prettier/pull/15209)) Prettier will not insert a linebreak inside an interpolation when one is not already present. + + +```jsx +// Input +`template with ${ + very.very.very.very.very.very.very.very.very.very.very.long.chain +}`; + +// Prettier stable +`template with ${very.very.very.very.very.very.very.very.very.very.very.long.chain}`; + +// Prettier main +`template with ${ + very.very.very.very.very.very.very.very.very.very.very.long.chain +}`; +``` diff --git a/src/language-js/print/template-literal.js b/src/language-js/print/template-literal.js index d4b1db9cc..effab7f88 100644 --- a/src/language-js/print/template-literal.js +++ b/src/language-js/print/template-literal.js @@ -20,7 +20,6 @@ import { isBinaryCastExpression, isBinaryish, isMemberExpression, - isSimpleTemplateLiteral, } from "../utils/index.js"; function printTemplateLiteral(path, print, options) { @@ -39,18 +38,7 @@ function printTemplateLiteral(path, print, options) { } const parts = []; - let expressionDocs = path.map(print, expressionsKey); - const isSimple = isSimpleTemplateLiteral(node); - - if (isSimple) { - expressionDocs = expressionDocs.map( - (doc) => - printDocToString(doc, { - ...options, - printWidth: Number.POSITIVE_INFINITY, - }).formatted, - ); - } + const expressionDocs = path.map(print, expressionsKey); parts.push(lineSuffixBoundary, "`"); @@ -82,44 +70,43 @@ function printTemplateLiteral(path, print, options) { let expressionDoc = expressionDocs[index]; - if (!isSimple) { - const expression = node[expressionsKey][index]; + const expression = node[expressionsKey][index]; - let interpolationHasNewline = hasNewlineInRange( - options.originalText, - locEnd(quasi), - locStart(node.quasis[index + 1]), - ); + let interpolationHasNewline = hasNewlineInRange( + options.originalText, + locEnd(quasi), + locStart(node.quasis[index + 1]), + ); - if (!interpolationHasNewline) { - // Never add a newline to an interpolation which didn't already have one... - const renderedExpression = printDocToString(expressionDoc, { - ...options, - printWidth: Number.POSITIVE_INFINITY, - }).formatted; + if (!interpolationHasNewline) { + // Never add a newline to an interpolation which didn't already have one... + const renderedExpression = printDocToString(expressionDoc, { + ...options, + printWidth: Number.POSITIVE_INFINITY, + }).formatted; - // ... unless one will be introduced anyway, e.g. by a nested function. - // This case is rare, so we can pay the cost of re-rendering. - if (renderedExpression.includes("\n")) { - interpolationHasNewline = true; - } else { - expressionDoc = renderedExpression; - } + // ... unless one will be introduced anyway, e.g. by a nested function. + // This case is rare, so we can pay the cost of re-rendering. + if (renderedExpression.includes("\n")) { + interpolationHasNewline = true; + } else { + expressionDoc = renderedExpression; } + } - // Breaks at the template element boundaries (${ and }) are preferred to breaking - // in the middle of a MemberExpression - if ( - interpolationHasNewline && - (hasComment(expression) || - isMemberExpression(expression) || - expression.type === "ConditionalExpression" || - expression.type === "SequenceExpression" || - isBinaryCastExpression(expression) || - isBinaryish(expression)) - ) { - expressionDoc = [indent([softline, expressionDoc]), softline]; - } + // Breaks at the template element boundaries (${ and }) are preferred to breaking + // in the middle of a MemberExpression + if ( + interpolationHasNewline && + (hasComment(expression) || + expression.type === "Identifier" || + isMemberExpression(expression) || + expression.type === "ConditionalExpression" || + expression.type === "SequenceExpression" || + isBinaryCastExpression(expression) || + isBinaryish(expression)) + ) { + expressionDoc = [indent([softline, expressionDoc]), softline]; } const aligned = diff --git a/src/language-js/utils/index.js b/src/language-js/utils/index.js index 3a5238382..9cf33716f 100644 --- a/src/language-js/utils/index.js +++ b/src/language-js/utils/index.js @@ -420,68 +420,6 @@ const isMemberExpression = skipChainExpression( createTypeCheckFunction(["MemberExpression", "OptionalMemberExpression"]), ); -/** - * - * @param {any} node - * @returns {boolean} - */ -function isSimpleTemplateLiteral(node) { - let expressionsKey = "expressions"; - if (node.type === "TSTemplateLiteralType") { - expressionsKey = "types"; - } - const expressions = node[expressionsKey]; - - if (expressions.length === 0) { - return false; - } - - return expressions.every((expr) => { - if (isSimpleAtomicExpression(expr) || isSimpleMemberExpression(expr)) { - return true; - } - }); -} - -function isSimpleMemberExpression( - node, - { maxDepth = Number.POSITIVE_INFINITY } = {}, -) { - if (hasComment(node)) { - return false; - } - if (node.type === "ChainExpression") { - return isSimpleMemberExpression(node.expression, { maxDepth }); - } - if (!isMemberExpression(node)) { - return false; - } - - let head = node; - let depth = 0; - while (isMemberExpression(head) && depth++ <= maxDepth) { - if (!isSimpleAtomicExpression(head.property)) { - return false; - } - head = head.object; - if (hasComment(head)) { - return false; - } - } - return isSimpleAtomicExpression(head); -} - -/** - * This is intended to return true for small expressions - * which cannot be broken. - */ -function isSimpleAtomicExpression(node) { - if (hasComment(node)) { - return false; - } - return isLiteral(node) || isSingleWordType(node); -} - /** * Attempts to gauge the rough complexity of a node, for example * to detect deeply-nested booleans, call expressions with lots of arguments, etc. @@ -1209,11 +1147,8 @@ export { isPrettierIgnoreComment, isRegExpLiteral, isSignedNumericLiteral, - isSimpleAtomicExpression, isSimpleCallArgument, isSimpleExpressionByNodeCount, - isSimpleMemberExpression, - isSimpleTemplateLiteral, isSimpleType, isStringLiteral, isTemplateOnItsOwnLine, diff --git a/tests/format/js/strings/__snapshots__/jsfmt.spec.js.snap b/tests/format/js/strings/__snapshots__/jsfmt.spec.js.snap index c5982dad0..b21ee067e 100644 --- a/tests/format/js/strings/__snapshots__/jsfmt.spec.js.snap +++ b/tests/format/js/strings/__snapshots__/jsfmt.spec.js.snap @@ -415,6 +415,12 @@ and this other one: \${ which already had a linebreak so can be broken up \`; +// https://github.com/prettier/prettier/issues/16114 +message = \`this is a long messsage a simple interpolation without a linebreak \${foo} <- like this\`; + +message = \`whereas this messsage has a linebreak in the interpolation \${ + foo} <- like this\`; + =====================================output===================================== foo( \`a long string \${1 + 2 + 3 + 2 + 3 + 2 + 3 + 2 + 3 + 2 + 3 + 2 + 3 + 2 + 3 + 2 + 3} with expr\`, @@ -537,6 +543,13 @@ and this other one: \${this( which already had a linebreak so can be broken up \`; +// https://github.com/prettier/prettier/issues/16114 +message = \`this is a long messsage a simple interpolation without a linebreak \${foo} <- like this\`; + +message = \`whereas this messsage has a linebreak in the interpolation \${ + foo +} <- like this\`; + ================================================================================ `; @@ -615,6 +628,12 @@ and this other one: \${ which already had a linebreak so can be broken up \`; +// https://github.com/prettier/prettier/issues/16114 +message = \`this is a long messsage a simple interpolation without a linebreak \${foo} <- like this\`; + +message = \`whereas this messsage has a linebreak in the interpolation \${ + foo} <- like this\`; + =====================================output===================================== foo( \`a long string \${1 + 2 + 3 + 2 + 3 + 2 + 3 + 2 + 3 + 2 + 3 + 2 + 3 + 2 + 3 + 2 + 3} with expr\` @@ -737,5 +756,12 @@ and this other one: \${this( which already had a linebreak so can be broken up \`; +// https://github.com/prettier/prettier/issues/16114 +message = \`this is a long messsage a simple interpolation without a linebreak \${foo} <- like this\`; + +message = \`whereas this messsage has a linebreak in the interpolation \${ + foo +} <- like this\`; + ================================================================================ `; diff --git a/tests/format/js/strings/template-literals.js b/tests/format/js/strings/template-literals.js index fc6bca5d9..d7c9e71eb 100644 --- a/tests/format/js/strings/template-literals.js +++ b/tests/format/js/strings/template-literals.js @@ -65,3 +65,9 @@ and this other one: ${ } which already had a linebreak so can be broken up `; + +// https://github.com/prettier/prettier/issues/16114 +message = `this is a long messsage a simple interpolation without a linebreak ${foo} <- like this`; + +message = `whereas this messsage has a linebreak in the interpolation ${ + foo} <- like this`;