From 723ddf4cc2593ce0469231a76f6dcf4dfb58c3e3 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 2 Oct 2024 07:14:24 +0200 Subject: [PATCH] feat: Add support for config comments (#27) * feat: Add support for config comments * add unit tests for `getInlineConfigNodes` and `applyInlineConfig` * add unit tests for `getDisableDirectives` * update docs * add integration tests with rule config comments * add integration tests with disable directives * fix type error --- .npmrc | 1 + README.md | 30 ++ package.json | 9 +- src/languages/json-source-code.js | 151 +++++- tests/languages/json-source-code.test.js | 222 +++++++++ tests/plugin/eslint.test.js | 571 +++++++++++++++++++++++ 6 files changed, 979 insertions(+), 5 deletions(-) create mode 100644 .npmrc create mode 100644 tests/plugin/eslint.test.js diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..c1ca392 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock = false diff --git a/README.md b/README.md index 2fa67dd..8795a1b 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,36 @@ export default [ - `no-duplicate-keys` - warns when there are two keys in an object with the same text. - `no-empty-keys` - warns when there is a key in an object that is an empty string or contains only whitespace (note: `package-lock.json` uses empty keys intentionally) +## Configuration Comments + +In JSONC and JSON5 files, you can also use [rule configurations comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments) and [disable directives](https://eslint.org/docs/latest/use/configure/rules#disabling-rules). + +```jsonc +/* eslint json/no-empty-keys: "error" */ + +{ + "foo": { + "": 1, // eslint-disable-line json/no-empty-keys -- We want an empty key here + }, + "bar": { + // eslint-disable-next-line json/no-empty-keys -- We want an empty key here too + "": 2, + }, + /* eslint-disable json/no-empty-keys -- Empty keys are allowed in the following code as well */ + "baz": [ + { + "": 3, + }, + { + "": 4, + }, + ], + /* eslint-enable json/no-empty-keys -- re-enable now */ +} +``` + +Both line and block comments can be used for all kinds of configuration comments. + ## Frequently Asked Questions ### How does this relate to `eslint-plugin-json` and `eslint-plugin-jsonc`? diff --git a/package.json b/package.json index bbc616d..d4fe3b4 100644 --- a/package.json +++ b/package.json @@ -61,14 +61,15 @@ ], "license": "Apache-2.0", "dependencies": { - "@eslint/plugin-kit": "^0.1.0", - "@humanwhocodes/momoa": "^3.2.0" + "@eslint/plugin-kit": "^0.2.0", + "@humanwhocodes/momoa": "^3.2.1" }, "devDependencies": { - "@eslint/core": "^0.3.0", + "@eslint/core": "^0.6.0", "@types/eslint": "^8.56.10", "c8": "^9.1.0", - "eslint": "^9.6.0", + "dedent": "^1.5.3", + "eslint": "^9.11.1", "eslint-config-eslint": "^11.0.0", "lint-staged": "^15.2.7", "mocha": "^10.4.0", diff --git a/src/languages/json-source-code.js b/src/languages/json-source-code.js index 5503698..acec0ec 100644 --- a/src/languages/json-source-code.js +++ b/src/languages/json-source-code.js @@ -8,7 +8,12 @@ //----------------------------------------------------------------------------- import { iterator } from "@humanwhocodes/momoa"; -import { VisitNodeStep, TextSourceCodeBase } from "@eslint/plugin-kit"; +import { + VisitNodeStep, + TextSourceCodeBase, + ConfigCommentParser, + Directive, +} from "@eslint/plugin-kit"; //----------------------------------------------------------------------------- // Types @@ -23,11 +28,19 @@ import { VisitNodeStep, TextSourceCodeBase } from "@eslint/plugin-kit"; /** @typedef {import("@eslint/core").TraversalStep} TraversalStep */ /** @typedef {import("@eslint/core").TextSourceCode} TextSourceCode */ /** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */ +/** @typedef {import("@eslint/core").FileProblem} FileProblem */ +/** @typedef {import("@eslint/core").DirectiveType} DirectiveType */ +/** @typedef {import("@eslint/core").RulesConfig} RulesConfig */ //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- +const commentParser = new ConfigCommentParser(); + +const INLINE_CONFIG = + /^\s*(?:eslint(?:-enable|-disable(?:(?:-next)?-line)?)?)(?:\s|$)/u; + /** * A class to represent a step in the traversal process. */ @@ -72,6 +85,12 @@ export class JSONSourceCode extends TextSourceCodeBase { */ #parents = new WeakMap(); + /** + * Collection of inline configuration comments. + * @type {Array} + */ + #inlineConfigComments; + /** * The AST of the source code. * @type {DocumentNode} @@ -98,6 +117,136 @@ export class JSONSourceCode extends TextSourceCodeBase { : []; } + /** + * Returns the value of the given comment. + * @param {JSONToken} comment The comment to get the value of. + * @returns {string} The value of the comment. + * @throws {Error} When an unexpected comment type is passed. + */ + #getCommentValue(comment) { + if (comment.type === "LineComment") { + return this.getText(comment).slice(2); // strip leading `//` + } + + if (comment.type === "BlockComment") { + return this.getText(comment).slice(2, -2); // strip leading `/*` and trailing `*/` + } + + throw new Error(`Unexpected comment type '${comment.type}'`); + } + + /** + * Returns an array of all inline configuration nodes found in the + * source code. + * @returns {Array} An array of all inline configuration nodes. + */ + getInlineConfigNodes() { + if (!this.#inlineConfigComments) { + this.#inlineConfigComments = this.comments.filter(comment => + INLINE_CONFIG.test(this.#getCommentValue(comment)), + ); + } + + return this.#inlineConfigComments; + } + + /** + * Returns directives that enable or disable rules along with any problems + * encountered while parsing the directives. + * @returns {{problems:Array,directives:Array}} Information + * that ESLint needs to further process the directives. + */ + getDisableDirectives() { + const problems = []; + const directives = []; + + this.getInlineConfigNodes().forEach(comment => { + const { label, value, justification } = + commentParser.parseDirective(this.#getCommentValue(comment)); + + // `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply + if ( + label === "eslint-disable-line" && + comment.loc.start.line !== comment.loc.end.line + ) { + const message = `${label} comment should not span multiple lines.`; + + problems.push({ + ruleId: null, + message, + loc: comment.loc, + }); + return; + } + + switch (label) { + case "eslint-disable": + case "eslint-enable": + case "eslint-disable-next-line": + case "eslint-disable-line": { + const directiveType = label.slice("eslint-".length); + + directives.push( + new Directive({ + type: /** @type {DirectiveType} */ (directiveType), + node: comment, + value, + justification, + }), + ); + } + + // no default + } + }); + + return { problems, directives }; + } + + /** + * Returns inline rule configurations along with any problems + * encountered while parsing the configurations. + * @returns {{problems:Array,configs:Array<{config:{rules:RulesConfig},loc:SourceLocation}>}} Information + * that ESLint needs to further process the rule configurations. + */ + applyInlineConfig() { + const problems = []; + const configs = []; + + this.getInlineConfigNodes().forEach(comment => { + const { label, value } = commentParser.parseDirective( + this.#getCommentValue(comment), + ); + + if (label === "eslint") { + const parseResult = commentParser.parseJSONLikeConfig(value); + + if (parseResult.ok) { + configs.push({ + config: { + rules: parseResult.config, + }, + loc: comment.loc, + }); + } else { + problems.push({ + ruleId: null, + message: + /** @type {{ok: false, error: { message: string }}} */ ( + parseResult + ).error.message, + loc: comment.loc, + }); + } + } + }); + + return { + configs, + problems, + }; + } + /** * Returns the parent of the given node. * @param {JSONNode} node The node to get the parent of. diff --git a/tests/languages/json-source-code.test.js b/tests/languages/json-source-code.test.js index ea87d1b..007e78f 100644 --- a/tests/languages/json-source-code.test.js +++ b/tests/languages/json-source-code.test.js @@ -10,6 +10,7 @@ import { JSONSourceCode } from "../../src/languages/json-source-code.js"; import { JSONLanguage } from "../../src/languages/json-language.js"; import assert from "node:assert"; +import dedent from "dedent"; //----------------------------------------------------------------------------- // Tests @@ -275,4 +276,225 @@ describe("JSONSourceCode", () => { ]); }); }); + + describe("config comments", () => { + const text = dedent` + { + /* rule config comments */ + //eslint json/no-duplicate-keys: error + // eslint json/no-duplicate-keys: [1] -- comment + /*eslint json/no-duplicate-keys: [2, { allow: ["foo"] }]*/ + /* + eslint + json/no-empty-keys: warn, + json/no-duplicate-keys: [2, "strict"] + -- + comment + */ + + // invalid rule config comments + // eslint json/no-duplicate-keys: [error + /*eslint json/no-duplicate-keys: [1, { allow: ["foo"] ]*/ + + // not rule config comments + //eslintjson/no-duplicate-keys: error + /*-eslint json/no-duplicate-keys: error*/ + + /* disable directives */ + //eslint-disable + /* eslint-disable json/no-duplicate-keys -- we want duplicate keys */ + // eslint-enable json/no-duplicate-keys, json/no-empty-keys + /*eslint-enable*/ + "": 5, // eslint-disable-line json/no-empty-keys + /*eslint-disable-line json/no-empty-keys -- special case*/ "": 6, + //eslint-disable-next-line + "": 7, + /* eslint-disable-next-line json/no-duplicate-keys, json/no-empty-keys + -- another special case + */ + "": 8 + + // invalid disable directives + /* eslint-disable-line json/no-duplicate-keys + */ + + // not disable directives + ///eslint-disable + /*eslint-disable-*/ + } + `; + + ["jsonc", "json5"].forEach(languageMode => { + describe(`with ${languageMode} language`, () => { + let sourceCode = null; + + beforeEach(() => { + const file = { body: text, path: `test.${languageMode}` }; + const language = new JSONLanguage({ mode: languageMode }); + const parseResult = language.parse(file); + sourceCode = new JSONSourceCode({ + text: file.body, + ast: parseResult.ast, + }); + }); + + afterEach(() => { + sourceCode = null; + }); + + describe("getInlineConfigNodes()", () => { + it("should return inline config comments", () => { + const allComments = sourceCode.comments; + const configComments = + sourceCode.getInlineConfigNodes(); + + const configCommentsIndexes = [ + 1, 2, 3, 4, 6, 7, 12, 13, 14, 15, 16, 17, 18, 19, + 21, + ]; + + assert.strictEqual( + configComments.length, + configCommentsIndexes.length, + ); + + configComments.forEach((configComment, i) => { + assert.strictEqual( + configComment, + allComments[configCommentsIndexes[i]], + ); + }); + }); + }); + + describe("applyInlineConfig()", () => { + it("should return rule configs and problems", () => { + const allComments = sourceCode.comments; + const { configs, problems } = + sourceCode.applyInlineConfig(); + + assert.deepStrictEqual(configs, [ + { + config: { + rules: { + "json/no-duplicate-keys": "error", + }, + }, + loc: allComments[1].loc, + }, + { + config: { + rules: { + "json/no-duplicate-keys": [1], + }, + }, + loc: allComments[2].loc, + }, + { + config: { + rules: { + "json/no-duplicate-keys": [ + 2, + { allow: ["foo"] }, + ], + }, + }, + loc: allComments[3].loc, + }, + { + config: { + rules: { + "json/no-empty-keys": "warn", + "json/no-duplicate-keys": [2, "strict"], + }, + }, + loc: allComments[4].loc, + }, + ]); + + assert.strictEqual(problems.length, 2); + assert.strictEqual(problems[0].ruleId, null); + assert.match(problems[0].message, /Failed to parse/u); + assert.strictEqual(problems[0].loc, allComments[6].loc); + assert.strictEqual(problems[1].ruleId, null); + assert.match(problems[1].message, /Failed to parse/u); + assert.strictEqual(problems[1].loc, allComments[7].loc); + }); + }); + + describe("getDisableDirectives()", () => { + it("should return disable directives and problems", () => { + const allComments = sourceCode.comments; + const { directives, problems } = + sourceCode.getDisableDirectives(); + + assert.deepStrictEqual( + directives.map(obj => ({ ...obj })), + [ + { + type: "disable", + value: "", + justification: "", + node: allComments[12], + }, + { + type: "disable", + value: "json/no-duplicate-keys", + justification: "we want duplicate keys", + node: allComments[13], + }, + { + type: "enable", + value: "json/no-duplicate-keys, json/no-empty-keys", + justification: "", + node: allComments[14], + }, + { + type: "enable", + value: "", + justification: "", + node: allComments[15], + }, + { + type: "disable-line", + value: "json/no-empty-keys", + justification: "", + node: allComments[16], + }, + { + type: "disable-line", + value: "json/no-empty-keys", + justification: "special case", + node: allComments[17], + }, + { + type: "disable-next-line", + value: "", + justification: "", + node: allComments[18], + }, + { + type: "disable-next-line", + value: "json/no-duplicate-keys, json/no-empty-keys", + justification: "another special case", + node: allComments[19], + }, + ], + ); + + assert.strictEqual(problems.length, 1); + assert.strictEqual(problems[0].ruleId, null); + assert.strictEqual( + problems[0].message, + "eslint-disable-line comment should not span multiple lines.", + ); + assert.strictEqual( + problems[0].loc, + allComments[21].loc, + ); + }); + }); + }); + }); + }); }); diff --git a/tests/plugin/eslint.test.js b/tests/plugin/eslint.test.js new file mode 100644 index 0000000..3ad4bd7 --- /dev/null +++ b/tests/plugin/eslint.test.js @@ -0,0 +1,571 @@ +/** + * @fileoverview Integration tests with ESLint. + * @author Milos Djermanovic + */ + +//------------------------------------------------------------------------------ +// Imports +//------------------------------------------------------------------------------ + +import json from "../../src/index.js"; +import ESLintAPI from "eslint"; +const { ESLint } = ESLintAPI; + +import assert from "node:assert"; +import dedent from "dedent"; + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("when the plugin is used with ESLint", () => { + describe("config comments", () => { + ["jsonc", "json5"].forEach(language => { + describe(`with ${language} language`, () => { + const config = { + files: [`**/*.${language}`], + plugins: { + json, + }, + language: `json/${language}`, + rules: { + "json/no-empty-keys": "error", + }, + }; + const filePath = `test.${language}`; + + let eslint = null; + + beforeEach(() => { + eslint = new ESLint({ + overrideConfigFile: true, + overrideConfig: config, + }); + }); + + afterEach(() => { + eslint = null; + }); + + describe("rule configuration comments", () => { + it("should be able to turn off rule", async () => { + const [result] = await eslint.lintText( + dedent` + /* eslint json/no-empty-keys: off */ + { + "": 42 + } + `, + { + filePath, + }, + ); + + assert.strictEqual(result.messages.length, 0); + assert.strictEqual(result.suppressedMessages.length, 0); + }); + + it("should be able to enable rule", async () => { + const [result] = await eslint.lintText( + dedent` + /* eslint json/no-duplicate-keys: error */ + { + "foo": 42, + "foo": 43 + } + `, + { + filePath, + }, + ); + + assert.strictEqual(result.messages.length, 1); + + assert.strictEqual( + result.messages[0].ruleId, + "json/no-duplicate-keys", + ); + assert.strictEqual( + result.messages[0].messageId, + "duplicateKey", + ); + assert.strictEqual(result.messages[0].severity, 2); + assert.strictEqual(result.messages[0].line, 4); + + assert.strictEqual(result.suppressedMessages.length, 0); + }); + + it("should be able to enable/reconfigure multiple rules", async () => { + const [result] = await eslint.lintText( + dedent` + /* eslint json/no-duplicate-keys: [2], json/no-empty-keys: [1] */ + { + "": 42, + "foo": 43, + "foo": 44 + } + `, + { + filePath, + }, + ); + + assert.strictEqual(result.messages.length, 2); + + assert.strictEqual( + result.messages[0].ruleId, + "json/no-empty-keys", + ); + assert.strictEqual( + result.messages[0].messageId, + "emptyKey", + ); + assert.strictEqual(result.messages[0].severity, 1); + assert.strictEqual(result.messages[0].line, 3); + + assert.strictEqual( + result.messages[1].ruleId, + "json/no-duplicate-keys", + ); + assert.strictEqual( + result.messages[1].messageId, + "duplicateKey", + ); + assert.strictEqual(result.messages[1].severity, 2); + assert.strictEqual(result.messages[1].line, 5); + + assert.strictEqual(result.suppressedMessages.length, 0); + }); + + it("should be reported when invalid", async () => { + const [result] = await eslint.lintText( + dedent` + // foo + /* eslint json/no-duplicate-keys: [2 */ + { + } + `, + { + filePath, + }, + ); + + assert.strictEqual(result.messages.length, 1); + + assert.strictEqual(result.messages[0].ruleId, null); + assert.match( + result.messages[0].message, + /Failed to parse/u, + ); + assert.strictEqual(result.messages[0].severity, 2); + assert.strictEqual(result.messages[0].line, 2); + assert.strictEqual(result.messages[0].column, 1); + assert.strictEqual(result.messages[0].endLine, 2); + assert.strictEqual(result.messages[0].endColumn, 40); + + assert.strictEqual(result.suppressedMessages.length, 0); + }); + }); + + describe("disable directives", () => { + it("eslint-disable should suppress rule errors, eslint-enable should re-enable rule errors", async () => { + const [result] = await eslint.lintText( + dedent` + [ + /* eslint-disable json/no-empty-keys -- allowed in first two elements */ + { + "": 42 + }, + { + "": 43 + }, + /* eslint-enable json/no-empty-keys */ + { + "": 44 + } + ] + `, + { + filePath, + }, + ); + + assert.strictEqual(result.messages.length, 1); + + assert.strictEqual( + result.messages[0].ruleId, + "json/no-empty-keys", + ); + assert.strictEqual( + result.messages[0].messageId, + "emptyKey", + ); + assert.strictEqual(result.messages[0].severity, 2); + assert.strictEqual(result.messages[0].line, 11); + + assert.strictEqual(result.suppressedMessages.length, 2); + + assert.strictEqual( + result.suppressedMessages[0].ruleId, + "json/no-empty-keys", + ); + assert.strictEqual( + result.suppressedMessages[0].messageId, + "emptyKey", + ); + assert.strictEqual( + result.suppressedMessages[0].severity, + 2, + ); + assert.strictEqual( + result.suppressedMessages[0].line, + 4, + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions.length, + 1, + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions[0].kind, + "directive", + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions[0] + .justification, + "allowed in first two elements", + ); + + assert.strictEqual( + result.suppressedMessages[1].ruleId, + "json/no-empty-keys", + ); + assert.strictEqual( + result.suppressedMessages[1].messageId, + "emptyKey", + ); + assert.strictEqual( + result.suppressedMessages[1].severity, + 2, + ); + assert.strictEqual( + result.suppressedMessages[1].line, + 7, + ); + assert.strictEqual( + result.suppressedMessages[1].suppressions.length, + 1, + ); + assert.strictEqual( + result.suppressedMessages[1].suppressions[0].kind, + "directive", + ); + assert.strictEqual( + result.suppressedMessages[1].suppressions[0] + .justification, + "allowed in first two elements", + ); + }); + + it("eslint-disable should suppress errors from multiple rules", async () => { + const [result] = await eslint.lintText( + dedent` + /* eslint json/no-duplicate-keys: warn */ + /* eslint-disable json/no-empty-keys, json/no-duplicate-keys */ + { + "": 42, + "foo": 5, + "foo": 6 + } + `, + { + filePath, + }, + ); + + assert.strictEqual(result.suppressedMessages.length, 2); + + assert.strictEqual( + result.suppressedMessages[0].ruleId, + "json/no-empty-keys", + ); + assert.strictEqual( + result.suppressedMessages[0].messageId, + "emptyKey", + ); + assert.strictEqual( + result.suppressedMessages[0].severity, + 2, + ); + assert.strictEqual( + result.suppressedMessages[0].line, + 4, + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions.length, + 1, + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions[0].kind, + "directive", + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions[0] + .justification, + "", + ); + + assert.strictEqual( + result.suppressedMessages[1].ruleId, + "json/no-duplicate-keys", + ); + assert.strictEqual( + result.suppressedMessages[1].messageId, + "duplicateKey", + ); + assert.strictEqual( + result.suppressedMessages[1].severity, + 1, + ); + assert.strictEqual( + result.suppressedMessages[1].line, + 6, + ); + assert.strictEqual( + result.suppressedMessages[1].suppressions.length, + 1, + ); + assert.strictEqual( + result.suppressedMessages[1].suppressions[0].kind, + "directive", + ); + assert.strictEqual( + result.suppressedMessages[1].suppressions[0] + .justification, + "", + ); + }); + + it("eslint-disable-line should suppress rule errors on the same line", async () => { + const [result] = await eslint.lintText( + dedent` + { + "": 42, // eslint-disable-line json/no-empty-keys -- allowed here + "": 43 + } + `, + { + filePath, + }, + ); + + assert.strictEqual(result.messages.length, 1); + + assert.strictEqual( + result.messages[0].ruleId, + "json/no-empty-keys", + ); + assert.strictEqual( + result.messages[0].messageId, + "emptyKey", + ); + assert.strictEqual(result.messages[0].severity, 2); + assert.strictEqual(result.messages[0].line, 3); + + assert.strictEqual(result.suppressedMessages.length, 1); + + assert.strictEqual( + result.suppressedMessages[0].ruleId, + "json/no-empty-keys", + ); + assert.strictEqual( + result.suppressedMessages[0].messageId, + "emptyKey", + ); + assert.strictEqual( + result.suppressedMessages[0].severity, + 2, + ); + assert.strictEqual( + result.suppressedMessages[0].line, + 2, + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions.length, + 1, + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions[0].kind, + "directive", + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions[0] + .justification, + "allowed here", + ); + }); + + it("eslint-disable-next-line should suppress rule errors on the next line", async () => { + const [result] = await eslint.lintText( + dedent` + { + "": 42, // eslint-disable-next-line json/no-empty-keys -- allowed here + "": 43 + } + `, + { + filePath, + }, + ); + + assert.strictEqual(result.messages.length, 1); + + assert.strictEqual( + result.messages[0].ruleId, + "json/no-empty-keys", + ); + assert.strictEqual( + result.messages[0].messageId, + "emptyKey", + ); + assert.strictEqual(result.messages[0].severity, 2); + assert.strictEqual(result.messages[0].line, 2); + + assert.strictEqual(result.suppressedMessages.length, 1); + + assert.strictEqual( + result.suppressedMessages[0].ruleId, + "json/no-empty-keys", + ); + assert.strictEqual( + result.suppressedMessages[0].messageId, + "emptyKey", + ); + assert.strictEqual( + result.suppressedMessages[0].severity, + 2, + ); + assert.strictEqual( + result.suppressedMessages[0].line, + 3, + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions.length, + 1, + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions[0].kind, + "directive", + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions[0] + .justification, + "allowed here", + ); + }); + + it("multiline eslint-disable-next-line should suppress rule errors on the next line", async () => { + const [result] = await eslint.lintText( + dedent` + { + /* eslint-disable-next-line + json/no-empty-keys + */ + "": 42 + } + `, + { + filePath, + }, + ); + + assert.strictEqual(result.messages.length, 0); + + assert.strictEqual(result.suppressedMessages.length, 1); + + assert.strictEqual( + result.suppressedMessages[0].ruleId, + "json/no-empty-keys", + ); + assert.strictEqual( + result.suppressedMessages[0].messageId, + "emptyKey", + ); + assert.strictEqual( + result.suppressedMessages[0].severity, + 2, + ); + assert.strictEqual( + result.suppressedMessages[0].line, + 5, + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions.length, + 1, + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions[0].kind, + "directive", + ); + assert.strictEqual( + result.suppressedMessages[0].suppressions[0] + .justification, + "", + ); + }); + + it("multiline eslint-disable-line should be reported as error and not suppress any rule errors", async () => { + const [result] = await eslint.lintText( + dedent` + { + "": 42, /* eslint-disable-line + json/no-empty-keys + */ "": 43 + } + `, + { + filePath, + }, + ); + + assert.strictEqual(result.messages.length, 3); + + assert.strictEqual( + result.messages[0].ruleId, + "json/no-empty-keys", + ); + assert.strictEqual( + result.messages[0].messageId, + "emptyKey", + ); + assert.strictEqual(result.messages[0].severity, 2); + assert.strictEqual(result.messages[0].line, 2); + + assert.strictEqual(result.messages[1].ruleId, null); + assert.strictEqual( + result.messages[1].message, + "eslint-disable-line comment should not span multiple lines.", + ); + assert.strictEqual(result.messages[1].severity, 2); + assert.strictEqual(result.messages[1].line, 2); + assert.strictEqual(result.messages[1].column, 10); + assert.strictEqual(result.messages[1].endLine, 4); + assert.strictEqual(result.messages[1].endColumn, 5); + + assert.strictEqual( + result.messages[2].ruleId, + "json/no-empty-keys", + ); + assert.strictEqual( + result.messages[2].messageId, + "emptyKey", + ); + assert.strictEqual(result.messages[2].severity, 2); + assert.strictEqual(result.messages[2].line, 4); + + assert.strictEqual(result.suppressedMessages.length, 0); + }); + }); + }); + }); + }); +});