Skip to content

Commit

Permalink
feat: Add allowTrailingCommas option for JSONC (#42)
Browse files Browse the repository at this point in the history
* feat: Add allowTrailingCommas option for JSONC

fixes #41

* Fix validation issue

* Update README.md

Co-authored-by: Milos Djermanovic <[email protected]>

* Update README.md

Co-authored-by: Milos Djermanovic <[email protected]>

---------

Co-authored-by: Milos Djermanovic <[email protected]>
  • Loading branch information
nzakas and mdjermanovic authored Oct 25, 2024
1 parent b7f039b commit c94953b
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 6 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,35 @@ 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`?
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 26 additions & 5 deletions src/languages/json-language.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import { visitorKeys } from "@humanwhocodes/momoa";
/** @typedef {import("@eslint/core").OkParseResult<DocumentNode>} OkParseResult */
/** @typedef {import("@eslint/core").ParseResult<DocumentNode>} ParseResult */
/** @typedef {import("@eslint/core").File} File */
/**
* @typedef {Object} JSONLanguageOptions
* @property {boolean} [allowTrailingCommas] Whether to allow trailing commas.
*/

//-----------------------------------------------------------------------------
// Exports
Expand Down Expand Up @@ -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
Expand All @@ -108,6 +128,7 @@ export class JSONLanguage {
mode: this.#mode,
ranges: true,
tokens: true,
allowTrailingCommas,
});

return {
Expand Down
92 changes: 92 additions & 0 deletions tests/languages/json-language.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,55 @@ 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({});
});
});

it("should not throw an error when allowTrailingCommas is not provided and other keys are present", () => {
const language = new JSONLanguage({ mode: "jsonc" });
assert.doesNotThrow(() => {
language.validateLanguageOptions({ foo: "bar" });
});
});
});

describe("parse()", () => {
it("should not parse jsonc by default", () => {
const language = new JSONLanguage({ mode: "json" });
Expand All @@ -38,6 +87,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({
Expand Down

0 comments on commit c94953b

Please sign in to comment.