Fix unstable embedded template literals (#13532)

This commit is contained in:
Georgii Dolzhykov authored and GitHub committed 2022-09-26 16:24:41 +03:00
1 parent 822f354ceb
commit 6d4d011252
22 files changed
+243 -62

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
#### [BREAKING] Allow using arbitrary truthy values for `label` docs (#13532 by @thorn0)
The `label` doc builder has been changed. See the [documentation](https://github.com/prettier/prettier/blob/main/commands.md#label).
+32
View File
@@ -0,0 +1,32 @@
#### Fix unstable template literals with embedded languages (#13532 by @thorn0)
If a template literal with embedded syntax is the only argument of a call or the body of an arrow function and has leading and trailing whitespace, it won't be printed on a new line.
<!-- prettier-ignore -->
```js
// Input
foo(/* HTML */ ` <!-- bar1 --> bar <!-- bar2 --> `);
// Prettier stable (first output)
foo(
/* HTML */ `
<!-- bar1 -->
bar
<!-- bar2 -->
`
);
// Prettier stable (second output)
foo(/* HTML */ `
<!-- bar1 -->
bar
<!-- bar2 -->
`);
// Prettier main (first output)
foo(/* HTML */ `
<!-- bar1 -->
bar
<!-- bar2 -->
`);
```
+4 -2
View File
@@ -276,13 +276,15 @@ It doesn't make sense to apply `indentIfBreak` to the current group because "ind
_Added in v2.3.0_
```ts
declare function label(label: string, doc: Doc): Doc;
declare function label(label: any, doc: Doc): Doc;
```
Mark a doc with a string label. This doesn't affect how the doc is printed, but can be useful for heuristics based on doc introspection.
Mark a doc with an arbitrary truthy value. This doesn't affect how the doc is printed, but can be useful for heuristics based on doc introspection.
E.g., to decide how to print an assignment expression, we might want to know whether its right-hand side has been printed as a method call chain, not as a plain function call. If the method chain printing code uses `label` to mark its result, checking that condition can be as easy as `rightHandSideDoc.label === 'method-chain'`.
If the `label` argument is falsy, `doc` is returned as is, without wrapping.
### `hardlineWithoutBreakParent` and `literallineWithoutBreakParent`
_Added in v2.3.0_
+4 -4
View File
@@ -62,7 +62,7 @@ for (const category of CHANGELOG_CATEGORIES) {
continue;
}
const match = prFile.match(/^(\d{4,})\.md$/);
const match = prFile.match(/^(\d{4,})(-\d+)?\.md$/);
const displayPath = `${CHANGELOG_DIR}/${category}/${prFile}`;
if (!match) {
@@ -73,16 +73,16 @@ for (const category of CHANGELOG_CATEGORIES) {
}
const [, prNumber] = match;
const prLink = `#${prNumber}`;
if (checkedFiles.has(prNumber)) {
if (checkedFiles.has(prFile)) {
showErrorMessage(
outdent`
Duplicate files for ${prLink} found.
- ${checkedFiles.get(prNumber)}
- ${checkedFiles.get(prFile)}
- ${displayPath}
`
);
}
checkedFiles.set(prNumber, displayPath);
checkedFiles.set(prFile, displayPath);
const content = fs.readFileSync(
path.join(CHANGELOG_DIR, category, prFile),
"utf8"
+6 -1
View File
@@ -250,8 +250,13 @@ function addAlignmentToDoc(doc, size, tabWidth) {
return aligned;
}
/**
* Mark a doc with an arbitrary truthy value. This doesn't affect how the doc is printed, but can be useful for heuristics based on doc introspection.
* @param {any} label If falsy, the `contents` doc is returned as is.
* @param {Doc} contents
*/
function label(label, contents) {
return { type: DOC_TYPE_LABEL, label, contents };
return label ? { type: DOC_TYPE_LABEL, label, contents } : contents;
}
// TODO: Remove this in v4
+5 -1
View File
@@ -1,3 +1,4 @@
import { label } from "../document/builders.js";
import {
hasComment,
CommentCheckFlags,
@@ -28,7 +29,10 @@ function embed(path) {
return "``";
}
return embedder;
return async (...args) => {
const doc = await embedder(...args);
return doc && label({ embed: true, ...doc.label }, doc);
};
}
}
+17 -8
View File
@@ -1,4 +1,10 @@
import { indent, line, hardline, group } from "../../document/builders.js";
import {
indent,
line,
hardline,
group,
label,
} from "../../document/builders.js";
import { mapDoc } from "../../document/utils.js";
import {
printTemplateExpressions,
@@ -77,13 +83,16 @@ async function embedHtmlLike(parser, textToDoc, print, path, options) {
return group(["`", indent([linebreak, group(contentDoc)]), linebreak, "`"]);
}
return group([
"`",
leadingWhitespace,
topLevelCount > 1 ? indent(group(contentDoc)) : group(contentDoc),
trailingWhitespace,
"`",
]);
return label(
{ hug: false },
group([
"`",
leadingWhitespace,
topLevelCount > 1 ? indent(group(contentDoc)) : group(contentDoc),
trailingWhitespace,
"`",
])
);
}
export const embedHtml = embedHtmlLike.bind(undefined, "html");
+1 -1
View File
@@ -347,7 +347,7 @@ function isPoorlyBreakableMemberOrCallChain(
if (isCallExpression(node)) {
/** @type {any} TODO */
const doc = printCallExpression(path, options, print);
if (doc.label === "member-chain") {
if (doc.label?.memberChain) {
return false;
}
+15 -8
View File
@@ -56,20 +56,19 @@ function printCallArguments(path, options, print) {
let anyArgEmptyLine = false;
const lastArgIndex = args.length - 1;
const printedArguments = [];
iterateCallArgumentsPath(path, (argPath, index) => {
const arg = argPath.node;
const parts = [print()];
iterateCallArgumentsPath(path, ({ node: arg }, index) => {
let argDoc = print();
if (index === lastArgIndex) {
// do nothing
} else if (isNextLineEmpty(arg, options)) {
anyArgEmptyLine = true;
parts.push(",", hardline, hardline);
argDoc = [argDoc, ",", hardline, hardline];
} else {
parts.push(",", line);
argDoc = [argDoc, ",", line];
}
printedArguments.push(parts);
printedArguments.push(argDoc);
});
// Dynamic imports cannot have trailing commas
@@ -128,7 +127,7 @@ function printCallArguments(path, options, print) {
]);
}
if (shouldExpandLastArg(args, options)) {
if (shouldExpandLastArg(args, printedArguments, options)) {
const headArgs = printedArguments.slice(0, -1);
if (headArgs.some(willBreak)) {
return allArgsBrokenOut();
@@ -221,8 +220,16 @@ function couldExpandArg(arg, arrowChainRecursion = false) {
);
}
function shouldExpandLastArg(args, options) {
function shouldExpandLastArg(args, argDocs, options) {
const lastArg = args.at(-1);
if (args.length === 1) {
const lastArgDoc = argDocs.at(-1);
if (lastArgDoc.label?.embed && lastArgDoc.label?.hug !== false) {
return true;
}
}
const penultimateArg = args.at(-2);
return (
!hasComment(lastArg, CommentCheckFlags.Leading) &&
+27 -22
View File
@@ -15,40 +15,45 @@ import printCallArguments from "./call-arguments.js";
import { printOptionalToken, printFunctionTypeParameters } from "./misc.js";
function printCallExpression(path, options, print) {
const { node } = path;
const parentNode = path.getParentNode();
const { node, parent } = path;
const isNew = node.type === "NewExpression";
const isDynamicImport = node.type === "ImportExpression";
const optional = printOptionalToken(path);
const args = getCallArguments(node);
const isTemplateLiteralSingleArg =
args.length === 1 && isTemplateOnItsOwnLine(args[0], options.originalText);
if (
isTemplateLiteralSingleArg ||
// Dangling comments are not handled, all these special cases should have arguments #9668
args.length > 0 &&
// We want to keep CommonJS- and AMD-style require calls, and AMD-style
// define calls, as a unit.
// e.g. `define(["some/lib"], (lib) => {`
((!isDynamicImport && !isNew && isCommonsJsOrAmdCall(node, parentNode)) ||
// Template literals as single arguments
(args.length === 1 &&
isTemplateOnItsOwnLine(args[0], options.originalText)) ||
// Keep test declarations on a single line
// e.g. `it('long name', () => {`
(!isNew && isTestCall(node, parentNode)))
(args.length > 0 &&
!isNew &&
!isDynamicImport &&
// We want to keep CommonJS- and AMD-style require calls, and AMD-style
// define calls, as a unit.
// e.g. `define(["some/lib"], (lib) => {`
(isCommonsJsOrAmdCall(node, parent) ||
// Keep test declarations on a single line
// e.g. `it('long name', () => {`
isTestCall(node, parent)))
) {
const printed = [];
iterateCallArgumentsPath(path, () => {
printed.push(print());
});
return [
isNew ? "new " : "",
print("callee"),
optional,
printFunctionTypeParameters(path, options, print),
"(",
join(", ", printed),
")",
];
if (!(isTemplateLiteralSingleArg && printed[0].label?.embed)) {
return [
isNew ? "new " : "",
print("callee"),
optional,
printFunctionTypeParameters(path, options, print),
"(",
join(", ", printed),
")",
];
}
}
// Inline Flow annotation comments following Identifiers in Call nodes need to
+3 -1
View File
@@ -340,7 +340,9 @@ function printArrowFunction(path, options, print, args) {
node.body.type === "ObjectExpression" ||
node.body.type === "BlockStatement" ||
isJsxNode(node.body) ||
isTemplateOnItsOwnLine(node.body, options.originalText) ||
(body[0].label?.hug !== false &&
(body[0].label?.embed ||
isTemplateOnItsOwnLine(node.body, options.originalText))) ||
node.body.type === "ArrowFunctionExpression" ||
node.body.type === "DoExpression")
) {
+1 -1
View File
@@ -399,7 +399,7 @@ function printMemberChain(path, options, print) {
];
}
return label("member-chain", result);
return label({ memberChain: true }, result);
}
export default printMemberChain;
+2 -2
View File
@@ -40,9 +40,9 @@ function printMemberExpression(path, options, print) {
(node.object.type === "TSNonNullExpression" &&
isCallExpression(node.object.expression) &&
node.object.expression.arguments.length > 0) ||
objectDoc.label === "member-chain"));
objectDoc.label?.memberChain));
return label(objectDoc.label === "member-chain" ? "member-chain" : "member", [
return label(objectDoc.label, [
objectDoc,
shouldInline ? lookupDoc : group(indent([softline, lookupDoc])),
]);
+12
View File
@@ -8,6 +8,7 @@ import {
align,
lineSuffixBoundary,
addAlignmentToDoc,
label,
} from "../../document/builders.js";
import { printDocToString } from "../../document/printer.js";
import { mapDoc } from "../../document/utils.js";
@@ -106,6 +107,16 @@ function printTemplateLiteral(path, print, options) {
return parts;
}
function printTaggedTemplateLiteral(print) {
const quasiDoc = print("quasi");
return label(quasiDoc.label && { tagged: true, ...quasiDoc.label }, [
print("tag"),
print("typeParameters"),
lineSuffixBoundary,
quasiDoc,
]);
}
function printJestEachTemplateLiteral(path, options, print) {
/**
* a | b | expected
@@ -229,6 +240,7 @@ function uncookTemplateElementValue(cookedValue) {
export {
printTemplateLiteral,
printTaggedTemplateLiteral,
printTemplateExpressions,
escapeTemplateCharacters,
uncookTemplateElementValue,
+5 -2
View File
@@ -58,7 +58,10 @@ import {
printModuleSpecifier,
} from "./print/module.js";
import { printTernary } from "./print/ternary.js";
import { printTemplateLiteral } from "./print/template-literal.js";
import {
printTaggedTemplateLiteral,
printTemplateLiteral,
} from "./print/template-literal.js";
import { printArray } from "./print/array.js";
import { printObject } from "./print/object.js";
import {
@@ -755,7 +758,7 @@ function printPathNoParens(path, options, print, args) {
case "TemplateLiteral":
return printTemplateLiteral(path, print, options);
case "TaggedTemplateExpression":
return [print("tag"), print("typeParameters"), print("quasi")];
return printTaggedTemplateLiteral(print);
case "PrivateIdentifier":
return ["#", node.name];
case "PrivateName":
+7 -1
View File
@@ -7,6 +7,7 @@ import {
lineSuffix,
join,
cursor,
label,
} from "../document/builders.js";
import {
hasNewline,
@@ -585,7 +586,12 @@ function printComments(path, doc, options, ignored) {
if (!leading && !trailing) {
return doc;
}
return [leading, doc, trailing];
return label(
// Propagate object labels so that the printing logic for ancestor nodes
// could easily check them.
typeof doc.label === "object" && { commented: true, ...doc.label },
[leading, doc, trailing]
);
}
function ensureAllCommentsPrinted(astComments) {
-1
View File
@@ -28,7 +28,6 @@ const unstableTests = new Map(
"js/comments/tagged-template-literal.js",
"markdown/spec/example-234.md",
"markdown/spec/example-235.md",
"html/multiparser/js/script-tag-escaping.html",
[
"js/multiparser-markdown/codeblock.js",
(options) => options.proseWrap === "always",
@@ -291,6 +291,67 @@ someReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReal
================================================================================
`;
exports[`embed.js format 1`] = `
====================================options=====================================
parsers: ["babel", "flow"]
printWidth: 80
| printWidth
=====================================input======================================
foo(/* HTML */ \`<!-- bar1 --> bar <!-- bar2 -->\`);
foo(/* HTML */ \` <!-- bar1 --> bar <!-- bar2 --> \`);
foo(/* HTML */ \`<div><p>bar</p>foo</div>\`);
foo(/* HTML */ \` <div><p>bar</p>foo</div> \`);
foo(/* GraphQL */ \`query { foo { bar } }\`);
foo(/* ... */ css\`color:magenta\`);
const a = b => /* HTML */ \`<!-- bar1 --> bar <!-- bar2 -->\`
const c = b => /* HTML */ \` <!-- bar1 --> bar <!-- bar2 --> \`
=====================================output=====================================
foo(
/* HTML */ \`<!-- bar1 -->
bar
<!-- bar2 -->\`,
);
foo(/* HTML */ \`
<!-- bar1 -->
bar
<!-- bar2 -->
\`);
foo(
/* HTML */ \`<div>
<p>bar</p>
foo
</div>\`,
);
foo(/* HTML */ \`
<div>
<p>bar</p>
foo
</div>
\`);
foo(/* GraphQL */ \`
query {
foo {
bar
}
}
\`);
foo(/* ... */ css\`
color: magenta;
\`);
const a = (b) =>
/* HTML */ \`<!-- bar1 -->
bar
<!-- bar2 -->\`;
const c = (b) => /* HTML */ \`
<!-- bar1 -->
bar
<!-- bar2 -->
\`;
================================================================================
`;
exports[`empty-lines.js format 1`] = `
====================================options=====================================
parsers: ["babel", "flow"]
@@ -0,0 +1,8 @@
foo(/* HTML */ `<!-- bar1 --> bar <!-- bar2 -->`);
foo(/* HTML */ ` <!-- bar1 --> bar <!-- bar2 --> `);
foo(/* HTML */ `<div><p>bar</p>foo</div>`);
foo(/* HTML */ ` <div><p>bar</p>foo</div> `);
foo(/* GraphQL */ `query { foo { bar } }`);
foo(/* ... */ css`color:magenta`);
const a = b => /* HTML */ `<!-- bar1 --> bar <!-- bar2 -->`
const c = b => /* HTML */ ` <!-- bar1 --> bar <!-- bar2 --> `
@@ -165,3 +165,26 @@ graphql.experimental\`
================================================================================
`;
exports[`tagged.js format 1`] = `
====================================options=====================================
parsers: ["babel", "flow"]
printWidth: 80
| printWidth
=====================================input======================================
foo(html // oops
\` <div><p>bar</p>foo</div> \`);
=====================================output=====================================
foo(
html // oops
\`
<div>
<p>bar</p>
foo
</div>
\`,
);
================================================================================
`;
@@ -0,0 +1,2 @@
foo(html // oops
` <div><p>bar</p>foo</div> `);
@@ -67,13 +67,11 @@ css\`
md\`\`;
graphql(
\`
{
id
}
\`,
);
graphql(\`
{
id
}
\`);
html\`<a></a>\`;