Repository: prettier/prettier (JavaScript) — Bug Fix

Issue:
Unstable property key print
**Prettier 3.2.5**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEBDABAXncAOlddXEAMwgmKXQEYAafQ4gI111QCdL0AmeqAXxC0QEAA4wAltADOyUB3YQA7gAUOCWSlQAbJagCes4U3aowAazgwAyqgC2cADISocZCR3S4x0xavXRMxcAc2QYdgBXbxAvOwkwyOi4AA9ROHYJB1gdABV0qA4JOE0PbS9haRDtOABFCIh4d09ogCtpZOsq2vrGpFLykABHHrgVRVFNcBkJaXhEYXDUCW0QgGEIOztUZDRtbSEYroBBGHCJJgj4FXTnVyay6IALGDttAHVHiXhpQLA4aw0XwkADcvvodmBpEYQMCogBJKAAEwQNjAGXERyR1hg+mq9wGokUXjeplEO0JxXSwLcwhcXnYMDGqGCW3x0UC7HpOyYqCYcH2wkJLhgbwkiJgj2QAA4AAzCdhwYYSBVMlnbPrNBa80XiyVIXggCJeHK8kqakBwOx8xHIxGOVBQYIRZlwABiEHYW1OIR2qEuEBA-H4QA)
<!-- prettier-ignore -->
```sh
--parser babel
--quote-props consistent
```

**Input:**
<!-- prettier-ignore -->
```jsx
a = {
  "foo": 1,
  "b\ar": 2,
}
```

**Output:**
<!-- prettier-ignore -->
```jsx
a = {
  "foo": 1,
  "bar": 2,
};

```

**Second Output:**
<!-- prettier-ignore -->
```jsx
a = {
  foo: 1,
  bar: 2,
};

```

**Expected output:**
<!-- prettier-ignore -->
```jsx
a = {
  foo: 1,
  bar: 2,
};
```

**Why?**

<!-- short explanation of expected output -->

Should be stable


Discussion:

Resolving patch:
diff --git a/changelog_unreleased/javascript/16058.md b/changelog_unreleased/javascript/16058.md
new file mode 100644
index 000000000000..f870d51b783d
--- /dev/null
+++ b/changelog_unreleased/javascript/16058.md
@@ -0,0 +1,16 @@
+#### Fix unstable object print (#16058 by @fisker)
+
+<!-- prettier-ignore -->
+```jsx
+// Input
+a = {"\a": 1, "b": 2}
+
+// Prettier stable (--quote-props consistent)
+a = { "a": 1, "b": 2 };
+
+// Prettier stable (--quote-props as-needed)
+a = { "a": 1, b: 2 };
+
+// Prettier main
+a = { a: 1, b: 2 };
+```
diff --git a/src/language-js/print/property.js b/src/language-js/print/property.js
index 569a40206c58..c62ad0a56a91 100644
--- a/src/language-js/print/property.js
+++ b/src/language-js/print/property.js
@@ -44,8 +44,7 @@ function isStringKeySafeToUnquote(node, options) {
     options.parser === "json" ||
     options.parser === "jsonc" ||
     !isStringLiteral(node.key) ||
-    // TODO[@fisker]: Use `printString` instead
-    rawText(node.key).slice(1, -1) !== node.key.value
+    printString(rawText(node.key), options).slice(1, -1) !== node.key.value
   ) {
     return false;
   }


Test patch:
diff --git a/tests/format/js/quote-props/__snapshots__/jsfmt.spec.js.snap b/tests/format/js/quote-props/__snapshots__/jsfmt.spec.js.snap
index 607faf8b47ce..2c612aa6d1c5 100644
--- a/tests/format/js/quote-props/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/format/js/quote-props/__snapshots__/jsfmt.spec.js.snap
@@ -293,32 +293,32 @@ printWidth: 80
 quoteProps: "as-needed"
                                                                                 | printWidth
 =====================================input======================================
