Allow comment only JSONC files (#17269)

This commit is contained in:
fisker Cheung authored and GitHub committed 2025-03-20 23:01:10 +08:00
1 parent 959ec00486
commit 5d8337e8e0
8 files changed
+465 -30

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
#### Allow format comment-only JSONC files (#17269 by @fisker)
<!-- prettier-ignore -->
```jsonc
// Input
// Comment
// Prettier stable
SyntaxError: Unexpected token (1:11)
> 1 | // Comment
| ^
// Prettier main
// Comment
```
+1 -1
View File
@@ -101,7 +101,7 @@ function printEstree(path, options, print, args) {
case "JsExpressionRoot":
return print("node");
case "JsonRoot":
return [print("node"), hardline];
return [printDanglingComments(path, options), print("node"), hardline];
case "File":
return printHtmlBinding(path, options, print) ?? print("program");
// Babel extension.
+65 -28
View File
@@ -1,35 +1,76 @@
import { parseExpression } from "@babel/parser";
import { parse, parseExpression } from "@babel/parser";
import createError from "../common/parser-create-error.js";
import createBabelParseError from "../language-js/parse/utils/create-babel-parse-error.js";
import createParser from "../language-js/parse/utils/create-parser.js";
import wrapBabelExpression from "../language-js/parse/utils/wrap-babel-expression.js";
import isNonEmptyArray from "../utils/is-non-empty-array.js";
function createJsonParse(options = {}) {
const { allowComments = true } = options;
const babelParseOptions = {
tokens: true,
ranges: true,
attachComment: false,
};
return function parse(text) {
let ast;
try {
ast = parseExpression(text, {
tokens: true,
ranges: true,
attachComment: false,
});
} catch (/** @type {any} */ error) {
function parseEmptyJson(text) {
const file = parse(text, babelParseOptions);
const { program } = file;
// Not an empty JSON
/* c8 ignore next */
if (
!(
program.body.length === 0 &&
program.directives.length === 0 &&
!program.interpreter
)
) {
return;
}
return file;
}
function parseJson(text, options = {}) {
const { allowComments = true, allowEmpty = false } = options;
let ast;
try {
ast = parseExpression(text, babelParseOptions);
} catch (/** @type {any} */ error) {
if (
allowEmpty &&
error.code === "BABEL_PARSER_SYNTAX_ERROR" &&
error.reasonCode === "UnexpectedToken" &&
error.pos === text.length
) {
try {
ast = parseEmptyJson(text);
} catch {
// No op
}
}
if (!ast) {
throw createBabelParseError(error);
}
}
// @ts-expect-error
if (!allowComments && isNonEmptyArray(ast.comments)) {
// @ts-expect-error
if (!allowComments && isNonEmptyArray(ast.comments)) {
// @ts-expect-error
throw createJsonError(ast.comments[0], "Comment");
}
throw createJsonError(ast.comments[0], "Comment");
}
assertJsonNode(ast);
ast = wrapBabelExpression(ast, { type: "JsonRoot", text });
return wrapBabelExpression(ast, { type: "JsonRoot", text });
};
if (ast.node.type === "File") {
delete ast.node;
} else {
assertJsonNode(ast.node);
}
return ast;
}
function createJsonError(node, description) {
@@ -131,19 +172,15 @@ function assertJsonNode(node) {
}
}
const parseJson = createJsonParse();
const jsonParsers = {
json: createParser({
parse: parseJson,
hasPragma() {
return true;
},
parse: (text) => parseJson(text),
hasPragma: () => true,
}),
json5: createParser(parseJson),
jsonc: createParser(parseJson),
json5: createParser((text) => parseJson(text)),
jsonc: createParser((text) => parseJson(text, { allowEmpty: true })),
"json-stringify": createParser({
parse: createJsonParse({ allowComments: false }),
parse: (text) => parseJson(text, { allowComments: false }),
astFormat: "estree-json",
}),
};
+1 -1
View File
@@ -193,7 +193,7 @@ function attachComments(ast, options) {
addLeadingComment(ast, comment);
continue;
}
if (locEnd(comment) - locEnd(ast) >= 0) {
if (locEnd(comment) > locEnd(ast)) {
addTrailingComment(ast, comment);
continue;
}
@@ -0,0 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`snippet: #0 [json] format 1`] = `
"Unexpected token (4:29)
2 | /** Block comment 1 */
3 | // Line comment 2
> 4 | /** Block comment 3 */
| ^
Cause: Unexpected token (4:28)"
`;
exports[`snippet: #0 [json-stringify] format 1`] = `
"Unexpected token (4:29)
2 | /** Block comment 1 */
3 | // Line comment 2
> 4 | /** Block comment 3 */
| ^
Cause: Unexpected token (4:28)"
`;
exports[`snippet: #0 [json5] format 1`] = `
"Unexpected token (4:29)
2 | /** Block comment 1 */
3 | // Line comment 2
> 4 | /** Block comment 3 */
| ^
Cause: Unexpected token (4:28)"
`;
exports[`snippet: #0 format 1`] = `
====================================options=====================================
parsers: ["jsonc", "json", "json5", "json-stringify"]
printWidth: 80
| printWidth
=====================================input======================================
// Line comment 1
/** Block comment 1 */
// Line comment 2
/** Block comment 3 */
=====================================output=====================================
// Line comment 1
/** Block comment 1 */
// Line comment 2
/** Block comment 3 */
================================================================================
`;
@@ -0,0 +1,25 @@
import { outdent } from "outdent";
runFormatTest(
{
importMeta: import.meta,
snippets: [
// "// Only line comment",
// "/* Only block comment */",
outdent`
// Line comment 1
/** Block comment 1 */
// Line comment 2
/** Block comment 3 */
`,
],
},
["jsonc", "json", "json5", "json-stringify"],
{
errors: {
json: true,
json5: true,
"json-stringify": true,
},
},
);
@@ -13,18 +13,46 @@ exports[`snippet: #0 [json] format 2`] = `
Cause: A 'set' accessor must have exactly one formal parameter. (1:8)"
`;
exports[`snippet: #0 [json] format 3`] = `
"Unexpected token (1:2)
> 1 | [
| ^
Cause: Unexpected token (1:1)"
`;
exports[`snippet: #0 [json-stringify] format 1`] = `
"Shorthand property is not allowed in JSON. (1:2)
> 1 | {foo}
| ^^^"
`;
exports[`snippet: #0 [json-stringify] format 2`] = `
"Unexpected token (1:2)
> 1 | [
| ^
Cause: Unexpected token (1:1)"
`;
exports[`snippet: #0 [json5] format 1`] = `
"Shorthand property is not allowed in JSON. (1:2)
> 1 | {foo}
| ^^^"
`;
exports[`snippet: #0 [json5] format 2`] = `
"Unexpected token (1:2)
> 1 | [
| ^
Cause: Unexpected token (1:1)"
`;
exports[`snippet: #0 [jsonc] format 1`] = `
"Unexpected token (1:2)
> 1 | [
| ^
Cause: Unexpected token (1:1)"
`;
exports[`snippet: #1 [json] format 1`] = `
"Computed key is not allowed in JSON. (1:3)
> 1 | {["foo"]:"bar"}
@@ -38,18 +66,46 @@ exports[`snippet: #1 [json] format 2`] = `
Cause: Private name #bar is not defined. (1:20)"
`;
exports[`snippet: #1 [json] format 3`] = `
"Unexpected token (1:1)
> 1 | ;
| ^
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #1 [json-stringify] format 1`] = `
"Computed key is not allowed in JSON. (1:3)
> 1 | {["foo"]:"bar"}
| ^^^^^"
`;
exports[`snippet: #1 [json-stringify] format 2`] = `
"Unexpected token (1:1)
> 1 | ;
| ^
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #1 [json5] format 1`] = `
"Computed key is not allowed in JSON. (1:3)
> 1 | {["foo"]:"bar"}
| ^^^^^"
`;
exports[`snippet: #1 [json5] format 2`] = `
"Unexpected token (1:1)
> 1 | ;
| ^
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #1 [jsonc] format 1`] = `
"Unexpected token (1:1)
> 1 | ;
| ^
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #2 [json] format 1`] = `
"Operator '~' is not allowed in JSON. (1:9)
> 1 | {"foo": ~1}
@@ -63,18 +119,46 @@ exports[`snippet: #2 [json] format 2`] = `
Cause: 'await' is only allowed within async functions and at the top levels of modules. (1:13)"
`;
exports[`snippet: #2 [json] format 3`] = `
"Unexpected token (1:9)
> 1 | "string";
| ^
Cause: Unexpected token (1:8)"
`;
exports[`snippet: #2 [json-stringify] format 1`] = `
"Operator '~' is not allowed in JSON. (1:9)
> 1 | {"foo": ~1}
| ^^"
`;
exports[`snippet: #2 [json-stringify] format 2`] = `
"Unexpected token (1:9)
> 1 | "string";
| ^
Cause: Unexpected token (1:8)"
`;
exports[`snippet: #2 [json5] format 1`] = `
"Operator '~' is not allowed in JSON. (1:9)
> 1 | {"foo": ~1}
| ^^"
`;
exports[`snippet: #2 [json5] format 2`] = `
"Unexpected token (1:9)
> 1 | "string";
| ^
Cause: Unexpected token (1:8)"
`;
exports[`snippet: #2 [jsonc] format 1`] = `
"Unexpected token (1:9)
> 1 | "string";
| ^
Cause: Unexpected token (1:8)"
`;
exports[`snippet: #3 [json] format 1`] = `
"'LogicalExpression' is not allowed in JSON. (1:9)
> 1 | {"foo": false || "bar"}
@@ -87,18 +171,50 @@ exports[`snippet: #3 [json] format 2`] = `
| ^"
`;
exports[`snippet: #3 [json] format 3`] = `
"Unexpected token (2:1)
1 | "string"
> 2 | ;
| ^
Cause: Unexpected token (2:0)"
`;
exports[`snippet: #3 [json-stringify] format 1`] = `
"'LogicalExpression' is not allowed in JSON. (1:9)
> 1 | {"foo": false || "bar"}
| ^^^^^^^^^^^^^^"
`;
exports[`snippet: #3 [json-stringify] format 2`] = `
"Unexpected token (2:1)
1 | "string"
> 2 | ;
| ^
Cause: Unexpected token (2:0)"
`;
exports[`snippet: #3 [json5] format 1`] = `
"'LogicalExpression' is not allowed in JSON. (1:9)
> 1 | {"foo": false || "bar"}
| ^^^^^^^^^^^^^^"
`;
exports[`snippet: #3 [json5] format 2`] = `
"Unexpected token (2:1)
1 | "string"
> 2 | ;
| ^
Cause: Unexpected token (2:0)"
`;
exports[`snippet: #3 [jsonc] format 1`] = `
"Unexpected token (2:1)
1 | "string"
> 2 | ;
| ^
Cause: Unexpected token (2:0)"
`;
exports[`snippet: #4 [json] format 1`] = `
"'ArrowFunctionExpression' is not allowed in JSON. (1:9)
> 1 | {"foo": () => {}}
@@ -112,18 +228,46 @@ exports[`snippet: #4 [json] format 2`] = `
Cause: 'import' and 'export' may only appear at the top level. (1:13)"
`;
exports[`snippet: #4 [json] format 3`] = `
"Unexpected token (1:15)
> 1 | /* comment */[
| ^
Cause: Unexpected token (1:14)"
`;
exports[`snippet: #4 [json-stringify] format 1`] = `
"'ArrowFunctionExpression' is not allowed in JSON. (1:9)
> 1 | {"foo": () => {}}
| ^^^^^^^^"
`;
exports[`snippet: #4 [json-stringify] format 2`] = `
"Unexpected token (1:15)
> 1 | /* comment */[
| ^
Cause: Unexpected token (1:14)"
`;
exports[`snippet: #4 [json5] format 1`] = `
"'ArrowFunctionExpression' is not allowed in JSON. (1:9)
> 1 | {"foo": () => {}}
| ^^^^^^^^"
`;
exports[`snippet: #4 [json5] format 2`] = `
"Unexpected token (1:15)
> 1 | /* comment */[
| ^
Cause: Unexpected token (1:14)"
`;
exports[`snippet: #4 [jsonc] format 1`] = `
"Unexpected token (1:15)
> 1 | /* comment */[
| ^
Cause: Unexpected token (1:14)"
`;
exports[`snippet: #5 [json] format 1`] = `
"Expecting Unicode escape sequence \\uXXXX. (1:10)
> 1 | packages\\the-hub\\cypress\\fixtures\\gridConfiguration.json
@@ -138,6 +282,13 @@ exports[`snippet: #5 [json] format 2`] = `
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #5 [json] format 3`] = `
"Unexpected token (1:14)
> 1 | /* comment */;
| ^
Cause: Unexpected token (1:13)"
`;
exports[`snippet: #5 [json-stringify] format 1`] = `
"Expecting Unicode escape sequence \\uXXXX. (1:10)
> 1 | packages\\the-hub\\cypress\\fixtures\\gridConfiguration.json
@@ -145,6 +296,13 @@ exports[`snippet: #5 [json-stringify] format 1`] = `
Cause: Expecting Unicode escape sequence \\uXXXX. (1:9)"
`;
exports[`snippet: #5 [json-stringify] format 2`] = `
"Unexpected token (1:14)
> 1 | /* comment */;
| ^
Cause: Unexpected token (1:13)"
`;
exports[`snippet: #5 [json5] format 1`] = `
"Expecting Unicode escape sequence \\uXXXX. (1:10)
> 1 | packages\\the-hub\\cypress\\fixtures\\gridConfiguration.json
@@ -152,6 +310,20 @@ exports[`snippet: #5 [json5] format 1`] = `
Cause: Expecting Unicode escape sequence \\uXXXX. (1:9)"
`;
exports[`snippet: #5 [json5] format 2`] = `
"Unexpected token (1:14)
> 1 | /* comment */;
| ^
Cause: Unexpected token (1:13)"
`;
exports[`snippet: #5 [jsonc] format 1`] = `
"Unexpected token (1:14)
> 1 | /* comment */;
| ^
Cause: Unexpected token (1:13)"
`;
exports[`snippet: #6 [json] format 1`] = `
"'BinaryExpression' is not allowed in JSON. (1:1)
> 1 | 1+2
@@ -165,18 +337,46 @@ exports[`snippet: #6 [json] format 2`] = `
Cause: Unexpected token, expected "," (1:2)"
`;
exports[`snippet: #6 [json] format 3`] = `
"Unexpected token (1:22)
> 1 | /* comment */"string";
| ^
Cause: Unexpected token (1:21)"
`;
exports[`snippet: #6 [json-stringify] format 1`] = `
"'BinaryExpression' is not allowed in JSON. (1:1)
> 1 | 1+2
| ^^^"
`;
exports[`snippet: #6 [json-stringify] format 2`] = `
"Unexpected token (1:22)
> 1 | /* comment */"string";
| ^
Cause: Unexpected token (1:21)"
`;
exports[`snippet: #6 [json5] format 1`] = `
"'BinaryExpression' is not allowed in JSON. (1:1)
> 1 | 1+2
| ^^^"
`;
exports[`snippet: #6 [json5] format 2`] = `
"Unexpected token (1:22)
> 1 | /* comment */"string";
| ^
Cause: Unexpected token (1:21)"
`;
exports[`snippet: #6 [jsonc] format 1`] = `
"Unexpected token (1:22)
> 1 | /* comment */"string";
| ^
Cause: Unexpected token (1:21)"
`;
exports[`snippet: #7 [json] format 1`] = `
"Shorthand property is not allowed in JSON. (1:2)
> 1 | {Infinity}
@@ -189,18 +389,50 @@ exports[`snippet: #7 [json] format 2`] = `
| ^"
`;
exports[`snippet: #7 [json] format 3`] = `
"Unexpected token (2:1)
1 | /* comment */"string"
> 2 | ;
| ^
Cause: Unexpected token (2:0)"
`;
exports[`snippet: #7 [json-stringify] format 1`] = `
"Shorthand property is not allowed in JSON. (1:2)
> 1 | {Infinity}
| ^^^^^^^^"
`;
exports[`snippet: #7 [json-stringify] format 2`] = `
"Unexpected token (2:1)
1 | /* comment */"string"
> 2 | ;
| ^
Cause: Unexpected token (2:0)"
`;
exports[`snippet: #7 [json5] format 1`] = `
"Shorthand property is not allowed in JSON. (1:2)
> 1 | {Infinity}
| ^^^^^^^^"
`;
exports[`snippet: #7 [json5] format 2`] = `
"Unexpected token (2:1)
1 | /* comment */"string"
> 2 | ;
| ^
Cause: Unexpected token (2:0)"
`;
exports[`snippet: #7 [jsonc] format 1`] = `
"Unexpected token (2:1)
1 | /* comment */"string"
> 2 | ;
| ^
Cause: Unexpected token (2:0)"
`;
exports[`snippet: #8 [json] format 1`] = `
"Computed key is not allowed in JSON. (1:3)
> 1 | {[key]: 1}
@@ -213,18 +445,46 @@ exports[`snippet: #8 [json] format 2`] = `
| ^"
`;
exports[`snippet: #8 [json] format 3`] = `
"Unexpected token (1:1)
> 1 | #!/usr/bin/env node
| ^
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #8 [json-stringify] format 1`] = `
"Computed key is not allowed in JSON. (1:3)
> 1 | {[key]: 1}
| ^^^"
`;
exports[`snippet: #8 [json-stringify] format 2`] = `
"Unexpected token (1:1)
> 1 | #!/usr/bin/env node
| ^
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #8 [json5] format 1`] = `
"Computed key is not allowed in JSON. (1:3)
> 1 | {[key]: 1}
| ^^^"
`;
exports[`snippet: #8 [json5] format 2`] = `
"Unexpected token (1:1)
> 1 | #!/usr/bin/env node
| ^
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #8 [jsonc] format 1`] = `
"Unexpected token (1:1)
> 1 | #!/usr/bin/env node
| ^
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #9 [json] format 1`] = `
"Computed key is not allowed in JSON. (1:3)
> 1 | {[key()]: 1}
@@ -237,18 +497,50 @@ exports[`snippet: #9 [json] format 2`] = `
| ^"
`;
exports[`snippet: #9 [json] format 3`] = `
"Unexpected token (1:1)
> 1 | #!/usr/bin/env node
| ^
2 | []
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #9 [json-stringify] format 1`] = `
"Computed key is not allowed in JSON. (1:3)
> 1 | {[key()]: 1}
| ^^^^^"
`;
exports[`snippet: #9 [json-stringify] format 2`] = `
"Unexpected token (1:1)
> 1 | #!/usr/bin/env node
| ^
2 | []
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #9 [json5] format 1`] = `
"Computed key is not allowed in JSON. (1:3)
> 1 | {[key()]: 1}
| ^^^^^"
`;
exports[`snippet: #9 [json5] format 2`] = `
"Unexpected token (1:1)
> 1 | #!/usr/bin/env node
| ^
2 | []
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #9 [jsonc] format 1`] = `
"Unexpected token (1:1)
> 1 | #!/usr/bin/env node
| ^
2 | []
Cause: Unexpected token (1:0)"
`;
exports[`snippet: #10 [json] format 1`] = `
"Computed key is not allowed in JSON. (1:3)
> 1 | {['CallExpression']: 1}
@@ -101,3 +101,22 @@ runFormatTest(
},
["json"],
);
runFormatTest(
{
importMeta: import.meta,
snippets: [
"[",
";",
'"string";',
'"string"\n;',
"/* comment */[",
"/* comment */;",
'/* comment */"string";',
'/* comment */"string"\n;',
"#!/usr/bin/env node",
"#!/usr/bin/env node\n[]",
],
},
["json", "json5", "jsonc", "json-stringify"],
);