Fix unstable embedded template literals (#13532)
This commit is contained in:
1 parent
822f354ceb
commit
6d4d011252
22 files changed
+243
-62
No files matched your search
@@ -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).
|
||||
@@ -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
@@ -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_
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
) {
|
||||
|
||||
@@ -399,7 +399,7 @@ function printMemberChain(path, options, print) {
|
||||
];
|
||||
}
|
||||
|
||||
return label("member-chain", result);
|
||||
return label({ memberChain: true }, result);
|
||||
}
|
||||
|
||||
export default printMemberChain;
|
||||
@@ -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])),
|
||||
]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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> `);
|
||||
+5
-7
@@ -67,13 +67,11 @@ css\`
|
||||
|
||||
md\`\`;
|
||||
|
||||
graphql(
|
||||
\`
|
||||
{
|
||||
id
|
||||
}
|
||||
\`,
|
||||
);
|
||||
graphql(\`
|
||||
{
|
||||
id
|
||||
}
|
||||
\`);
|
||||
|
||||
html\`<a></a>\`;
|
||||
|
||||
|
||||
Reference in new issue
Block a user