Skip to content

Commit

Permalink
feat: Add support for config comments (#27)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mdjermanovic authored Oct 2, 2024
1 parent 55bd09c commit 723ddf4
Show file tree
Hide file tree
Showing 6 changed files with 979 additions and 5 deletions.
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock = false
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`?
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
151 changes: 150 additions & 1 deletion src/languages/json-source-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
*/
Expand Down Expand Up @@ -72,6 +85,12 @@ export class JSONSourceCode extends TextSourceCodeBase {
*/
#parents = new WeakMap();

/**
* Collection of inline configuration comments.
* @type {Array<JSONToken>}
*/
#inlineConfigComments;

/**
* The AST of the source code.
* @type {DocumentNode}
Expand All @@ -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<JSONToken>} 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<FileProblem>,directives:Array<Directive>}} 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<FileProblem>,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.
Expand Down
Loading

0 comments on commit 723ddf4

Please sign in to comment.