Fix string print (#18351)

This commit is contained in:
fisker Cheung authored and GitHub committed 2025-11-28 20:52:53 +08:00
1 parent 8785c6e3af
commit 3e770b95a7
7 files changed
+193 -9

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
#### Fix string print when switching quotes (#18351 by @fisker)
<!-- prettier-ignore -->
```jsx
// Input
console.log("A descriptor\\'s .kind must be \"method\" or \"field\".")
// Prettier stable
console.log('A descriptor\\'s .kind must be "method" or "field".');
// Prettier main
console.log('A descriptor\\\'s .kind must be "method" or "field".');
```
+21 -9
View File
@@ -1,7 +1,8 @@
/** @import {Quote} from "./get-preferred-quote.js" */
// Matches _any_ escape and unescaped quotes (both single and double).
const REGEX = /\\?(?<quote>["'])/gu;
// Matching `\` in first group, so we won't treat `\\"` as escaped quote.
const REGEX = /\\(["'\\])|(["'])/gu;
/**
* @param {string} rawText
@@ -9,14 +10,25 @@ const REGEX = /\\?(?<quote>["'])/gu;
* @returns {string}
*/
function makeString(rawText, enclosingQuote) {
return (
enclosingQuote +
rawText.replaceAll(
REGEX,
(_, quote) => (quote === enclosingQuote ? "\\" : "") + quote,
) +
enclosingQuote
);
const otherQuote = enclosingQuote === '"' ? "'" : '"';
// Escape and unescape single and double quotes as needed to be able to
// enclose `rawText` with `enclosingQuote`.
const raw = rawText.replaceAll(REGEX, (match, escaped, unescaped) => {
// If we matched an escape, and the escaped character is a quote of the
// other type than we intend to enclose the string with, there's no need for
// it to be escaped, so return it _without_ the backslash.
if (escaped) {
return escaped === otherQuote ? otherQuote : match;
}
// If we matched an unescaped quote and it is of the _same_ type as we
// intend to enclose the string with, it must be escaped, so return it with
// a backslash.
return unescaped === enclosingQuote ? "\\" + unescaped : unescaped;
});
return enclosingQuote + raw + enclosingQuote;
}
export default makeString;
@@ -0,0 +1,53 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`switch-quote.js format 1`] = `
====================================options=====================================
parsers: ["babel", "typescript", "flow"]
printWidth: 80
| printWidth
=====================================input======================================
console.log([
// 18351
"A descriptor\\\\'s .kind must be \\"method\\" or \\"field\\".",
// Will switch to single quote
"0 backslash [ ' ] \\"\\"",
"0 backslash [ \\' ] \\"\\"",
"1 backslash [ \\\\' ] \\"\\"",
"1 backslash [ \\\\\\' ] \\"\\"",
// Other charectors
"\\\\'\\"\\" [ \\ ]",
"\\\\'\\"\\" [ \\\\ ]",
"\\\\'\\"\\" [ \\\\\\ ]",
"\\\\'\\"\\" [ \\\\\\\\ ]",
"\\\\'\\"\\" [ \\n ]",
"\\\\'\\"\\" [ \\\\n ]",
"\\\\'\\"\\" [ \\\\\\n ]",
"\\\\'\\"\\" [ \\\\\\\\n ]",
]);
=====================================output=====================================
console.log([
// 18351
'A descriptor\\\\\\'s .kind must be "method" or "field".',
// Will switch to single quote
'0 backslash [ \\' ] ""',
'0 backslash [ \\' ] ""',
'1 backslash [ \\\\\\' ] ""',
'1 backslash [ \\\\\\' ] ""',
// Other charectors
'\\\\\\'"" [ \\ ]',
'\\\\\\'"" [ \\\\ ]',
'\\\\\\'"" [ \\\\\\ ]',
'\\\\\\'"" [ \\\\\\\\ ]',
'\\\\\\'"" [ \\n ]',
'\\\\\\'"" [ \\\\n ]',
'\\\\\\'"" [ \\\\\\n ]',
'\\\\\\'"" [ \\\\\\\\n ]',
]);
================================================================================
`;
@@ -0,0 +1 @@
runFormatTest(import.meta, ["babel", "typescript", "flow"]);
@@ -0,0 +1,20 @@
console.log([
// 18351
"A descriptor\\'s .kind must be \"method\" or \"field\".",
// Will switch to single quote
"0 backslash [ ' ] \"\"",
"0 backslash [ \' ] \"\"",
"1 backslash [ \\' ] \"\"",
"1 backslash [ \\\' ] \"\"",
// Other charectors
"\\'\"\" [ \ ]",
"\\'\"\" [ \\ ]",
"\\'\"\" [ \\\ ]",
"\\'\"\" [ \\\\ ]",
"\\'\"\" [ \n ]",
"\\'\"\" [ \\n ]",
"\\'\"\" [ \\\n ]",
"\\'\"\" [ \\\\n ]",
]);
@@ -225,6 +225,39 @@ test("sharedUtil.makeString", () => {
expect(makeString(`a${DOUBLE_QUOTE}`, SINGLE_QUOTE)).toBe(
`${SINGLE_QUOTE}a${DOUBLE_QUOTE}${SINGLE_QUOTE}`,
);
expect(makeString(`a${DOUBLE_QUOTE}`, DOUBLE_QUOTE)).toBe(
`${DOUBLE_QUOTE}a\\${DOUBLE_QUOTE}${DOUBLE_QUOTE}`,
);
expect(makeString(`a\\${DOUBLE_QUOTE}\\${SINGLE_QUOTE}`, DOUBLE_QUOTE)).toBe(
`${DOUBLE_QUOTE}a\\${DOUBLE_QUOTE}${SINGLE_QUOTE}${DOUBLE_QUOTE}`,
);
expect(makeString(`a\\${DOUBLE_QUOTE}\\${SINGLE_QUOTE}`, SINGLE_QUOTE)).toBe(
`${SINGLE_QUOTE}a${DOUBLE_QUOTE}\\${SINGLE_QUOTE}${SINGLE_QUOTE}`,
);
expect(
makeString(`a\\\\${DOUBLE_QUOTE}\\\\${SINGLE_QUOTE}`, DOUBLE_QUOTE),
).toBe(
`${DOUBLE_QUOTE}a\\\\\\${DOUBLE_QUOTE}\\\\${SINGLE_QUOTE}${DOUBLE_QUOTE}`,
);
expect(
makeString(`a\\\\${DOUBLE_QUOTE}\\\\${SINGLE_QUOTE}`, SINGLE_QUOTE),
).toBe(
`${SINGLE_QUOTE}a\\\\${DOUBLE_QUOTE}\\\\\\${SINGLE_QUOTE}${SINGLE_QUOTE}`,
);
expect(
makeString(`a\\\\\\${DOUBLE_QUOTE}\\\\\\${SINGLE_QUOTE}`, DOUBLE_QUOTE),
).toBe(
`${DOUBLE_QUOTE}a\\\\\\${DOUBLE_QUOTE}\\\\${SINGLE_QUOTE}${DOUBLE_QUOTE}`,
);
expect(
makeString(`a\\\\\\${DOUBLE_QUOTE}\\\\\\${SINGLE_QUOTE}`, SINGLE_QUOTE),
).toBe(
`${SINGLE_QUOTE}a\\\\${DOUBLE_QUOTE}\\\\\\${SINGLE_QUOTE}${SINGLE_QUOTE}`,
);
expect(
makeString(
String.raw`\a`,
+52
View File
@@ -0,0 +1,52 @@
import makeString from "../../src/utils/make-string.js";
const DOUBLE_QUOTE = '"';
const SINGLE_QUOTE = "'";
test("makeString", () => {
expect(makeString("a", DOUBLE_QUOTE)).toBe(`${DOUBLE_QUOTE}a${DOUBLE_QUOTE}`);
expect(makeString("a", SINGLE_QUOTE)).toBe(`${SINGLE_QUOTE}a${SINGLE_QUOTE}`);
expect(makeString(`a${DOUBLE_QUOTE}`, DOUBLE_QUOTE)).toBe(
`${DOUBLE_QUOTE}a\\${DOUBLE_QUOTE}${DOUBLE_QUOTE}`,
);
expect(makeString(`a${DOUBLE_QUOTE}`, SINGLE_QUOTE)).toBe(
`${SINGLE_QUOTE}a${DOUBLE_QUOTE}${SINGLE_QUOTE}`,
);
expect(makeString(`a${DOUBLE_QUOTE}`, DOUBLE_QUOTE)).toBe(
`${DOUBLE_QUOTE}a\\${DOUBLE_QUOTE}${DOUBLE_QUOTE}`,
);
expect(makeString(`a\\${DOUBLE_QUOTE}\\${SINGLE_QUOTE}`, DOUBLE_QUOTE)).toBe(
`${DOUBLE_QUOTE}a\\${DOUBLE_QUOTE}${SINGLE_QUOTE}${DOUBLE_QUOTE}`,
);
expect(makeString(`a\\${DOUBLE_QUOTE}\\${SINGLE_QUOTE}`, SINGLE_QUOTE)).toBe(
`${SINGLE_QUOTE}a${DOUBLE_QUOTE}\\${SINGLE_QUOTE}${SINGLE_QUOTE}`,
);
expect(
makeString(`a\\\\${DOUBLE_QUOTE}\\\\${SINGLE_QUOTE}`, DOUBLE_QUOTE),
).toBe(
`${DOUBLE_QUOTE}a\\\\\\${DOUBLE_QUOTE}\\\\${SINGLE_QUOTE}${DOUBLE_QUOTE}`,
);
expect(
makeString(`a\\\\${DOUBLE_QUOTE}\\\\${SINGLE_QUOTE}`, SINGLE_QUOTE),
).toBe(
`${SINGLE_QUOTE}a\\\\${DOUBLE_QUOTE}\\\\\\${SINGLE_QUOTE}${SINGLE_QUOTE}`,
);
expect(
makeString(`a\\\\\\${DOUBLE_QUOTE}\\\\\\${SINGLE_QUOTE}`, DOUBLE_QUOTE),
).toBe(
`${DOUBLE_QUOTE}a\\\\\\${DOUBLE_QUOTE}\\\\${SINGLE_QUOTE}${DOUBLE_QUOTE}`,
);
expect(
makeString(`a\\\\\\${DOUBLE_QUOTE}\\\\\\${SINGLE_QUOTE}`, SINGLE_QUOTE),
).toBe(
`${SINGLE_QUOTE}a\\\\${DOUBLE_QUOTE}\\\\\\${SINGLE_QUOTE}${SINGLE_QUOTE}`,
);
// Do not unescape `\a`
expect(makeString(String.raw`\a`, SINGLE_QUOTE)).toBe(
String.raw`${SINGLE_QUOTE}\a${SINGLE_QUOTE}`,
);
});