Repository: prettier/prettier (JavaScript)
Date: 2023-12-19 02:50:04+00:00
Task type: Feature

Issue:
Prettier should format JSONC with trailing commas
- Right now, Prettier's default value of "trailingCommas" is "all", which is fantastic. Presumably, most people who use Prettier would agree with this, as the reasons for wanting trailing commas are compelling: better Git diffs, easier refactoring, and so on.
- For all the same reasons that we would want trailing commas in a JavaScript/TypeScript file, we also want to have trailing commas in JSONC files.
- However, Prettier does not insert trailing commas into JSONC files by default.

You could frame this problem as either:
1. a bug in Prettier (since the default value of "trailingCommas" is supposed to be "all"), or
2. a failure in Prettier parsing JSONC properly

Either way, this should be changed.

<br>

## Steps to Reproduce

```ts
mkdir test
cd test
npm init --yes
npm install prettier --save-dev
echo '{ "foo": "bar" }' > test.jsonc
npx prettier test.jsonc --write
cat test.jsonc
```

Observe that the formatted JSONC file does not have trailing commas.

<br>

## Current Hacky Workaround

Use the following custom Prettier config:

```
  overrides: [
    {
      files: ["**/*.jsonc"],
      options: {
        parser: "json5",
        quoteProps: "preserve",
      },
    },
```

Since JSONC is a subset of JSON5, this works properly. However, it is important that we also specify `quoteProps: "preserve"`, as the default value for `quoteProps` for JSON5 is "as-needed", which would unquote JSON properties, making it non-valid JSONC.

Obviously, having to use a custom Prettier config is really unfortunate, as one of the main ideas of using Prettier is that you shouldn't use your own custom config, you should just use all the default values. But this bug / bad default behavior prevents that.

Also, as a semi-related note, it was proposed that the default value for `quoteProps` in JSON5 would be changed in #4639. If a PR for that was accepted, then we could simplify the workaround slightly, although the ideal solution is to not to have to do a workaround to begin with.

<br>

## Summarizing Prior Discussion

This issue continues #5708.

The core issue is that Prettier does not have a separate parser for JSONC files, it just uses the JSON parser. Since the JSON parser works with comments, this "just works" some of the time, but crashes and burns when trailing commas are involved.