-const a = {
+a = {
   a: "a"
 };
 
-const b = {
+a = {
   'b': "b"
 };
 
-const b2 = {
+a = {
   // Escapes should stay as escapes and not be unquoted.
   '\\u0062': "b",
   '\\u0031': "1"
 };
 
-const c = {
+a = {
   c1: "c1",
   'c2': "c2"
 };
 
-const d = {
+a = {
   d1: "d1",
   'd-2': "d2"
 };
 
 // None of these should become quoted, regardless of the quoteProps value.
-const e = {
+a = {
   NaN: null,
   1: null,
   1.5: null,
@@ -336,7 +336,7 @@ const e = {
   2n: null,
 }
 
-const f = {
+a = {
   // These should be unquoted for quoteProps=as-needed.
   "NaN": null,
   "1": null,
@@ -391,33 +391,38 @@ Object.entries({
   "-1.5": null,
 }
 
+a = {
+  "\\a": 1,
+  "b": 2
+}
+
 =====================================output=====================================
-const a = {
+a = {
   a: "a",
 };
 
-const b = {
+a = {
   b: "b",
 };
 
-const b2 = {
+a = {
   // Escapes should stay as escapes and not be unquoted.
   "\\u0062": "b",
   "\\u0031": "1",
 };
 
-const c = {
+a = {
   c1: "c1",
   c2: "c2",
 };
 
-const d = {
+a = {
   d1: "d1",
   "d-2": "d2",
 };
 
 // None of these should become quoted, regardless of the quoteProps value.
-const e = {
+a = {
   NaN: null,
   1: null,
   1.5: null,
@@ -435,7 +440,7 @@ const e = {
   2n: null,
 };
 
-const f = {
+a = {
   // These should be unquoted for quoteProps=as-needed.
   NaN: null,
   1: null,
@@ -490,6 +495,11 @@ Object.entries({
   "-1.5": null,
 };
 
+a = {
+  a: 1,
+  b: 2,
+};
+
 ================================================================================
 `;
 
@@ -501,32 +511,32 @@ quoteProps: "consistent"
 singleQuote: true
                                                                                 | printWidth
 =====================================input======================================
-const a = {
+a = {
   a: "a"
 };
 
-const b = {
+a = {
   'b': "b"
 };
 
-const b2 = {
+a = {
   // Escapes should stay as escapes and not be unquoted.
   '\\u0062': "b",
   '\\u0031': "1"
 };
 
-const c = {
+a = {
   c1: "c1",
   'c2': "c2"
 };
 
-const d = {
+a = {
   d1: "d1",
   'd-2': "d2"
 };
 
 // None of these should become quoted, regardless of the quoteProps value.
-const e = {
+a = {
   NaN: null,
   1: null,
   1.5: null,
@@ -544,7 +554,7 @@ const e = {
   2n: null,
 }
 
-const f = {
+a = {
   // These should be unquoted for quoteProps=as-needed.
   "NaN": null,
   "1": null,
@@ -599,33 +609,38 @@ Object.entries({
   "-1.5": null,
 }
 
+a = {
+  "\\a": 1,
+  "b": 2
+}
+
 =====================================output=====================================
-const a = {
+a = {
   a: 'a',
 };
 
-const b = {
+a = {
   b: 'b',
 };
 
-const b2 = {
+a = {
   // Escapes should stay as escapes and not be unquoted.
   '\\u0062': 'b',
   '\\u0031': '1',
 };
 
-const c = {
+a = {
   c1: 'c1',
   c2: 'c2',
 };
 
-const d = {
+a = {
   'd1': 'd1',
   'd-2': 'd2',
 };
 
 // None of these should become quoted, regardless of the quoteProps value.
-const e = {
+a = {
   NaN: null,
   1: null,
   1.5: null,
@@ -643,7 +658,7 @@ const e = {
   2n: null,
 };
 
-const f = {
+a = {
   // These should be unquoted for quoteProps=as-needed.
   'NaN': null,
   '1': null,
@@ -698,6 +713,11 @@ Object.entries({
   '-1.5': null,
 };
 
+a = {
+  a: 1,
+  b: 2,
+};
+
 ================================================================================
 `;
 
@@ -708,32 +728,32 @@ printWidth: 80
 quoteProps: "consistent"
                                                                                 | printWidth
 =====================================input======================================
-const a = {
+a = {
   a: "a"
 };
 
-const b = {
+a = {
   'b': "b"
 };
 
-const b2 = {
+a = {
   // Escapes should stay as escapes and not be unquoted.
   '\\u0062': "b",
   '\\u0031': "1"
 };
 
-const c = {
+a = {
   c1: "c1",
   'c2': "c2"
 };
 
-const d = {
+a = {
   d1: "d1",
   'd-2': "d2"
 };
 
 // None of these should become quoted, regardless of the quoteProps value.
-const e = {
+a = {
   NaN: null,
   1: null,
   1.5: null,
@@ -751,7 +771,7 @@ const e = {
   2n: null,
 }
 
-const f = {
+a = {
   // These should be unquoted for quoteProps=as-needed.
   "NaN": null,
   "1": null,
@@ -806,33 +826,38 @@ Object.entries({
   "-1.5": null,
 }
 
+a = {
+  "\\a": 1,
+  "b": 2
+}
+
 =====================================output=====================================
-const a = {
+a = {
   a: "a",
 };
 
-const b = {
+a = {
   b: "b",
 };
 
-const b2 = {
+a = {
   // Escapes should stay as escapes and not be unquoted.
   "\\u0062": "b",
   "\\u0031": "1",
 };
 
-const c = {
+a = {
   c1: "c1",
   c2: "c2",
 };
 
-const d = {
+a = {
   "d1": "d1",
   "d-2": "d2",
 };
 
 // None of these should become quoted, regardless of the quoteProps value.
-const e = {
+a = {
   NaN: null,
   1: null,
   1.5: null,
@@ -850,7 +875,7 @@ const e = {
   2n: null,
 };
 
-const f = {
+a = {
   // These should be unquoted for quoteProps=as-needed.
   "NaN": null,
   "1": null,
@@ -905,6 +930,11 @@ Object.entries({
   "-1.5": null,
 };
 
+a = {
+  a: 1,
+  b: 2,
+};
+
 ================================================================================
 `;
 
@@ -915,32 +945,32 @@ printWidth: 80
 quoteProps: "preserve"
                                                                                 | printWidth
 =====================================input======================================
-const a = {
+a = {
   a: "a"
 };
 
-const b = {
+a = {
   'b': "b"
 };
 
-const b2 = {
+a = {
   // Escapes should stay as escapes and not be unquoted.
   '\\u0062': "b",
   '\\u0031': "1"
 };
 
-const c = {
+a = {
   c1: "c1",
   'c2': "c2"
 };
 
-const d = {
+a = {
   d1: "d1",
   'd-2': "d2"
 };
 
 // None of these should become quoted, regardless of the quoteProps value.
-const e = {
+a = {
   NaN: null,
   1: null,
   1.5: null,
@@ -958,7 +988,7 @@ const e = {
   2n: null,
 }
 
-const f = {
+a = {
   // These should be unquoted for quoteProps=as-needed.
   "NaN": null,
   "1": null,
@@ -1013,33 +1043,38 @@ Object.entries({
   "-1.5": null,
 }
 
+a = {
+  "\\a": 1,
+  "b": 2
+}
+
 =====================================output=====================================
-const a = {
+a = {
   a: "a",
 };
 
-const b = {
+a = {
   "b": "b",
 };
 
-const b2 = {
+a = {
   // Escapes should stay as escapes and not be unquoted.
   "\\u0062": "b",
   "\\u0031": "1",
 };
 
-const c = {
+a = {
   c1: "c1",
   "c2": "c2",
 };
 
-const d = {
+a = {
   d1: "d1",
   "d-2": "d2",
 };
 
 // None of these should become quoted, regardless of the quoteProps value.
-const e = {
+a = {
   NaN: null,
   1: null,
   1.5: null,
@@ -1057,7 +1092,7 @@ const e = {
   2n: null,
 };
 
-const f = {
+a = {
   // These should be unquoted for quoteProps=as-needed.
   "NaN": null,
   "1": null,
@@ -1112,6 +1147,11 @@ Object.entries({
   "-1.5": null,
 };
 
+a = {
+  "a": 1,
+  "b": 2,
+};
+
 ================================================================================
 `;
 
diff --git a/tests/format/js/quote-props/jsfmt.spec.js b/tests/format/js/quote-props/jsfmt.spec.js
index 2457d95416b9..80d7cae87917 100644
--- a/tests/format/js/quote-props/jsfmt.spec.js
+++ b/tests/format/js/quote-props/jsfmt.spec.js
@@ -1,22 +1,7 @@
-const errors = {};
-
-runFormatTest(import.meta, ["babel"], {
-  quoteProps: "as-needed",
-  errors,
-});
-
-runFormatTest(import.meta, ["babel"], {
-  quoteProps: "preserve",
-  errors,
-});
-
-runFormatTest(import.meta, ["babel"], {
-  quoteProps: "consistent",
-  errors,
-});
-
+runFormatTest(import.meta, ["babel"], { quoteProps: "as-needed" });
+runFormatTest(import.meta, ["babel"], { quoteProps: "preserve" });
+runFormatTest(import.meta, ["babel"], { quoteProps: "consistent" });
 runFormatTest(import.meta, ["babel"], {
   quoteProps: "consistent",
   singleQuote: true,
-  errors,
 });
diff --git a/tests/format/js/quote-props/objects.js b/tests/format/js/quote-props/objects.js
index 1320543c4bf9..40cdc863c228 100644
--- a/tests/format/js/quote-props/objects.js
+++ b/tests/format/js/quote-props/objects.js
@@ -1,29 +1,29 @@
-const a = {
+a = {
   a: "a"
 };
 
-const b = {
+a = {
   'b': "b"
 };
 
-const b2 = {
+a = {
   // Escapes should stay as escapes and not be unquoted.
   '\u0062': "b",
   '\u0031': "1"
 };
 
-const c = {
+a = {
   c1: "c1",
   'c2': "c2"
 };
 
-const d = {
+a = {
   d1: "d1",
   'd-2': "d2"
 };
 
 // None of these should become quoted, regardless of the quoteProps value.
-const e = {
+a = {
   NaN: null,
   1: null,
   1.5: null,
@@ -41,7 +41,7 @@ const e = {
   2n: null,
 }
 
-const f = {
+a = {
   // These should be unquoted for quoteProps=as-needed.
   "NaN": null,
   "1": null,
@@ -95,3 +95,8 @@ Object.entries({
   "-1": null,
   "-1.5": null,
 }
+
+a = {
+  "\a": 1,
+  "b": 2
+}