From a3cbf3ccf9affee15bb6e585fa128746b8e36657 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 16 Oct 2024 14:15:19 -0400 Subject: [PATCH] feat: Add allowTrailingCommas option for JSONC fixes #41 --- README.md | 31 ++++++++++ package.json | 2 +- src/languages/json-language.js | 31 ++++++++-- tests/languages/json-language.test.js | 85 +++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 342c41c..00560d9 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,37 @@ In JSONC and JSON5 files, you can also use [rule configurations comments](https: Both line and block comments can be used for all kinds of configuration comments. +## Allowing trailing commas in JSONC + +The Microsoft implementation of JSONC optionally allows for trailing commas in objects and arrays (files like `tsconfig.json` have this option enabled by default in Visual Studio Code). To enable trailing commas in JSONC files, use the `allowTrailingCommas` language option, as in this example: + +```js +import json from "@eslint/json"; + +export default [ + + // lint JSONC files + { + files: ["**/*.jsonc"], + language: "json/jsonc", + ...json.configs.recommended, + }, + + // lint JSONC files and allow trailing commas + { + files: ["**/tsconfig.json", ".vscode/*.json"], + language: "json/jsonc", + languageOptions: { + allowTrailingCommas: true + } + ...json.configs.recommended, + }, + +]; +``` + +**Note:** The `allowTrailingCommas` option is only valid for the `json/jsonc` language. + ## Frequently Asked Questions ### How does this relate to `eslint-plugin-json` and `eslint-plugin-jsonc`? diff --git a/package.json b/package.json index 1499353..1c4a513 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "dependencies": { "@eslint/plugin-kit": "^0.2.0", - "@humanwhocodes/momoa": "^3.2.1" + "@humanwhocodes/momoa": "^3.3.0" }, "devDependencies": { "@eslint/core": "^0.6.0", diff --git a/src/languages/json-language.js b/src/languages/json-language.js index efd843d..27ca75e 100644 --- a/src/languages/json-language.js +++ b/src/languages/json-language.js @@ -21,6 +21,10 @@ import { visitorKeys } from "@humanwhocodes/momoa"; /** @typedef {import("@eslint/core").OkParseResult} OkParseResult */ /** @typedef {import("@eslint/core").ParseResult} ParseResult */ /** @typedef {import("@eslint/core").File} File */ +/** + * @typedef {Object} JSONLanguageOptions + * @property {boolean} [allowTrailingCommas] Whether to allow trailing commas. + */ //----------------------------------------------------------------------------- // Exports @@ -76,26 +80,42 @@ export class JSONLanguage { this.#mode = mode; } - /* eslint-disable class-methods-use-this, no-unused-vars -- Required to complete interface. */ /** * Validates the language options. - * @param {Object} languageOptions The language options to validate. + * @param {JSONLanguageOptions} languageOptions The language options to validate. * @returns {void} * @throws {Error} When the language options are invalid. */ validateLanguageOptions(languageOptions) { - // no-op + if (languageOptions.allowTrailingCommas !== undefined) { + if (typeof languageOptions.allowTrailingCommas !== "boolean") { + throw new Error( + "allowTrailingCommas must be a boolean if provided.", + ); + } + } + + // we know that allowTrailingCommas is a boolean here + + // only allowed in JSONC mode + if (this.#mode !== "jsonc") { + throw new Error( + "allowTrailingCommas option is only available in JSONC.", + ); + } } - /* eslint-enable class-methods-use-this, no-unused-vars -- Required to complete interface. */ /** * Parses the given file into an AST. * @param {File} file The virtual file to parse. + * @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing. * @returns {ParseResult} The result of parsing. */ - parse(file) { + parse(file, context) { // Note: BOM already removed const text = /** @type {string} */ (file.body); + const allowTrailingCommas = + context?.languageOptions?.allowTrailingCommas; /* * Check for parsing errors first. If there's a parsing error, nothing @@ -108,6 +128,7 @@ export class JSONLanguage { mode: this.#mode, ranges: true, tokens: true, + allowTrailingCommas, }); return { diff --git a/tests/languages/json-language.test.js b/tests/languages/json-language.test.js index 98d5bf1..2e43782 100644 --- a/tests/languages/json-language.test.js +++ b/tests/languages/json-language.test.js @@ -23,6 +23,48 @@ describe("JSONLanguage", () => { }); }); + describe("validateLanguageOptions()", () => { + it("should throw an error when allowTrailingCommas is not a boolean", () => { + const language = new JSONLanguage({ + mode: "jsonc", + allowTrailingCommas: "true", + }); + assert.throws(() => { + language.validateLanguageOptions({ + allowTrailingCommas: "true", + }); + }, /allowTrailingCommas/u); + }); + + it("should throw an error when allowTrailingCommas is a boolean in JSON mode", () => { + const language = new JSONLanguage({ mode: "json" }); + assert.throws(() => { + language.validateLanguageOptions({ allowTrailingCommas: true }); + }, /allowTrailingCommas/u); + }); + + it("should throw an error when allowTrailingCommas is a boolean in JSON5 mode", () => { + const language = new JSONLanguage({ mode: "json5" }); + assert.throws(() => { + language.validateLanguageOptions({ allowTrailingCommas: true }); + }, /allowTrailingCommas/u); + }); + + it("should not throw an error when allowTrailingCommas is a boolean in JSONC mode", () => { + const language = new JSONLanguage({ mode: "jsonc" }); + assert.doesNotThrow(() => { + language.validateLanguageOptions({ allowTrailingCommas: true }); + }); + }); + + it("should not throw an error when allowTrailingCommas is not provided", () => { + const language = new JSONLanguage({ mode: "jsonc" }); + assert.doesNotThrow(() => { + language.validateLanguageOptions({}); + }); + }); + }); + describe("parse()", () => { it("should not parse jsonc by default", () => { const language = new JSONLanguage({ mode: "json" }); @@ -38,6 +80,49 @@ describe("JSONLanguage", () => { ); }); + it("should not parse trailing commas by default in json mode", () => { + const language = new JSONLanguage({ mode: "json" }); + const result = language.parse({ + body: '{\n"a": 1,\n}', + path: "test.json", + }); + + assert.strictEqual(result.ok, false); + assert.strictEqual( + result.errors[0].message, + "Unexpected token RBrace found.", + ); + }); + + it("should not parse trailing commas by default in jsonc mode", () => { + const language = new JSONLanguage({ mode: "jsonc" }); + const result = language.parse({ + body: '{\n"a": 1,\n}', + path: "test.jsonc", + }); + + assert.strictEqual(result.ok, false); + assert.strictEqual( + result.errors[0].message, + "Unexpected token RBrace found.", + ); + }); + + it("should parse trailing commas when enabled in jsonc mode", () => { + const language = new JSONLanguage({ mode: "jsonc" }); + const result = language.parse( + { + body: '{\n"a": 1,\n}', + path: "test.jsonc", + }, + { languageOptions: { allowTrailingCommas: true } }, + ); + + assert.strictEqual(result.ok, true); + assert.strictEqual(result.ast.type, "Document"); + assert.strictEqual(result.ast.body.type, "Object"); + }); + it("should parse json by default", () => { const language = new JSONLanguage({ mode: "json" }); const result = language.parse({