ikatyang says [here](https://github.com/prettier/prettier/issues/5708#issuecomment-452523700) that they considered making a JSONC parser, but this was confusing, as it was really just a trailing commas parser and not a comment parser. Which is definitely a valid concern!

However, five years have passed since that comment, and JSONC files have only become more prevalent in the ecosystem. For example, popular tools like [CSpell](https://github.com/streetsidesoftware/cspell) (code spell checker), [Knip](https://github.com/webpro/knip) (dead code finder), and [TypeDoc](https://github.com/TypeStrong/typedoc) (documentation generator) all support JSONC files by default ("cspell.jsonc", "knip.jsonc", and "typedoc.jsonc" respectively).

Thus, there is a more compelling reason for Prettier to support the JSONC format more robustly. To @ikatyang, and the rest of the Prettier team, can we revisit this decision? A dedicated parser for JSONC means that we can finally format JSONC files properly out of the box without having to use the dirty JSON5 hack specified above.


Issue discussion:
Maybe we should add a jsonc parser?
Yes, that's what I'm proposing in the final section of the OP. 🙏 
I also have needs for this, @sosukesuzuki what do you think?
I think adding `jsonc` parser makes sense. This is because JSON and JSONC have different syntax.
PR welcome.

Solution patch:
diff --git a/changelog_unreleased/json/15831.md b/changelog_unreleased/json/15831.md
new file mode 100644
index 000000000000..a52ecdbab09e
--- /dev/null
+++ b/changelog_unreleased/json/15831.md
@@ -0,0 +1,9 @@
+#### [Highlight] New `jsonc` parser added (#15831 by @fisker)
+
+Previously, we infer the parser of `.jsonc` files to be `json`, but if we want keep the trailing comma, we'll have to use a hacky workaround config `{parser: "json5", quoteProps: "preserve", singleQuote: false}`.
+
+The new added `jsonc` parser:
+
+- Always quote the object keys.
+- Wrap strings with double quotes.
+- Of course, respect the [`trailingComma`](https://prettier.io/docs/en/options#trailing-commas) option.
diff --git a/docs/options.md b/docs/options.md
index c79643c816bb..0e911d293abc 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -115,8 +115,6 @@ Note that Prettier never unquotes numeric property names in Angular expressions,
 [quote-props-flow]: https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVBjOA7AzgFzAA8wBeMAb1TDAAYAuMARlQF8g
 [quote-props-vue]: https://github.com/prettier/prettier/issues/10127
 
-If this option is set to `preserve`, `singleQuote` to `false` (default value), and `parser` to `json5`, double quotes are always used for strings. This effectively allows using the `json5` parser for “JSON with comments and trailing commas”.
-
 ## JSX Quotes
 
 Use single quotes instead of double quotes in JSX.
@@ -282,6 +280,7 @@ Valid options:
 - `"less"` (via [postcss-less](https://github.com/shellscape/postcss-less)) _First available in v1.7.1_
 - `"json"` (via [@babel/parser parseExpression](https://babeljs.io/docs/en/next/babel-parser.html#babelparserparseexpressioncode-options)) _First available in v1.5.0_
 - `"json5"` (same parser as `"json"`, but outputs as [json5](https://json5.org/)) _First available in v1.13.0_
+- `"jsonc"` (same parser as `"json"`, but outputs as "JSON with Comments") _First available in v3.2.0_
 - `"json-stringify"` (same parser as `"json"`, but outputs like `JSON.stringify`) _First available in v1.13.0_
 - `"graphql"` (via [graphql/language](https://github.com/graphql/graphql-js/tree/master/src/language)) _First available in v1.5.0_
 - `"markdown"` (via [remark-parse](https://github.com/wooorm/remark/tree/main/packages/remark-parse)) _First available in v1.8.0_
diff --git a/src/index.d.ts b/src/index.d.ts
index a2d0cdff542d..00bdb913ac72 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -291,6 +291,7 @@ export type BuiltInParserName =
   | "json-stringify"
   | "json"
   | "json5"
+  | "jsonc"
   | "less"
   | "lwc"
   | "markdown"
diff --git a/src/language-js/print/assignment.js b/src/language-js/print/assignment.js
index a196fb6988cf..9ea30b5b2275 100644
--- a/src/language-js/print/assignment.js
+++ b/src/language-js/print/assignment.js
@@ -142,6 +142,7 @@ function chooseLayout(path, options, print, leftDoc, rightPropertyName) {
       rightNode.callee.name === "require") ||
     // do not put values on a separate line from the key in json
     options.parser === "json5" ||
+    options.parser === "jsonc" ||
     options.parser === "json"
   ) {
     return "never-break-after-operator";
diff --git a/src/language-js/print/property.js b/src/language-js/print/property.js
index 7a3504807189..8afc5c5ff89d 100644
--- a/src/language-js/print/property.js
+++ b/src/language-js/print/property.js
@@ -47,6 +47,7 @@ function printPropertyKey(path, options, print) {
         // mentioned in `isStringPropSafeToUnquote`).
         !(options.parser === "typescript" || options.parser === "babel-ts"))) &&
     (options.parser === "json" ||
+      options.parser === "jsonc" ||
       (options.quoteProps === "consistent" && needsQuoteProps.get(parent)))
   ) {
     // a -> "a"
diff --git a/src/language-js/utils/index.js b/src/language-js/utils/index.js
index 020f18d588f1..dda7197351e6 100644
--- a/src/language-js/utils/index.js
+++ b/src/language-js/utils/index.js
@@ -642,6 +642,7 @@ function hasLeadingOwnLineComment(text, node) {
 function isStringPropSafeToUnquote(node, options) {
   return (
     options.parser !== "json" &&
+    options.parser !== "jsonc" &&
     isStringLiteral(node.key) &&
     rawText(node.key).slice(1, -1) === node.key.value &&
     ((isEs5IdentifierName(node.key.value) &&
diff --git a/src/language-json/languages.evaluate.js b/src/language-json/languages.evaluate.js
index 0ecc9196552b..eade08a15e09 100644
--- a/src/language-json/languages.evaluate.js
+++ b/src/language-json/languages.evaluate.js
@@ -15,10 +15,9 @@ const languages = [
     vscodeLanguageIds: ["json"],
     extensions: data.extensions.filter((extension) => extension !== ".jsonl"),
   })),
-  createLanguage(linguistLanguages["JSON with Comments"], (data) => ({
-    parsers: ["json"],
+  createLanguage(linguistLanguages["JSON with Comments"], () => ({
+    parsers: ["jsonc"],
     vscodeLanguageIds: ["jsonc"],
-    filenames: [...data.filenames, ".eslintrc"],
   })),
   createLanguage(linguistLanguages.JSON5, () => ({
     parsers: ["json5"],
diff --git a/src/language-json/parser-json.js b/src/language-json/parser-json.js
index 26692bfc3ea5..595cdc205c68 100644
--- a/src/language-json/parser-json.js
+++ b/src/language-json/parser-json.js
@@ -152,6 +152,7 @@ const jsonParsers = {
     },
   }),
   json5: createParser(parseJson),
+  jsonc: createParser(parseJson),
   "json-stringify": createParser({
     parse: createJsonParse({ allowComments: false }),
     astFormat: "estree-json",
diff --git a/src/main/comments/attach.js b/src/main/comments/attach.js
index 0b3468c6e50c..62f4c405ad48 100644
--- a/src/main/comments/attach.js
+++ b/src/main/comments/attach.js
@@ -184,6 +184,7 @@ function attachComments(ast, options) {
     if (
       options.parser === "json" ||
       options.parser === "json5" ||
+      options.parser === "jsonc" ||
       options.parser === "__js_expression" ||
       options.parser === "__ts_expression" ||
       options.parser === "__vue_expression" ||
diff --git a/src/main/core-options.evaluate.js b/src/main/core-options.evaluate.js
index efb0b3579916..a2add71679b3 100644
--- a/src/main/core-options.evaluate.js
+++ b/src/main/core-options.evaluate.js
@@ -120,6 +120,7 @@ const options = {
       { value: "scss", description: "SCSS" },
       { value: "json", description: "JSON" },
       { value: "json5", description: "JSON5" },
+      { value: "jsonc", description: "JSON with Comments" },
       { value: "json-stringify", description: "JSON.stringify" },
       { value: "graphql", description: "GraphQL" },
       { value: "markdown", description: "Markdown" },
diff --git a/src/main/range-util.js b/src/main/range-util.js
index 56b593014950..7420b49136a0 100644
--- a/src/main/range-util.js
+++ b/src/main/range-util.js
@@ -3,7 +3,10 @@ import assert from "node:assert";
 import { getSortedChildNodes } from "./comments/attach.js";
 
 const isJsonParser = ({ parser }) =>
-  parser === "json" || parser === "json5" || parser === "json-stringify";
+  parser === "json" ||
+  parser === "json5" ||
+  parser === "jsonc" ||
+  parser === "json-stringify";
 
 function findCommonAncestor(startNodeAndParents, endNodeAndParents) {
   const startNodeAndAncestors = [
@@ -180,6 +183,7 @@ function isSourceElement(opts, node, parentNode) {
       return isJsSourceElement(node.type, parentNode?.type);
     case "json":
     case "json5":
+    case "jsonc":
     case "json-stringify":
       return jsonSourceElements.has(node.type);
     case "graphql":
diff --git a/src/plugins/builtin-plugins-proxy.js b/src/plugins/builtin-plugins-proxy.js
index 2bbd30c82d1c..75d30e5b826a 100644
--- a/src/plugins/builtin-plugins-proxy.js
+++ b/src/plugins/builtin-plugins-proxy.js
@@ -93,6 +93,7 @@ export const { parsers, printers } = createParsersAndPrinters([
       "__babel_estree",
       "json",
       "json5",
+      "jsonc",
       "json-stringify",
     ],
   },
diff --git a/src/utils/print-string.js b/src/utils/print-string.js
index 310698be4804..8f4cf6efaa61 100644
--- a/src/utils/print-string.js
+++ b/src/utils/print-string.js
@@ -11,6 +11,13 @@ function printString(raw, options) {
   /** @type {Quote} */
   const enclosingQuote =
     options.parser === "json" ||
+    options.parser === "jsonc" ||
+    // This was added before we have the `jsonc` parser
+    // If `{quoteProps: "preserve"}` and `{singleQuote: false}` (default value),
+    // and `{parser: "json5"}`, double quotes are always used for strings.
+    // This effectively allows using the `json5` parser for “JSON with comments and trailing commas”.
+    // See https://github.com/prettier/prettier/pull/10323
+    // See https://github.com/prettier/prettier/pull/15831#discussion_r1431010636
     (options.parser === "json5" &&
       options.quoteProps === "preserve" &&
       !options.singleQuote)
diff --git a/website/playground/codeSamples.mjs b/website/playground/codeSamples.mjs
index 3038fbca396e..e194b4581385 100644
--- a/website/playground/codeSamples.mjs
+++ b/website/playground/codeSamples.mjs
@@ -116,6 +116,7 @@ export default function getCodeSamples(parser) {
       ].join("\n");
     case "json":
     case "json5":
+    case "jsonc":
     case "json-stringify":
       // Excerpted & adapted from Wikipedia, under the Creative Commons Attribution-ShareAlike License
       // https://en.wikipedia.org/wiki/JSON#Example


Test patch:
diff --git a/tests/config/utils/check-parsers.js b/tests/config/utils/check-parsers.js
index 8fa94e73e4b5..bf8665a44455 100644
--- a/tests/config/utils/check-parsers.js
+++ b/tests/config/utils/check-parsers.js
@@ -72,9 +72,9 @@ const categoryParsers = new Map([
   [
     "json",
     {
-      parsers: ["json", "json5", "json-stringify"],
-      verifyParsers: ["json", "json5", "json-stringify"],
-      extensions: [".json"],
+      parsers: ["json", "json5", "jsonc", "json-stringify"],
+      verifyParsers: ["json", "json5", "jsonc", "json-stringify"],
+      extensions: [".json", ".json5", ".jsonc"],
     },
   ],
   [
diff --git a/tests/format/json/jsonc/quote-props/__snapshots__/jsfmt.spec.js.snap b/tests/format/json/jsonc/quote-props/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..13707c16f5c3
--- /dev/null
+++ b/tests/format/json/jsonc/quote-props/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,76 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`test.jsonc - {"quoteProps":"as-needed"} format 1`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+quoteProps: "as-needed"
+                                                                                | printWidth
+=====================================input======================================
+[
+  "Object keys should always quoted!",
+  {"key":""},
+  {"key":"","key-can-not-unquote":""},
+  {key:""},
+]
+
+=====================================output=====================================
+[
+  "Object keys should always quoted!",
+  { "key": "" },
+  { "key": "", "key-can-not-unquote": "" },
+  { "key": "" },
+]
+
+================================================================================
+`;
+
+exports[`test.jsonc - {"quoteProps":"consistent"} format 1`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+quoteProps: "consistent"
+                                                                                | printWidth
+=====================================input======================================
+[
+  "Object keys should always quoted!",
+  {"key":""},
+  {"key":"","key-can-not-unquote":""},
+  {key:""},
+]
+
+=====================================output=====================================
+[
+  "Object keys should always quoted!",
+  { "key": "" },
+  { "key": "", "key-can-not-unquote": "" },
+  { "key": "" },
+]
+
+================================================================================
+`;
+
+exports[`test.jsonc - {"quoteProps":"preserve"} format 1`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+quoteProps: "preserve"
+                                                                                | printWidth
+=====================================input======================================
+[
+  "Object keys should always quoted!",
+  {"key":""},
+  {"key":"","key-can-not-unquote":""},
+  {key:""},
+]
+
+=====================================output=====================================
+[
+  "Object keys should always quoted!",
+  { "key": "" },
+  { "key": "", "key-can-not-unquote": "" },
+  { "key": "" },
+]
+
+================================================================================
+`;
diff --git a/tests/format/json/jsonc/quote-props/jsfmt.spec.js b/tests/format/json/jsonc/quote-props/jsfmt.spec.js
new file mode 100644
index 000000000000..af447f4ac1aa
--- /dev/null
+++ b/tests/format/json/jsonc/quote-props/jsfmt.spec.js
@@ -0,0 +1,3 @@
+run_spec(import.meta, ["jsonc"], { quoteProps: "as-needed" });
+run_spec(import.meta, ["jsonc"], { quoteProps: "consistent" });
+run_spec(import.meta, ["jsonc"], { quoteProps: "preserve" });
diff --git a/tests/format/json/jsonc/quote-props/test.jsonc b/tests/format/json/jsonc/quote-props/test.jsonc
new file mode 100644
index 000000000000..d295207ad380
--- /dev/null
+++ b/tests/format/json/jsonc/quote-props/test.jsonc
@@ -0,0 +1,6 @@
+[
+  "Object keys should always quoted!",
+  {"key":""},
+  {"key":"","key-can-not-unquote":""},
+  {key:""},
+]
diff --git a/tests/format/json/jsonc/single-quote/__snapshots__/jsfmt.spec.js.snap b/tests/format/json/jsonc/single-quote/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..f470d01240fe
--- /dev/null
+++ b/tests/format/json/jsonc/single-quote/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`test.jsonc - {"singleQuote":false} format 1`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+singleQuote: false
+                                                                                | printWidth
+=====================================input======================================
+[
+  "Should always use double quote",
+  'Even it is single quoted originally',
+  'Or contains double quotes """""""""""',
+  {"object-key-should-also-double-quoted":""},
+]
+
+=====================================output=====================================
+[
+  "Should always use double quote",
+  "Even it is single quoted originally",
+  "Or contains double quotes \\"\\"\\"\\"\\"\\"\\"\\"\\"\\"\\"",
+  { "object-key-should-also-double-quoted": "" },
+]
+
+================================================================================
+`;
+
+exports[`test.jsonc - {"singleQuote":true} format 1`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+singleQuote: true
+                                                                                | printWidth
+=====================================input======================================
+[
+  "Should always use double quote",
+  'Even it is single quoted originally',
+  'Or contains double quotes """""""""""',
+  {"object-key-should-also-double-quoted":""},
+]
+
+=====================================output=====================================
+[
+  "Should always use double quote",
+  "Even it is single quoted originally",
+  "Or contains double quotes \\"\\"\\"\\"\\"\\"\\"\\"\\"\\"\\"",
+  { "object-key-should-also-double-quoted": "" },
+]
+
+================================================================================
+`;
diff --git a/tests/format/json/jsonc/single-quote/jsfmt.spec.js b/tests/format/json/jsonc/single-quote/jsfmt.spec.js
new file mode 100644
index 000000000000..e03db11ace17
--- /dev/null
+++ b/tests/format/json/jsonc/single-quote/jsfmt.spec.js
@@ -0,0 +1,2 @@
+run_spec(import.meta, ["jsonc"], { singleQuote: true });
+run_spec(import.meta, ["jsonc"], { singleQuote: false });
diff --git a/tests/format/json/jsonc/single-quote/test.jsonc b/tests/format/json/jsonc/single-quote/test.jsonc
new file mode 100644
index 000000000000..5a25b2d8a636
--- /dev/null
+++ b/tests/format/json/jsonc/single-quote/test.jsonc
@@ -0,0 +1,6 @@
+[
+  "Should always use double quote",
+  'Even it is single quoted originally',
+  'Or contains double quotes """""""""""',
+  {"object-key-should-also-double-quoted":""},
+]
diff --git a/tests/format/json/jsonc/trialing-comma/__snapshots__/jsfmt.spec.js.snap b/tests/format/json/jsonc/trialing-comma/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..277e4b107144
--- /dev/null
+++ b/tests/format/json/jsonc/trialing-comma/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,139 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`test.jsonc - {"trailingComma":"all"} format 1`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "all"
+                                                                                | printWidth
+=====================================input======================================
+[
+  ["omit-if-short"],
+  ["long","long","long","long","long","long","enough","to","break","into","multiple","lines"],
+  {"short":"object"},
+  {"long":"","enough":"","to":"","break":"","into":"","multiple":"","lines":""}
+]
+
+=====================================output=====================================
+[
+  ["omit-if-short"],
+  [
+    "long",
+    "long",
+    "long",
+    "long",
+    "long",
+    "long",
+    "enough",
+    "to",
+    "break",
+    "into",
+    "multiple",
+    "lines",
+  ],
+  { "short": "object" },
+  {
+    "long": "",
+    "enough": "",
+    "to": "",
+    "break": "",
+    "into": "",
+    "multiple": "",
+    "lines": "",
+  },
+]
+
+================================================================================
+`;
+
+exports[`test.jsonc - {"trailingComma":"es5"} format 1`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "es5"
+                                                                                | printWidth
+=====================================input======================================
+[
+  ["omit-if-short"],
+  ["long","long","long","long","long","long","enough","to","break","into","multiple","lines"],
+  {"short":"object"},
+  {"long":"","enough":"","to":"","break":"","into":"","multiple":"","lines":""}
+]
+
+=====================================output=====================================
+[
+  ["omit-if-short"],
+  [
+    "long",
+    "long",
+    "long",
+    "long",
+    "long",
+    "long",
+    "enough",
+    "to",
+    "break",
+    "into",
+    "multiple",
+    "lines",
+  ],
+  { "short": "object" },
+  {
+    "long": "",
+    "enough": "",
+    "to": "",
+    "break": "",
+    "into": "",
+    "multiple": "",
+    "lines": "",
+  },
+]
+
+================================================================================
+`;
+
+exports[`test.jsonc - {"trailingComma":"none"} format 1`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "none"
+                                                                                | printWidth
+=====================================input======================================
+[
+  ["omit-if-short"],
+  ["long","long","long","long","long","long","enough","to","break","into","multiple","lines"],
+  {"short":"object"},
+  {"long":"","enough":"","to":"","break":"","into":"","multiple":"","lines":""}
+]
+
+=====================================output=====================================
+[
+  ["omit-if-short"],
+  [
+    "long",
+    "long",
+    "long",
+    "long",
+    "long",
+    "long",
+    "enough",
+    "to",
+    "break",
+    "into",
+    "multiple",
+    "lines"
+  ],
+  { "short": "object" },
+  {
+    "long": "",
+    "enough": "",
+    "to": "",
+    "break": "",
+    "into": "",
+    "multiple": "",
+    "lines": ""
+  }
+]
+
+================================================================================
+`;
diff --git a/tests/format/json/jsonc/trialing-comma/jsfmt.spec.js b/tests/format/json/jsonc/trialing-comma/jsfmt.spec.js
new file mode 100644
index 000000000000..ee3b05357967
--- /dev/null
+++ b/tests/format/json/jsonc/trialing-comma/jsfmt.spec.js
@@ -0,0 +1,3 @@
+run_spec(import.meta, ["jsonc"], { trailingComma: "none" });
+run_spec(import.meta, ["jsonc"], { trailingComma: "es5" });
+run_spec(import.meta, ["jsonc"], { trailingComma: "all" });
diff --git a/tests/format/json/jsonc/trialing-comma/test.jsonc b/tests/format/json/jsonc/trialing-comma/test.jsonc
new file mode 100644
index 000000000000..32c381f6213b
--- /dev/null
+++ b/tests/format/json/jsonc/trialing-comma/test.jsonc
@@ -0,0 +1,6 @@
+[
+  ["omit-if-short"],
+  ["long","long","long","long","long","long","enough","to","break","into","multiple","lines"],
+  {"short":"object"},
+  {"long":"","enough":"","to":"","break":"","into":"","multiple":"","lines":""}
+]
diff --git a/tests/format/json/range/jsonc/__snapshots__/jsfmt.spec.js.snap b/tests/format/json/range/jsonc/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..14b8fb57162f
--- /dev/null
+++ b/tests/format/json/range/jsonc/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`inside-array.jsonc format 1`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+rangeEnd: 30
+rangeStart: 11
+     |                                                                                | printWidth
+=====================================input======================================
+  1 | {
+  2 | a: [
+  3 | 1,
+> 4 |  2,  3,4,5,6,      7,
+    |  ^^^^^^^^^^^^^^^^^^^
+  5 | 8
+  6 | ],
+  7 | b: [1, 2,      3, 4]
+  8 | }
+  9 |
+=====================================output=====================================
+{
+a: [1, 2, 3, 4, 5, 6, 7, 8],
+b: [1, 2,      3, 4]
+}
+
+================================================================================
+`;
diff --git a/tests/format/json/range/jsonc/inside-array.jsonc b/tests/format/json/range/jsonc/inside-array.jsonc
new file mode 100644
index 000000000000..2f740ea154b6
--- /dev/null
+++ b/tests/format/json/range/jsonc/inside-array.jsonc
@@ -0,0 +1,8 @@
+{
+a: [
+1,
+ <<<PRETTIER_RANGE_START>>>2,  3,4,5,6,      7<<<PRETTIER_RANGE_END>>>,
+8
+],
+b: [1, 2,      3, 4]
+}
diff --git a/tests/format/json/range/jsonc/jsfmt.spec.js b/tests/format/json/range/jsonc/jsfmt.spec.js
new file mode 100644
index 000000000000..96a857a61fbb
--- /dev/null
+++ b/tests/format/json/range/jsonc/jsfmt.spec.js
@@ -0,0 +1,1 @@
+run_spec(import.meta, ["jsonc"]);
diff --git a/tests/format/json/with-comment/__snapshots__/jsfmt.spec.js.snap b/tests/format/json/with-comment/__snapshots__/jsfmt.spec.js.snap
index 9bd1856ba7ed..6904af079c52 100644
--- a/tests/format/json/with-comment/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/format/json/with-comment/__snapshots__/jsfmt.spec.js.snap
@@ -30,6 +30,21 @@ trailingComma: "all"
 ================================================================================
 `;
 
+exports[`block-comment.json - {"trailingComma":"all"} format 3`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "all"
+                                                                                | printWidth
+=====================================input======================================
+{/*comment*/"K":"V"}
+
+=====================================output=====================================
+{ /*comment*/ "K": "V" }
+
+================================================================================
+`;
+
 exports[`block-comment.json - {"trailingComma":"es5"} format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -60,6 +75,21 @@ trailingComma: "es5"
 ================================================================================
 `;
 
+exports[`block-comment.json - {"trailingComma":"es5"} format 3`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "es5"
+                                                                                | printWidth
+=====================================input======================================
+{/*comment*/"K":"V"}
+
+=====================================output=====================================
+{ /*comment*/ "K": "V" }
+
+================================================================================
+`;
+
 exports[`bottom-block-comment.json - {"trailingComma":"all"} format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -90,6 +120,21 @@ trailingComma: "all"
 ================================================================================
 `;
 
+exports[`bottom-block-comment.json - {"trailingComma":"all"} format 3`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "all"
+                                                                                | printWidth
+=====================================input======================================
+1 /* block-comment */
+
+=====================================output=====================================
+1 /* block-comment */
+
+================================================================================
+`;
+
 exports[`bottom-block-comment.json - {"trailingComma":"es5"} format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -120,6 +165,21 @@ trailingComma: "es5"
 ================================================================================
 `;
 
+exports[`bottom-block-comment.json - {"trailingComma":"es5"} format 3`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "es5"
+                                                                                | printWidth
+=====================================input======================================
+1 /* block-comment */
+
+=====================================output=====================================
+1 /* block-comment */
+
+================================================================================
+`;
+
 exports[`bottom-line-comment.json - {"trailingComma":"all"} format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -150,6 +210,21 @@ trailingComma: "all"
 ================================================================================
 `;
 
+exports[`bottom-line-comment.json - {"trailingComma":"all"} format 3`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "all"
+                                                                                | printWidth
+=====================================input======================================
+1 // line-comment
+
+=====================================output=====================================
+1 // line-comment
+
+================================================================================
+`;
+
 exports[`bottom-line-comment.json - {"trailingComma":"es5"} format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -180,6 +255,21 @@ trailingComma: "es5"
 ================================================================================
 `;
 
+exports[`bottom-line-comment.json - {"trailingComma":"es5"} format 3`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "es5"
+                                                                                | printWidth
+=====================================input======================================
+1 // line-comment
+
+=====================================output=====================================
+1 // line-comment
+
+================================================================================
+`;
+
 exports[`line-comment.json - {"trailingComma":"all"} format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -222,6 +312,27 @@ trailingComma: "all"
 ================================================================================
 `;
 
+exports[`line-comment.json - {"trailingComma":"all"} format 3`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "all"
+                                                                                | printWidth
+=====================================input======================================
+{
+  //comment
+  "K":"V"
+}
+
+=====================================output=====================================
+{
+  //comment
+  "K": "V",
+}
+
+================================================================================
+`;
+
 exports[`line-comment.json - {"trailingComma":"es5"} format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -264,6 +375,27 @@ trailingComma: "es5"
 ================================================================================
 `;
 
+exports[`line-comment.json - {"trailingComma":"es5"} format 3`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "es5"
+                                                                                | printWidth
+=====================================input======================================
+{
+  //comment
+  "K":"V"
+}
+
+=====================================output=====================================
+{
+  //comment
+  "K": "V",
+}
+
+================================================================================
+`;
+
 exports[`top-block-comment.json - {"trailingComma":"all"} format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -302,6 +434,25 @@ trailingComma: "all"
 ================================================================================
 `;
 
+exports[`top-block-comment.json - {"trailingComma":"all"} format 3`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "all"
+                                                                                | printWidth
+=====================================input======================================
+/* comment */{
+  "foo": "bar"
+}
+
+=====================================output=====================================
+/* comment */ {
+  "foo": "bar",
+}
+
+================================================================================
+`;
+
 exports[`top-block-comment.json - {"trailingComma":"es5"} format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -340,6 +491,25 @@ trailingComma: "es5"
 ================================================================================
 `;
 
+exports[`top-block-comment.json - {"trailingComma":"es5"} format 3`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "es5"
+                                                                                | printWidth
+=====================================input======================================
+/* comment */{
+  "foo": "bar"
+}
+
+=====================================output=====================================
+/* comment */ {
+  "foo": "bar",
+}
+
+================================================================================
+`;
+
 exports[`top-line-comment.json - {"trailingComma":"all"} format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -386,6 +556,29 @@ trailingComma: "all"
 ================================================================================
 `;
 
+exports[`top-line-comment.json - {"trailingComma":"all"} format 3`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "all"
+                                                                                | printWidth
+=====================================input======================================
+// comment 1
+// comment 2
+{
+  "foo": "bar"
+}
+
+=====================================output=====================================
+// comment 1
+// comment 2
+{
+  "foo": "bar",
+}
+
+================================================================================
+`;
+
 exports[`top-line-comment.json - {"trailingComma":"es5"} format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -431,3 +624,26 @@ trailingComma: "es5"
 
 ================================================================================
 `;
+
+exports[`top-line-comment.json - {"trailingComma":"es5"} format 3`] = `
+====================================options=====================================
+parsers: ["jsonc"]
+printWidth: 80
+trailingComma: "es5"
+                                                                                | printWidth
+=====================================input======================================
+// comment 1
+// comment 2
+{
+  "foo": "bar"
+}
+
+=====================================output=====================================
+// comment 1
+// comment 2
+{
+  "foo": "bar",
+}
+
+================================================================================
+`;
diff --git a/tests/format/json/with-comment/jsfmt.spec.js b/tests/format/json/with-comment/jsfmt.spec.js
index 6bfa8957e978..b2c0da3b38ec 100644
--- a/tests/format/json/with-comment/jsfmt.spec.js
+++ b/tests/format/json/with-comment/jsfmt.spec.js
@@ -2,3 +2,5 @@ run_spec(import.meta, ["json"], { trailingComma: "es5" });
 run_spec(import.meta, ["json"], { trailingComma: "all" });
 run_spec(import.meta, ["json5"], { trailingComma: "es5" });
 run_spec(import.meta, ["json5"], { trailingComma: "all" });
+run_spec(import.meta, ["jsonc"], { trailingComma: "es5" });
+run_spec(import.meta, ["jsonc"], { trailingComma: "all" });
diff --git a/tests/format/markdown/multiparser-json/__snapshots__/jsfmt.spec.js.snap b/tests/format/markdown/multiparser-json/__snapshots__/jsfmt.spec.js.snap
index c6ab38824553..4943e1b4c85d 100644
--- a/tests/format/markdown/multiparser-json/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/format/markdown/multiparser-json/__snapshots__/jsfmt.spec.js.snap
@@ -39,7 +39,7 @@ trailingComma: "all"
 \`\`\`jsonc
 // comment
 {
-  "foo": "bar"
+  "foo": "bar",
 }
 \`\`\`
 
diff --git a/tests/integration/__tests__/__snapshots__/early-exit.js.snap b/tests/integration/__tests__/__snapshots__/early-exit.js.snap
index 25bc0e206a2f..822cdb98431b 100644
--- a/tests/integration/__tests__/__snapshots__/early-exit.js.snap
+++ b/tests/integration/__tests__/__snapshots__/early-exit.js.snap
@@ -60,7 +60,7 @@ Format options:
                            Defaults to css.
   --jsx-single-quote       Use single quotes in JSX.
                            Defaults to false.
-  --parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc>
+  --parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|jsonc|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc>
                            Which parser to use.
   --print-width <int>      The line length where Prettier will try wrap.
                            Defaults to 80.
@@ -155,7 +155,7 @@ exports[`show version with --version (write) 1`] = `[]`;
 exports[`show warning with --help not-found (typo) (stderr) 1`] = `"[warn] Unknown flag "parserr", did you mean "parser"?"`;
 
 exports[`show warning with --help not-found (typo) (stdout) 1`] = `
-"--parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc>
+"--parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|jsonc|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc>
 
   Which parser to use.
 
@@ -174,6 +174,7 @@ Valid options:
   scss             SCSS
   json             JSON
   json5            JSON5
+  jsonc            JSON with Comments
   json-stringify   JSON.stringify
   graphql          GraphQL
   markdown         Markdown
@@ -224,7 +225,7 @@ Format options:
                            Defaults to css.
   --jsx-single-quote       Use single quotes in JSX.
                            Defaults to false.
-  --parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc>
+  --parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|jsonc|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc>
                            Which parser to use.
   --print-width <int>      The line length where Prettier will try wrap.
                            Defaults to 80.
diff --git a/tests/integration/__tests__/__snapshots__/help-options.js.snap b/tests/integration/__tests__/__snapshots__/help-options.js.snap
index 554a07d2ca3a..fbb7d4189c2e 100644
--- a/tests/integration/__tests__/__snapshots__/help-options.js.snap
+++ b/tests/integration/__tests__/__snapshots__/help-options.js.snap
@@ -404,7 +404,7 @@ exports[`show detailed usage with --help no-semi (write) 1`] = `[]`;
 exports[`show detailed usage with --help parser (stderr) 1`] = `""`;
 
 exports[`show detailed usage with --help parser (stdout) 1`] = `
-"--parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc>
+"--parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|jsonc|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc>
 
   Which parser to use.
 
@@ -423,6 +423,7 @@ Valid options:
   scss             SCSS
   json             JSON
   json5            JSON5
+  jsonc            JSON with Comments
   json-stringify   JSON.stringify
   graphql          GraphQL
   markdown         Markdown
diff --git a/tests/integration/__tests__/__snapshots__/plugin-options-string.js.snap b/tests/integration/__tests__/__snapshots__/plugin-options-string.js.snap
index 149499c4b606..95919cddf847 100644
--- a/tests/integration/__tests__/__snapshots__/plugin-options-string.js.snap
+++ b/tests/integration/__tests__/__snapshots__/plugin-options-string.js.snap
@@ -30,8 +30,8 @@ exports[`show external options with \`--help\` 1`] = `
                              Defaults to css.
     --jsx-single-quote       Use single quotes in JSX.
                              Defaults to false.
--   --parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc>
-+   --parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc|foo-parser>
+-   --parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|jsonc|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc>
++   --parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|jsonc|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc|foo-parser>
                              Which parser to use.
     --print-width <int>      The line length where Prettier will try wrap.
                              Defaults to 80.
diff --git a/tests/integration/__tests__/__snapshots__/plugin-options.js.snap b/tests/integration/__tests__/__snapshots__/plugin-options.js.snap
index d4f522b3520c..baa1f447ccdf 100644
--- a/tests/integration/__tests__/__snapshots__/plugin-options.js.snap
+++ b/tests/integration/__tests__/__snapshots__/plugin-options.js.snap
@@ -3,7 +3,7 @@
 exports[`include plugin's parsers to the values of the \`parser\` option\` (stderr) 1`] = `""`;
 
 exports[`include plugin's parsers to the values of the \`parser\` option\` (stdout) 1`] = `
-"--parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc|foo-parser>
+"--parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|jsonc|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc|foo-parser>
 
   Which parser to use.
 
@@ -22,6 +22,7 @@ Valid options:
   scss             SCSS
   json             JSON
   json5            JSON5
+  jsonc            JSON with Comments
   json-stringify   JSON.stringify
   graphql          GraphQL
   markdown         Markdown
@@ -72,8 +73,8 @@ exports[`show external options with \`--help\` 1`] = `
                              Defaults to css.
     --jsx-single-quote       Use single quotes in JSX.
                              Defaults to false.
--   --parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc>
-+   --parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc|foo-parser>
+-   --parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|jsonc|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc>
++   --parser <flow|babel|babel-flow|babel-ts|typescript|acorn|espree|meriyah|css|less|scss|json|json5|jsonc|json-stringify|graphql|markdown|mdx|vue|yaml|glimmer|html|angular|lwc|foo-parser>
                              Which parser to use.
     --print-width <int>      The line length where Prettier will try wrap.
                              Defaults to 80.
diff --git a/tests/integration/__tests__/__snapshots__/schema.js.snap b/tests/integration/__tests__/__snapshots__/schema.js.snap
index a9a3e05a7838..cb5d0bd91e33 100644
--- a/tests/integration/__tests__/__snapshots__/schema.js.snap
+++ b/tests/integration/__tests__/__snapshots__/schema.js.snap
@@ -212,6 +212,12 @@ exports[`schema 1`] = `
                 "json5",
               ],
             },
+            {
+              "description": "JSON with Comments",
+              "enum": [
+                "jsonc",
+              ],
+            },
             {
               "description": "JSON.stringify",
               "enum": [
diff --git a/tests/integration/__tests__/__snapshots__/support-info.js.snap b/tests/integration/__tests__/__snapshots__/support-info.js.snap
index a74818f21c1a..b1803945bed9 100644
--- a/tests/integration/__tests__/__snapshots__/support-info.js.snap
+++ b/tests/integration/__tests__/__snapshots__/support-info.js.snap
@@ -26,7 +26,7 @@ exports[`API getSupportInfo() 1`] = `
       "json",
     ],
     "JSON with Comments": [
-      "json",
+      "jsonc",
     ],
     "JSON.stringify": [
       "json-stringify",
@@ -170,6 +170,7 @@ exports[`API getSupportInfo() 1`] = `
         "scss",
         "json",
         "json5",
+        "jsonc",
         "json-stringify",
         "graphql",
         "markdown",
@@ -533,13 +534,12 @@ exports[`CLI --support-info (stdout) 1`] = `
         "jsconfig.json",
         "language-configuration.json",
         "tsconfig.json",
-        "tslint.json",
-        ".eslintrc"
+        "tslint.json"
       ],
       "group": "JSON",
       "linguistLanguageId": 423,
       "name": "JSON with Comments",
-      "parsers": ["json"],
+      "parsers": ["jsonc"],
       "tmScope": "source.js",
       "type": "data",
       "vscodeLanguageIds": ["jsonc"]
@@ -926,6 +926,7 @@ exports[`CLI --support-info (stdout) 1`] = `
         { "description": "SCSS", "value": "scss" },
         { "description": "JSON", "value": "json" },
         { "description": "JSON5", "value": "json5" },
+        { "description": "JSON with Comments", "value": "jsonc" },
         { "description": "JSON.stringify", "value": "json-stringify" },
         { "description": "GraphQL", "value": "graphql" },
         { "description": "Markdown", "value": "markdown" },
diff --git a/tests/integration/__tests__/with-parser-inference.js b/tests/integration/__tests__/with-parser-inference.js
index 3c99d1f444bd..42d1649fac16 100644
--- a/tests/integration/__tests__/with-parser-inference.js
+++ b/tests/integration/__tests__/with-parser-inference.js
@@ -66,8 +66,8 @@ describe("infers parser from filename", () => {
         {
           "jsc": {
             // Requires v1.2.50 or upper and requires target to be es2016 or upper.
-            "keepClassNames": false
-          }
+            "keepClassNames": false,
+          },
         }
       ` + "\n",
     );
diff --git a/tests/unit/builtin-plugins.js b/tests/unit/builtin-plugins.js
index 7fe8fe8c81ca..dfe3ad5ed213 100644
--- a/tests/unit/builtin-plugins.js
+++ b/tests/unit/builtin-plugins.js
@@ -33,6 +33,7 @@ test("builtin parsers", async () => {
       "json",
       "json-stringify",
       "json5",
+      "jsonc",
       "less",
       "lwc",
       "markdown",