Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add allowTrailingCommas option for JSONC #42

Merged
merged 4 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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