Repository: prettier/prettier (JavaScript) — Bug Fix

Issue:
JSON range formatting throws syntax error
**Prettier 1.19.1**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEwAEAdEAjLS0BMaAviADQgQAOMAltAM7KgCGATmxAO4AK7CTFCwBuEWgBNyONizABrODADKVWbSgBzZDDYBXOBQAWMALYAbAOqHa8BqrBwlAm7WE2AnsnAMmFdQzg2GB4ZDRMWZAAzFjMAigArBgAPACEZeUUlFhM4ABl1OCiYuJBEpKV1DTM4AEVdCHgi2IMQVTYAti9E6CkqNnUYCwkYQ2QADgAGCj6IAIsZKi8+uA7hQooAR3r4EOpBEBYGAFooODhxc6k2OC3aa5CWMIikaOaKAJNabT0WhkrquoNQovYotGAsbBDcQjZAECg6Fi0MyVADCEBM4S8UGg6xAugCABUIYJXiVhPoAJJQC6wJRgfo0ACC1KUMHc1SaJRkmkc4KCyAArBRuRo4ABRanIACcxGIQA)
```sh
--parser json
--range-end 9
--range-start 5
```

**Input:**
```jsonc
{ "b": 2 }
```

**Output:**
```jsonc
SyntaxError: Unexpected token (1:4)
> 1 | "b": 2
    |    ^
```

**Expected behavior:**
```jsonc
{ "b": 2 }
```

This is the smallest example I could make that causes the parser to fail. It seems like if the range starts with a `:` but doesn't end with a `,` or `}` after the object value, then the parser throws a syntax error.


Discussion:
Also ran into the same issue. Not a blocker since I can format the whole file for my use case, but was hoping to get a performance boost by only formatting part of the file that changed.

Resolving patch:
diff --git a/changelog_unreleased/json/10497.md b/changelog_unreleased/json/10497.md
new file mode 100644
index 000000000000..10e7b5dc7f6b
--- /dev/null
+++ b/changelog_unreleased/json/10497.md
@@ -0,0 +1,16 @@
+#### Fix syntax error on JSON range formatting (#10497 by @fisker)
+
+<!-- prettier-ignore -->
+```js
+// Input
+[{ a: 1.0000}, {"b":   2.0000 }]
+//                 ^^^^^^^^^^^ range
+
+// Prettier stable
+SyntaxError: Unexpected token (1:4)
+> 1 | "b":   2.0000
+    |    ^
+
+// Prettier main
+[{ a: 1.0000}, { "b": 2.0 }]
+```
diff --git a/src/main/range-util.js b/src/main/range-util.js
index 718026a55ae2..f90a3f9dfa51 100644
--- a/src/main/range-util.js
+++ b/src/main/range-util.js
@@ -3,6 +3,23 @@
 const assert = require("assert");
 const comments = require("./comments");
 
