Repository: prettier/prettier (JavaScript)
Date: 2021-02-19 02:07:25+00:00
Task type: Bug Fix

Issue:
Markdown code-block with wrong language yields garbage
<!--

BEFORE SUBMITTING AN ISSUE:

1.  Search for your issue on GitHub: https://github.com/prettier/prettier/issues
    A large number of opened issues are duplicates of existing issues.
    If someone has already opened an issue for what you are experiencing,
    you do not need to open a new issue — please add a 👍 reaction to the
    existing issue instead.

2.  We get a lot of requests for adding options, but Prettier is
    built on the principle of being opinionated about code formatting.
    This means we have a very high bar for adding new options.
    Find out more: https://prettier.io/docs/en/option-philosophy.html

Tip! Don't write this stuff manually.

1. Go to https://prettier.io/playground
2. Paste your code and set options
3. Press the "Report issue" button in the lower right

-->

**Prettier 2.2.1**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEADdArAztAOlABwEMwBrIgczi11xgAs4BaegVwCNawBPAgJ2o1cAMwCWADxisBQin1EATAMLQxFaURijoAOmx4o6VPhAAaEBAJboWZKCJ8+EAO4AFBwlsoiAG2dFuW3N2PhJSOBgAZWIwUSgKZBg+VjhzOABbdjgFBWyAGSJ41ko4ADEIPnTNLXjkECJWGAgzEHoYdJ8AdXpReCwYuEjPXtEAN17uOrAsIJA4rDg+GFdQiirkYV8F82xxACFQsgjIonS4PLi4Da3UkF3IuIofOABFVgh4a59tkGI+Bb4dSqfFIChcUBa-DiME6igYyAAHAAGcz8CALTqhAh1fjURajK7mACO73gK0sXnqWCYUDg2WyLQEJNEAhWlHWSE231uC3SokSyR5j2ebw+V05N3MMCI7FhCnhSAATFLQqIfI8VOkOSBqABWFqsBYAFRlXi5P1GKQAklBcrBImB5FYAIK2yIwbjPL4LAC+PqAA)

**Input:**

Markdown document with:
<pre>
```json
packages\the-hub\cypress\fixtures\gridConfiguration.json
```
</pre>

**Output:**

<pre>
```json
packagespackages\the -
  hubhub\cypresshub\cypress\fixtureshub\cypress\fixtures\gridConfiguration.json
```
</pre>

**Expected behavior:**

If the content cannot be parsed as the requested language, throw.


Issue discussion:
> If the content cannot be parsed as the requested language, throw.

I'd caution against relying on such expectations. Prettier guarantees (well, strives to guarantee) only that correct code stays correct. It makes no promises about invalid code. That's why handling this case better would be an enhancement, not a fix.

Solution patch:
diff --git a/changelog_unreleased/json/10346.md b/changelog_unreleased/json/10346.md
new file mode 100644
index 000000000000..1bbccf9d59b6
--- /dev/null
+++ b/changelog_unreleased/json/10346.md
@@ -0,0 +1,17 @@
+#### Stricter JSON parser (#10346 by @fisker)
+
+Previously we allow all kinds of JavaScript expressions in `json` and `json5` parser, now they are stricter.
+
+<!-- prettier-ignore -->
+```js
+// Input
+[1, 2, 1 + 2]
+
+// Prettier stable
+[1, 2, 1 + 2]
+
+// Prettier main
+SyntaxError: BinaryExpression is not allowed in JSON. (1:8)
+> 1 | [1, 2, 1 + 2]
+    |        ^
+```
diff --git a/src/language-js/parser-babel.js b/src/language-js/parser-babel.js
index 1f9478c07e19..d219ed48d3b7 100644
--- a/src/language-js/parser-babel.js
+++ b/src/language-js/parser-babel.js
@@ -153,6 +153,7 @@ const parseTypeScript = createParse(
   ["typescript"]
 );
 const parseExpression = createParse("parseExpression", ["jsx"]);
