Don't break simple template literals (#5979)
A simple template literal is initially defined as a literal wherein all the expressions are identifiers or member access expressions where all parts are identifiers. We print these expressions into strings with infinite print width before printing the template.master
parent
b44748eecb
commit
6cee80b5b5
|
@ -188,3 +188,27 @@ Examples:
|
||||||
width,
|
width,
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- JavaScript: Don't break simple template literals ([#5979] by [@jwbay])
|
||||||
|
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
```js
|
||||||
|
// Input
|
||||||
|
console.log(chalk.white(`Covered Lines below threshold: ${coverageSettings.lines}%. Actual: ${coverageSummary.total.lines.pct}%`))
|
||||||
|
|
||||||
|
// Output (Prettier stable)
|
||||||
|
console.log(
|
||||||
|
chalk.white(
|
||||||
|
`Covered Lines below threshold: ${coverageSettings.lines}%. Actual: ${
|
||||||
|
coverageSummary.total.lines.pct
|
||||||
|
}%`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Output (Prettier master)
|
||||||
|
console.log(
|
||||||
|
chalk.white(
|
||||||
|
`Covered Lines below threshold: ${coverageSettings.lines}%. Actual: ${coverageSummary.total.lines.pct}%`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
|
@ -2364,125 +2364,27 @@ function printPathNoParens(path, options, print, args) {
|
||||||
case "TemplateElement":
|
case "TemplateElement":
|
||||||
return join(literalline, n.value.raw.split(/\r?\n/g));
|
return join(literalline, n.value.raw.split(/\r?\n/g));
|
||||||
case "TemplateLiteral": {
|
case "TemplateLiteral": {
|
||||||
const expressions = path.map(print, "expressions");
|
let expressions = path.map(print, "expressions");
|
||||||
const parentNode = path.getParentNode();
|
const parentNode = path.getParentNode();
|
||||||
|
|
||||||
/**
|
if (isJestEachTemplateLiteral(n, parentNode)) {
|
||||||
* describe.each`table`(name, fn)
|
const printed = printJestEachTemplateLiteral(n, expressions, options);
|
||||||
* describe.only.each`table`(name, fn)
|
if (printed) {
|
||||||
* describe.skip.each`table`(name, fn)
|
return printed;
|
||||||
* test.each`table`(name, fn)
|
}
|
||||||
* test.only.each`table`(name, fn)
|
}
|
||||||
* test.skip.each`table`(name, fn)
|
|
||||||
*
|
const isSimple = isSimpleTemplateLiteral(n);
|
||||||
* Ref: https://github.com/facebook/jest/pull/6102
|
if (isSimple) {
|
||||||
*/
|
expressions = expressions.map(
|
||||||
const jestEachTriggerRegex = /^[xf]?(describe|it|test)$/;
|
|
||||||
if (
|
|
||||||
parentNode.type === "TaggedTemplateExpression" &&
|
|
||||||
parentNode.quasi === n &&
|
|
||||||
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)))
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* a | b | expected
|
|
||||||
* ${1} | ${1} | ${2}
|
|
||||||
* ${1} | ${2} | ${3}
|
|
||||||
* ${2} | ${1} | ${3}
|
|
||||||
*/
|
|
||||||
const headerNames = n.quasis[0].value.raw.trim().split(/\s*\|\s*/);
|
|
||||||
if (
|
|
||||||
headerNames.length > 1 ||
|
|
||||||
headerNames.some(headerName => headerName.length !== 0)
|
|
||||||
) {
|
|
||||||
const stringifiedExpressions = expressions.map(
|
|
||||||
doc =>
|
doc =>
|
||||||
"${" +
|
|
||||||
printDocToString(
|
printDocToString(
|
||||||
doc,
|
doc,
|
||||||
Object.assign({}, options, {
|
Object.assign({}, options, {
|
||||||
printWidth: Infinity,
|
printWidth: Infinity
|
||||||
endOfLine: "lf"
|
|
||||||
})
|
})
|
||||||
).formatted +
|
).formatted
|
||||||
"}"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const tableBody = [{ hasLineBreak: false, cells: [] }];
|
|
||||||
for (let i = 1; i < n.quasis.length; i++) {
|
|
||||||
const row = tableBody[tableBody.length - 1];
|
|
||||||
const correspondingExpression = stringifiedExpressions[i - 1];
|
|
||||||
|
|
||||||
row.cells.push(correspondingExpression);
|
|
||||||
if (correspondingExpression.indexOf("\n") !== -1) {
|
|
||||||
row.hasLineBreak = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n.quasis[i].value.raw.indexOf("\n") !== -1) {
|
|
||||||
tableBody.push({ hasLineBreak: false, cells: [] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxColumnCount = tableBody.reduce(
|
|
||||||
(maxColumnCount, row) => Math.max(maxColumnCount, row.cells.length),
|
|
||||||
headerNames.length
|
|
||||||
);
|
|
||||||
|
|
||||||
const maxColumnWidths = Array.from(
|
|
||||||
new Array(maxColumnCount),
|
|
||||||
() => 0
|
|
||||||
);
|
|
||||||
const table = [{ cells: headerNames }].concat(
|
|
||||||
tableBody.filter(row => row.cells.length !== 0)
|
|
||||||
);
|
|
||||||
table
|
|
||||||
.filter(row => !row.hasLineBreak)
|
|
||||||
.forEach(row => {
|
|
||||||
row.cells.forEach((cell, index) => {
|
|
||||||
maxColumnWidths[index] = Math.max(
|
|
||||||
maxColumnWidths[index],
|
|
||||||
getStringWidth(cell)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
parts.push(
|
|
||||||
"`",
|
|
||||||
indent(
|
|
||||||
concat([
|
|
||||||
hardline,
|
|
||||||
join(
|
|
||||||
hardline,
|
|
||||||
table.map(row =>
|
|
||||||
join(
|
|
||||||
" | ",
|
|
||||||
row.cells.map((cell, index) =>
|
|
||||||
row.hasLineBreak
|
|
||||||
? cell
|
|
||||||
: cell +
|
|
||||||
" ".repeat(
|
|
||||||
maxColumnWidths[index] - getStringWidth(cell)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
])
|
|
||||||
),
|
|
||||||
hardline,
|
|
||||||
"`"
|
|
||||||
);
|
|
||||||
return concat(parts);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push("`");
|
parts.push("`");
|
||||||
|
@ -2510,6 +2412,9 @@ function printPathNoParens(path, options, print, args) {
|
||||||
|
|
||||||
let printed = expressions[i];
|
let printed = expressions[i];
|
||||||
|
|
||||||
|
if (!isSimple) {
|
||||||
|
// Breaks at the template element boundaries (${ and }) are preferred to breaking
|
||||||
|
// in the middle of a MemberExpression
|
||||||
if (
|
if (
|
||||||
(n.expressions[i].comments && n.expressions[i].comments.length) ||
|
(n.expressions[i].comments && n.expressions[i].comments.length) ||
|
||||||
n.expressions[i].type === "MemberExpression" ||
|
n.expressions[i].type === "MemberExpression" ||
|
||||||
|
@ -2518,6 +2423,7 @@ function printPathNoParens(path, options, print, args) {
|
||||||
) {
|
) {
|
||||||
printed = concat([indent(concat([softline, printed])), softline]);
|
printed = concat([indent(concat([softline, printed])), softline]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const aligned =
|
const aligned =
|
||||||
indentSize === 0 && quasi.value.raw.endsWith("\n")
|
indentSize === 0 && quasi.value.raw.endsWith("\n")
|
||||||
|
@ -3863,6 +3769,172 @@ function isSimpleFlowType(node) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
* ${1} | ${1} | ${2}
|
||||||
|
* ${1} | ${2} | ${3}
|
||||||
|
* ${2} | ${1} | ${3}
|
||||||
|
*/
|
||||||
|
const headerNames = node.quasis[0].value.raw.trim().split(/\s*\|\s*/);
|
||||||
|
if (
|
||||||
|
headerNames.length > 1 ||
|
||||||
|
headerNames.some(headerName => headerName.length !== 0)
|
||||||
|
) {
|
||||||
|
const parts = [];
|
||||||
|
const stringifiedExpressions = expressions.map(
|
||||||
|
doc =>
|
||||||
|
"${" +
|
||||||
|
printDocToString(
|
||||||
|
doc,
|
||||||
|
Object.assign({}, options, {
|
||||||
|
printWidth: Infinity,
|
||||||
|
endOfLine: "lf"
|
||||||
|
})
|
||||||
|
).formatted +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableBody = [{ hasLineBreak: false, cells: [] }];
|
||||||
|
for (let i = 1; i < node.quasis.length; i++) {
|
||||||
|
const row = tableBody[tableBody.length - 1];
|
||||||
|
const correspondingExpression = stringifiedExpressions[i - 1];
|
||||||
|
|
||||||
|
row.cells.push(correspondingExpression);
|
||||||
|
if (correspondingExpression.indexOf("\n") !== -1) {
|
||||||
|
row.hasLineBreak = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.quasis[i].value.raw.indexOf("\n") !== -1) {
|
||||||
|
tableBody.push({ hasLineBreak: false, cells: [] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxColumnCount = tableBody.reduce(
|
||||||
|
(maxColumnCount, row) => Math.max(maxColumnCount, row.cells.length),
|
||||||
|
headerNames.length
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxColumnWidths = Array.from(new Array(maxColumnCount), () => 0);
|
||||||
|
const table = [{ cells: headerNames }].concat(
|
||||||
|
tableBody.filter(row => row.cells.length !== 0)
|
||||||
|
);
|
||||||
|
table
|
||||||
|
.filter(row => !row.hasLineBreak)
|
||||||
|
.forEach(row => {
|
||||||
|
row.cells.forEach((cell, index) => {
|
||||||
|
maxColumnWidths[index] = Math.max(
|
||||||
|
maxColumnWidths[index],
|
||||||
|
getStringWidth(cell)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
parts.push(
|
||||||
|
"`",
|
||||||
|
indent(
|
||||||
|
concat([
|
||||||
|
hardline,
|
||||||
|
join(
|
||||||
|
hardline,
|
||||||
|
table.map(row =>
|
||||||
|
join(
|
||||||
|
" | ",
|
||||||
|
row.cells.map((cell, index) =>
|
||||||
|
row.hasLineBreak
|
||||||
|
? cell
|
||||||
|
: cell +
|
||||||
|
" ".repeat(maxColumnWidths[index] - getStringWidth(cell))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
])
|
||||||
|
),
|
||||||
|
hardline,
|
||||||
|
"`"
|
||||||
|
);
|
||||||
|
return concat(parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const functionCompositionFunctionNames = new Set([
|
const functionCompositionFunctionNames = new Set([
|
||||||
"pipe", // RxJS, Ramda
|
"pipe", // RxJS, Ramda
|
||||||
"pipeP", // Ramda
|
"pipeP", // Ramda
|
||||||
|
|
|
@ -88,6 +88,11 @@ const description =
|
||||||
|
|
||||||
const foo = \`such a long template string \${foo.bar.baz} that prettier will want to wrap it\`;
|
const foo = \`such a long template string \${foo.bar.baz} that prettier will want to wrap it\`;
|
||||||
|
|
||||||
|
const shouldWrapForNow = \`such a long template string \${foo().bar.baz} that prettier will want to wrap it\`;
|
||||||
|
|
||||||
|
const shouldNotWrap = \`simple expressions should not break \${this} \${variable} \${a.b.c} \${this.b.c} \${a[b].c} \${a.b[c]} \${a.b['c']} \${a?.b?.c}\`;
|
||||||
|
|
||||||
|
console.log(chalk.white(\`Covered Lines below threshold: \${coverageSettings.lines}%. Actual: \${coverageSummary.total.lines.pct}%\`))
|
||||||
|
|
||||||
x = \`mdl-textfield mdl-js-textfield \${className} \${content.length > 0
|
x = \`mdl-textfield mdl-js-textfield \${className} \${content.length > 0
|
||||||
? 'is-dirty'
|
? 'is-dirty'
|
||||||
|
@ -125,22 +130,26 @@ const long = \`long \${
|
||||||
} long longlong \${a.b.c.d.e} long longlong \${a.b.c.d.e} long longlong \${
|
} long longlong \${a.b.c.d.e} long longlong \${a.b.c.d.e} long longlong \${
|
||||||
a.b.c.d.e
|
a.b.c.d.e
|
||||||
} long long\`;
|
} long long\`;
|
||||||
const long = \`long \${
|
const long = \`long \${a.b.c.d.e} long longlong \${loooooooooooooooooong} long longlong \${loooooooooooooooooong} long longlong \${loooooooooooooooooong} long long\`;
|
||||||
a.b.c.d.e
|
|
||||||
} long longlong \${loooooooooooooooooong} long longlong \${loooooooooooooooooong} long longlong \${loooooooooooooooooong} long long\`;
|
|
||||||
|
|
||||||
const long = \`long long long long long long long long long long long \${
|
const long = \`long long long long long long long long long long long \${a.b.c.d.e} long long long long long long long long long long long long long\`;
|
||||||
a.b.c.d.e
|
|
||||||
} long long long long long long long long long long long long long\`;
|
|
||||||
|
|
||||||
const description = \`The value of the \${cssName} css of the \${
|
const description = \`The value of the \${cssName} css of the \${this._name} element\`;
|
||||||
this._name
|
|
||||||
} element\`;
|
|
||||||
|
|
||||||
const foo = \`such a long template string \${
|
const foo = \`such a long template string \${foo.bar.baz} that prettier will want to wrap it\`;
|
||||||
foo.bar.baz
|
|
||||||
|
const shouldWrapForNow = \`such a long template string \${
|
||||||
|
foo().bar.baz
|
||||||
} that prettier will want to wrap it\`;
|
} that prettier will want to wrap it\`;
|
||||||
|
|
||||||
|
const shouldNotWrap = \`simple expressions should not break \${this} \${variable} \${a.b.c} \${this.b.c} \${a[b].c} \${a.b[c]} \${a.b["c"]} \${a?.b?.c}\`;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
chalk.white(
|
||||||
|
\`Covered Lines below threshold: \${coverageSettings.lines}%. Actual: \${coverageSummary.total.lines.pct}%\`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
x = \`mdl-textfield mdl-js-textfield \${className} \${
|
x = \`mdl-textfield mdl-js-textfield \${className} \${
|
||||||
content.length > 0 ? "is-dirty" : ""
|
content.length > 0 ? "is-dirty" : ""
|
||||||
} combo-box__input\`;
|
} combo-box__input\`;
|
||||||
|
@ -160,9 +169,7 @@ function testing() {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
\`Trying update appcast for \${app.name} (\${app.cask.appcast}) -> (\${
|
\`Trying update appcast for \${app.name} (\${app.cask.appcast}) -> (\${app.cask.appcastGenerated})\`
|
||||||
app.cask.appcastGenerated
|
|
||||||
})\`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -170,9 +177,7 @@ console.log(
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
\`\\nApparently jetbrains changed the release artifact for \${app.name}@\${
|
\`\\nApparently jetbrains changed the release artifact for \${app.name}@\${app.jetbrains.version}.\\n\`
|
||||||
app.jetbrains.version
|
|
||||||
}.\\n\`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
descirbe("something", () => {
|
descirbe("something", () => {
|
||||||
|
|
|
@ -9,6 +9,11 @@ const description =
|
||||||
|
|
||||||
const foo = `such a long template string ${foo.bar.baz} that prettier will want to wrap it`;
|
const foo = `such a long template string ${foo.bar.baz} that prettier will want to wrap it`;
|
||||||
|
|
||||||
|
const shouldWrapForNow = `such a long template string ${foo().bar.baz} that prettier will want to wrap it`;
|
||||||
|
|
||||||
|
const shouldNotWrap = `simple expressions should not break ${this} ${variable} ${a.b.c} ${this.b.c} ${a[b].c} ${a.b[c]} ${a.b['c']} ${a?.b?.c}`;
|
||||||
|
|
||||||
|
console.log(chalk.white(`Covered Lines below threshold: ${coverageSettings.lines}%. Actual: ${coverageSummary.total.lines.pct}%`))
|
||||||
|
|
||||||
x = `mdl-textfield mdl-js-textfield ${className} ${content.length > 0
|
x = `mdl-textfield mdl-js-textfield ${className} ${content.length > 0
|
||||||
? 'is-dirty'
|
? 'is-dirty'
|
||||||
|
|
Loading…
Reference in New Issue