+const isJsonParser = ({ parser }) =>
+  parser === "json" || parser === "json5" || parser === "json-stringify";
+
+function findCommonAncestor(startNodeAndParents, endNodeAndParents) {
+  const startNodeAndAncestors = [
+    startNodeAndParents.node,
+    ...startNodeAndParents.parentNodes,
+  ];
+  const endNodeAndAncestors = new Set([
+    endNodeAndParents.node,
+    ...endNodeAndParents.parentNodes,
+  ]);
+  return startNodeAndAncestors.find(
+    (node) => jsonSourceElements.has(node.type) && endNodeAndAncestors.has(node)
+  );
+}
+
 function findSiblingAncestors(startNodeAndParents, endNodeAndParents, opts) {
   let resultStartNode = startNodeAndParents.node;
   let resultEndNode = endNodeAndParents.node;
@@ -107,6 +124,8 @@ const jsonSourceElements = new Set([
   "NumericLiteral",
   "BooleanLiteral",
   "NullLiteral",
+  "UnaryExpression",
+  "TemplateLiteral",
 ]);
 const graphqlSourceElements = new Set([
   "OperationDefinition",
@@ -141,6 +160,8 @@ function isSourceElement(opts, node) {
     case "meriyah":
       return isJsSourceElement(node.type);
     case "json":
+    case "json5":
+    case "json-stringify":
       return jsonSourceElements.has(node.type);
     case "graphql":
       return graphqlSourceElements.has(node.kind);
@@ -193,11 +214,22 @@ function calculateRange(text, opts, ast) {
     };
   }
 
-  const { startNode, endNode } = findSiblingAncestors(
-    startNodeAndParents,
-    endNodeAndParents,
-    opts
-  );
+  let startNode;
+  let endNode;
+  if (isJsonParser(opts)) {
+    const commonAncestor = findCommonAncestor(
+      startNodeAndParents,
+      endNodeAndParents
+    );
+    startNode = commonAncestor;
+    endNode = commonAncestor;
+  } else {
+    ({ startNode, endNode } = findSiblingAncestors(
+      startNodeAndParents,
+      endNodeAndParents,
+      opts
+    ));
+  }
 
   return {
     rangeStart: Math.min(locStart(startNode), locStart(endNode)),


Test patch:
diff --git a/tests/json/range/__snapshots__/jsfmt.spec.js.snap b/tests/json/range/__snapshots__/jsfmt.spec.js.snap
index dcdef5f998b6..53213d9c2049 100644
--- a/tests/json/range/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/json/range/__snapshots__/jsfmt.spec.js.snap
@@ -1,5 +1,115 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`cross-array.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 51
+rangeStart: 12
+      |                                                                                | printWidth
+=====================================input======================================
+   1 | [{
+   2 | a: [
+   3 | 1,
+>  4 |  2,  3,4,5,6,      7,
+     |  ^^^^^^^^^^^^^^^^^^^^
+>  5 | 8
+     | ^
+>  6 | ],
+     | ^
+>  7 | b: [1, 2,      3, 4],
+     | ^^^^^^^^^^^^^
+   8 | c: [1,     2]
+   9 | }
+  10 | ,{a:      2}]
+  11 |
+=====================================output=====================================
+[{
+  "a": [1, 2, 3, 4, 5, 6, 7, 8],
+  "b": [1, 2, 3, 4],
+  "c": [1, 2]
+}
+,{a:      2}]
+
+================================================================================
+`;
+
+exports[`cross-array2.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 36
+rangeStart: 11
+     |                                                                                | printWidth
+=====================================input======================================
+  1 | {
+  2 | a: [
+  3 | 1,
+> 4 |  2,  3,4,5,6,   [1, 2,      3, 4],   7,
+    |  ^^^^^^^^^^^^^^^^^^^^^^^^^
+  5 | 8
+  6 | ],
+  7 | c: [1,     2]
+  8 | }
+  9 |
+=====================================output=====================================
+{
+a: [1, 2, 3, 4, 5, 6, [1, 2, 3, 4], 7, 8],
+c: [1,     2]
+}
+
+================================================================================
+`;
+
+exports[`cross-object.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 31
+rangeStart: 12
+     |                                                                                | printWidth
+=====================================input======================================
+  1 | [{a:
+> 2 | { "b": 2, "c": 3 },
+    |        ^^^^^^^^^^^^
+> 3 | b: {d:4},
+    | ^^^^^^
+  4 | c: {d:     6}
+  5 | },
+  6 | {a:           1}]
+  7 |
+=====================================output=====================================
+[{ "a": { "b": 2, "c": 3 }, "b": { "d": 4 }, "c": { "d": 6 } },
+{a:           1}]
+
+================================================================================
+`;
+
+exports[`cross-object-2.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 28
+rangeStart: 11
+     |                                                                                | printWidth
+=====================================input======================================
+  1 | {a:
+> 2 | { "b": 2, "c": 3,
+    |        ^^^^^^^^^^
+> 3 | d: {d:4}, },
+    | ^^^^^^
+  4 | c: {d:     6}
+  5 | }
+  6 |
+=====================================output=====================================
+{a:
+{ "b": 2, "c": 3, "d": { "d": 4 } },
+c: {d:     6}
+}
+
+================================================================================
+`;
+
 exports[`identifier.json format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -17,6 +127,113 @@ rangeStart: 1
 ================================================================================
 `;
 
+exports[`identifier-2.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 3
+rangeStart: 2
+     |                                                                                | printWidth
+=====================================input======================================
+> 1 | Infinity
+    |   ^
+  2 |
+=====================================output=====================================
+Infinity
+
+================================================================================
+`;
+
+exports[`inside-array.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+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]
+}
+
+================================================================================
+`;
+
+exports[`inside-object.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 20
+rangeStart: 11
+     |                                                                                | printWidth
+=====================================input======================================
+  1 | {a:
+> 2 | { "b": 2, "c": 3 },
+    |        ^^^^^^^^^
+  3 | b: {d:4}
+  4 | }
+  5 |
+=====================================output=====================================
+{a:
+{ "b": 2, "c": 3 },
+b: {d:4}
+}
+
+================================================================================
+`;
+
+exports[`issue-4009.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 19
+rangeStart: 1
+     |                                                                                | printWidth
+=====================================input======================================
+> 1 | {
+    |  ^
+> 2 |     "foo": "bar"
+    | ^^^^^^^^^^^^^^^^
+> 3 | }
+    | ^
+  4 |
+=====================================output=====================================
+{
+  "foo": "bar"
+}
+
+================================================================================
+`;
+
+exports[`issue-7116.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 7
+rangeStart: 4
+     |                                                                                | printWidth
+=====================================input======================================
+> 1 | {"b": 2}
+    |     ^^^
+  2 |
+=====================================output=====================================
+{ "b": 2 }
+
+================================================================================
+`;
+
 exports[`issue2297.json format 1`] = `
 ====================================options=====================================
 parsers: ["json"]
@@ -33,3 +250,121 @@ rangeStart: 5
 
 ================================================================================
 `;
+
+exports[`number.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 16
+rangeStart: 7
+     |                                                                                | printWidth
+=====================================input======================================
+> 1 | { "b": 2, "c": 3 }
+    |        ^^^^^^^^^
+  2 |
+=====================================output=====================================
+{ "b": 2, "c": 3 }
+
+================================================================================
+`;
+
+exports[`template-literal.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 7
+rangeStart: 6
+     |                                                                                | printWidth
+=====================================input======================================
+  1 | [
+> 2 | {a:\`a\`,n:
+    |     ^
+  3 | ''},
+  4 | {a:\`a\`,n:
+  5 | ''}
+  6 | ]
+  7 |
+=====================================output=====================================
+[
+{a:\`a\`,n:
+''},
+{a:\`a\`,n:
+''}
+]
+
+================================================================================
+`;
+
+exports[`template-literal-2.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 7
+rangeStart: 4
+     |                                                                                | printWidth
+=====================================input======================================
+  1 | [
+> 2 | {a:\`a\`,n:
+    |   ^^^
+  3 | ''},
+  4 | {a:\`a\`,n:
+  5 | ''}
+  6 | ]
+  7 |
+=====================================output=====================================
+[
+{ "a": \`a\`, "n": "" },
+{a:\`a\`,n:
+''}
+]
+
+================================================================================
+`;
+
+exports[`unary-expression.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 15
+rangeStart: 9
+      |                                                                                | printWidth
+=====================================input======================================
+   1 | {a:
+   2 | [
+   3 | 1,
+>  4 | -2,
+     | ^^^
+>  5 | -3
+     | ^^
+   6 |
+   7 | ],
+   8 | b: [1,    2]
+   9 | }
+  10 |
+=====================================output=====================================
+{a:
+[1, -2, -3],
+b: [1,    2]
+}
+
+================================================================================
+`;
+
+exports[`unary-expression-2.json format 1`] = `
+====================================options=====================================
+parsers: ["json"]
+printWidth: 80
+rangeEnd: 9
+rangeStart: 2
+     |                                                                                | printWidth
+=====================================input======================================
+  1 | -
+> 2 | 2.00000
+    | ^^^^^^^
+  3 |
+=====================================output=====================================
+-
+2.0
+
+================================================================================
+`;
diff --git a/tests/json/range/cross-array.json b/tests/json/range/cross-array.json
new file mode 100644
index 000000000000..702f3c890c00
--- /dev/null
+++ b/tests/json/range/cross-array.json
@@ -0,0 +1,10 @@
+[{
+a: [
+1,
+ <<<PRETTIER_RANGE_START>>>2,  3,4,5,6,      7,
+8
+],
+b: [1, 2,    <<<PRETTIER_RANGE_END>>>  3, 4],
+c: [1,     2]
+}
+,{a:      2}]
diff --git a/tests/json/range/cross-array2.json b/tests/json/range/cross-array2.json
new file mode 100644
index 000000000000..6c1585ce20e2
--- /dev/null
+++ b/tests/json/range/cross-array2.json
@@ -0,0 +1,8 @@
+{
+a: [
+1,
+ <<<PRETTIER_RANGE_START>>>2,  3,4,5,6,   [1, 2,    <<<PRETTIER_RANGE_END>>>  3, 4],   7,
+8
+],
+c: [1,     2]
+}
diff --git a/tests/json/range/cross-object-2.json b/tests/json/range/cross-object-2.json
new file mode 100644
index 000000000000..05c3314fb36c
--- /dev/null
+++ b/tests/json/range/cross-object-2.json
@@ -0,0 +1,5 @@
+{a:
+{ "b": <<<PRETTIER_RANGE_START>>>2, "c": 3,
+d: {d:<<<PRETTIER_RANGE_END>>>4}, },
+c: {d:     6}
+}
diff --git a/tests/json/range/cross-object.json b/tests/json/range/cross-object.json
new file mode 100644
index 000000000000..891c742a0dfc
--- /dev/null
+++ b/tests/json/range/cross-object.json
@@ -0,0 +1,6 @@
+[{a:
+{ "b": <<<PRETTIER_RANGE_START>>>2, "c": 3 },
+b: {d:<<<PRETTIER_RANGE_END>>>4},
+c: {d:     6}
+},
+{a:           1}]
diff --git a/tests/json/range/identifier-2.json b/tests/json/range/identifier-2.json
new file mode 100644
index 000000000000..6dff4c257ca8
--- /dev/null
+++ b/tests/json/range/identifier-2.json
@@ -0,0 +1,1 @@
+In<<<PRETTIER_RANGE_START>>>f<<<PRETTIER_RANGE_END>>>inity
diff --git a/tests/json/range/inside-array.json b/tests/json/range/inside-array.json
new file mode 100644
index 000000000000..2f740ea154b6
--- /dev/null
+++ b/tests/json/range/inside-array.json
@@ -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/json/range/inside-object.json b/tests/json/range/inside-object.json
new file mode 100644
index 000000000000..13bbf7a9803d
--- /dev/null
+++ b/tests/json/range/inside-object.json
@@ -0,0 +1,4 @@
+{a:
+{ "b": <<<PRETTIER_RANGE_START>>>2, "c": 3<<<PRETTIER_RANGE_END>>> },
+b: {d:4}
+}
diff --git a/tests/json/range/issue-4009.json b/tests/json/range/issue-4009.json
new file mode 100644
index 000000000000..fd21f6a78ae5
--- /dev/null
+++ b/tests/json/range/issue-4009.json
@@ -0,0 +1,3 @@
+{<<<PRETTIER_RANGE_START>>>
+    "foo": "bar"
+<<<PRETTIER_RANGE_END>>>}
diff --git a/tests/json/range/issue-7116.json b/tests/json/range/issue-7116.json
new file mode 100644
index 000000000000..c55c0f83be80
--- /dev/null
+++ b/tests/json/range/issue-7116.json
@@ -0,0 +1,1 @@
+{"b"<<<PRETTIER_RANGE_START>>>: 2<<<PRETTIER_RANGE_END>>>}
diff --git a/tests/json/range/json-stringify/__snapshots__/jsfmt.spec.js.snap b/tests/json/range/json-stringify/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..7e343effcf12
--- /dev/null
+++ b/tests/json/range/json-stringify/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`template-literal.json format 1`] = `
+====================================options=====================================
+parsers: ["json-stringify"]
+printWidth: 80
+rangeEnd: 8
+rangeStart: 7
+     |                                                                                | printWidth
+=====================================input======================================
+  1 | [
+> 2 | {a:\`aaa\`,n:
+    |      ^
+  3 | ''},
+  4 | {a:\`a\`,n:
+  5 | ''}
+  6 | ]
+  7 |
+=====================================output=====================================
+[
+  {
+    "a": "aaa",
+    "n": ""
+  },
+  {
+    "a": "a",
+    "n": ""
+  }
+]
+
+================================================================================
+`;
diff --git a/tests/json/range/json-stringify/jsfmt.spec.js b/tests/json/range/json-stringify/jsfmt.spec.js
new file mode 100644
index 000000000000..22498fda370b
--- /dev/null
+++ b/tests/json/range/json-stringify/jsfmt.spec.js
@@ -0,0 +1,1 @@
+run_spec(__dirname, ["json-stringify"]);
diff --git a/tests/json/range/json-stringify/template-literal.json b/tests/json/range/json-stringify/template-literal.json
new file mode 100644
index 000000000000..cde30cef79e5
--- /dev/null
+++ b/tests/json/range/json-stringify/template-literal.json
@@ -0,0 +1,6 @@
+[
+{a:`a<<<PRETTIER_RANGE_START>>>a<<<PRETTIER_RANGE_END>>>a`,n:
+''},
+{a:`a`,n:
+''}
+]
diff --git a/tests/json/range/json5/__snapshots__/jsfmt.spec.js.snap b/tests/json/range/json5/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..b2da96a49a84
--- /dev/null
+++ b/tests/json/range/json5/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`inside-array.json format 1`] = `
+====================================options=====================================
+parsers: ["json5"]
+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/json/range/json5/inside-array.json b/tests/json/range/json5/inside-array.json
new file mode 100644
index 000000000000..2f740ea154b6
--- /dev/null
+++ b/tests/json/range/json5/inside-array.json
@@ -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/json/range/json5/jsfmt.spec.js b/tests/json/range/json5/jsfmt.spec.js
new file mode 100644
index 000000000000..a4031df291ee
--- /dev/null
+++ b/tests/json/range/json5/jsfmt.spec.js
@@ -0,0 +1,1 @@
+run_spec(__dirname, ["json5"]);
diff --git a/tests/json/range/number.json b/tests/json/range/number.json
new file mode 100644
index 000000000000..29bea7dc1027
--- /dev/null
+++ b/tests/json/range/number.json
@@ -0,0 +1,1 @@
+{ "b": <<<PRETTIER_RANGE_START>>>2, "c": 3<<<PRETTIER_RANGE_END>>> }
diff --git a/tests/json/range/template-literal-2.json b/tests/json/range/template-literal-2.json
new file mode 100644
index 000000000000..2c923a87af40
--- /dev/null
+++ b/tests/json/range/template-literal-2.json
@@ -0,0 +1,6 @@
+[
+{a<<<PRETTIER_RANGE_START>>>:`a<<<PRETTIER_RANGE_END>>>`,n:
+''},
+{a:`a`,n:
+''}
+]
diff --git a/tests/json/range/template-literal.json b/tests/json/range/template-literal.json
new file mode 100644
index 000000000000..2119b792585d
--- /dev/null
+++ b/tests/json/range/template-literal.json
@@ -0,0 +1,6 @@
+[
+{a:`<<<PRETTIER_RANGE_START>>>a<<<PRETTIER_RANGE_END>>>`,n:
+''},
+{a:`a`,n:
+''}
+]
diff --git a/tests/json/range/unary-expression-2.json b/tests/json/range/unary-expression-2.json
new file mode 100644
index 000000000000..e3f21808f2c6
--- /dev/null
+++ b/tests/json/range/unary-expression-2.json
@@ -0,0 +1,2 @@
+-
+<<<PRETTIER_RANGE_START>>>2.00000<<<PRETTIER_RANGE_END>>>
diff --git a/tests/json/range/unary-expression.json b/tests/json/range/unary-expression.json
new file mode 100644
index 000000000000..d9e9c931bd2c
--- /dev/null
+++ b/tests/json/range/unary-expression.json
@@ -0,0 +1,9 @@
+{a:
+[
+1,
+<<<PRETTIER_RANGE_START>>>-2,
+-3<<<PRETTIER_RANGE_END>>>
+
+],
+b: [1,    2]
+}