+const parseJson = createJsonParse();
 
 const messagesShouldThrow = new Set([
   // TSErrors.UnexpectedTypeAnnotation
@@ -179,15 +180,31 @@ function shouldRethrowRecoveredError(error) {
   return messagesShouldThrow.has(message);
 }
 
-function parseJson(text, parsers, opts) {
-  const ast = parseExpression(text, parsers, opts);
+function createJsonParse(options = {}) {
+  const { allowComments = true } = options;
 
-  for (const comment of ast.comments) {
-    assertJsonNode(comment);
-  }
-  assertJsonNode(ast);
+  return function parse(text /*, parsers, options*/) {
+    let ast;
+    try {
+      ast = require("@babel/parser").parseExpression(text, {
+        tokens: true,
+        ranges: true,
+      });
+    } catch (error) {
+      throw createParseError(error);
+    }
 
-  return ast;
+    if (!allowComments) {
+      // @ts-ignore
+      for (const comment of ast.comments) {
+        assertJsonNode(comment);
+      }
+    }
+
+    assertJsonNode(ast);
+
+    return ast;
+  };
 }
 
 function assertJsonNode(node, parent) {
@@ -262,15 +279,15 @@ module.exports = {
     babel,
     "babel-flow": createParser(parseFlow),
     "babel-ts": createParser(parseTypeScript),
-    json: {
-      ...babelExpression,
+    json: createParser({
+      parse: parseJson,
       hasPragma() {
         return true;
       },
-    },
-    json5: babelExpression,
+    }),
+    json5: createParser(parseJson),
     "json-stringify": createParser({
-      parse: parseJson,
+      parse: createJsonParse({ allowComments: false }),
       astFormat: "estree-json",
     }),
     /** @internal */


Test patch:
diff --git a/tests/markdown/multiparser-json/__snapshots__/jsfmt.spec.js.snap b/tests/markdown/multiparser-json/__snapshots__/jsfmt.spec.js.snap
index b3f00746c3ae..c6ab38824553 100644
--- a/tests/markdown/multiparser-json/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/markdown/multiparser-json/__snapshots__/jsfmt.spec.js.snap
@@ -1,5 +1,26 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`invalid-json.md - {"trailingComma":"all"} format 1`] = `
+====================================options=====================================
+parsers: ["markdown"]
+printWidth: 80
+trailingComma: "all"
+                                                                                | printWidth
+=====================================input======================================
+<!-- #10333 -->
+\`\`\`json
+packages\\the-hub\\cypress\\fixtures\\gridConfiguration.json
+\`\`\`
+=====================================output=====================================
+<!-- #10333 -->
+
+\`\`\`json
+packages\\the-hub\\cypress\\fixtures\\gridConfiguration.json
+\`\`\`
+
+================================================================================
+`;
+
 exports[`jsonc.md - {"trailingComma":"all"} format 1`] = `
 ====================================options=====================================
 parsers: ["markdown"]
diff --git a/tests/markdown/multiparser-json/invalid-json.md b/tests/markdown/multiparser-json/invalid-json.md
new file mode 100644
index 000000000000..4c4e7236b893
--- /dev/null
+++ b/tests/markdown/multiparser-json/invalid-json.md
@@ -0,0 +1,4 @@
+<!-- #10333 -->
+```json
+packages\the-hub\cypress\fixtures\gridConfiguration.json
+```
\ No newline at end of file
diff --git a/tests/misc/errors/json/__snapshots__/jsfmt.spec.js.snap b/tests/misc/errors/json/__snapshots__/jsfmt.spec.js.snap
index c39a1a644f26..488f87de9bc2 100644
--- a/tests/misc/errors/json/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/misc/errors/json/__snapshots__/jsfmt.spec.js.snap
@@ -1,11 +1,23 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`snippet: #0 [json] format 1`] = `
+"Expecting Unicode escape sequence \\\\uXXXX (1:10)
+> 1 | packages\\\\the-hub\\\\cypress\\\\fixtures\\\\gridConfiguration.json
+    |          ^"
+`;
+
 exports[`snippet: #0 [json-stringify] format 1`] = `
 "ObjectProperty with shorthand=true is not allowed in JSON. (1:2)
 > 1 | {foo}
     |  ^"
 `;
 
+exports[`snippet: #1 [json] format 1`] = `
+"BinaryExpression is not allowed in JSON. (1:1)
+> 1 | 1+2
+    | ^"
+`;
+
 exports[`snippet: #1 [json-stringify] format 1`] = `
 "ObjectProperty with computed=true is not allowed in JSON. (1:2)
 > 1 | {[\\"foo\\"]:\\"bar\\"}
@@ -35,3 +47,16 @@ exports[`snippet: #5 [json-stringify] format 1`] = `
 > 1 | {\\"foo\\": () => {}}
     |         ^"
 `;
+
+exports[`snippet: #6 [json-stringify] format 1`] = `
+"CommentBlock is not allowed in JSON. (1:1)
+> 1 | /* comment */{\\"foo\\": 1}
+    | ^"
+`;
+
+exports[`snippet: #7 [json-stringify] format 1`] = `
+"CommentLine is not allowed in JSON. (1:1)
+> 1 | // comment
+    | ^
+  2 | {\\"foo\\": 1}"
+`;
diff --git a/tests/misc/errors/json/jsfmt.spec.js b/tests/misc/errors/json/jsfmt.spec.js
index 38ba22de37d7..725a04abdb58 100644
--- a/tests/misc/errors/json/jsfmt.spec.js
+++ b/tests/misc/errors/json/jsfmt.spec.js
@@ -8,7 +8,20 @@ run_spec(
       '{"foo": false || "bar"}',
       '{"foo": undefined}',
       '{"foo": () => {}}',
+      '/* comment */{"foo": 1}',
+      '// comment\n{"foo": 1}',
     ],
   },
   ["json-stringify"]
 );
+
+run_spec(
+  {
+    dirname: __dirname,
+    snippets: [
+      "packages\\the-hub\\cypress\\fixtures\\gridConfiguration.json",
+      "1+2",
+    ],
+  },
+  ["json"]
+);