@eslint/json 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -155,6 +155,36 @@ export default [
155
155
  - `no-duplicate-keys` - warns when there are two keys in an object with the same text.
156
156
  - `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)
157
157
 
158
+ ## Configuration Comments
159
+
160
+ 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).
161
+
162
+ ```jsonc
163
+ /* eslint json/no-empty-keys: "error" */
164
+
165
+ {
166
+ "foo": {
167
+ "": 1, // eslint-disable-line json/no-empty-keys -- We want an empty key here
168
+ },
169
+ "bar": {
170
+ // eslint-disable-next-line json/no-empty-keys -- We want an empty key here too
171
+ "": 2,
172
+ },
173
+ /* eslint-disable json/no-empty-keys -- Empty keys are allowed in the following code as well */
174
+ "baz": [
175
+ {
176
+ "": 3,
177
+ },
178
+ {
179
+ "": 4,
180
+ },
181
+ ],
182
+ /* eslint-enable json/no-empty-keys -- re-enable now */
183
+ }
184
+ ```
185
+
186
+ Both line and block comments can be used for all kinds of configuration comments.
187
+
158
188
  ## Frequently Asked Questions
159
189
 
160
190
  ### How does this relate to `eslint-plugin-json` and `eslint-plugin-jsonc`?
@@ -24,11 +24,19 @@ var pluginKit = require('@eslint/plugin-kit');
24
24
  /** @typedef {import("@eslint/core").TraversalStep} TraversalStep */
25
25
  /** @typedef {import("@eslint/core").TextSourceCode} TextSourceCode */
26
26
  /** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */
27
+ /** @typedef {import("@eslint/core").FileProblem} FileProblem */
28
+ /** @typedef {import("@eslint/core").DirectiveType} DirectiveType */
29
+ /** @typedef {import("@eslint/core").RulesConfig} RulesConfig */
27
30
 
28
31
  //-----------------------------------------------------------------------------
29
32
  // Helpers
30
33
  //-----------------------------------------------------------------------------
31
34
 
35
+ const commentParser = new pluginKit.ConfigCommentParser();
36
+
37
+ const INLINE_CONFIG =
38
+ /^\s*(?:eslint(?:-enable|-disable(?:(?:-next)?-line)?)?)(?:\s|$)/u;
39
+
32
40
  /**
33
41
  * A class to represent a step in the traversal process.
34
42
  */
@@ -73,6 +81,12 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
73
81
  */
74
82
  #parents = new WeakMap();
75
83
 
84
+ /**
85
+ * Collection of inline configuration comments.
86
+ * @type {Array<JSONToken>}
87
+ */
88
+ #inlineConfigComments;
89
+
76
90
  /**
77
91
  * The AST of the source code.
78
92
  * @type {DocumentNode}
@@ -99,6 +113,136 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
99
113
  : [];
100
114
  }
101
115
 
116
+ /**
117
+ * Returns the value of the given comment.
118
+ * @param {JSONToken} comment The comment to get the value of.
119
+ * @returns {string} The value of the comment.
120
+ * @throws {Error} When an unexpected comment type is passed.
121
+ */
122
+ #getCommentValue(comment) {
123
+ if (comment.type === "LineComment") {
124
+ return this.getText(comment).slice(2); // strip leading `//`
125
+ }
126
+
127
+ if (comment.type === "BlockComment") {
128
+ return this.getText(comment).slice(2, -2); // strip leading `/*` and trailing `*/`
129
+ }
130
+
131
+ throw new Error(`Unexpected comment type '${comment.type}'`);
132
+ }
133
+
134
+ /**
135
+ * Returns an array of all inline configuration nodes found in the
136
+ * source code.
137
+ * @returns {Array<JSONToken>} An array of all inline configuration nodes.
138
+ */
139
+ getInlineConfigNodes() {
140
+ if (!this.#inlineConfigComments) {
141
+ this.#inlineConfigComments = this.comments.filter(comment =>
142
+ INLINE_CONFIG.test(this.#getCommentValue(comment)),
143
+ );
144
+ }
145
+
146
+ return this.#inlineConfigComments;
147
+ }
148
+
149
+ /**
150
+ * Returns directives that enable or disable rules along with any problems
151
+ * encountered while parsing the directives.
152
+ * @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
153
+ * that ESLint needs to further process the directives.
154
+ */
155
+ getDisableDirectives() {
156
+ const problems = [];
157
+ const directives = [];
158
+
159
+ this.getInlineConfigNodes().forEach(comment => {
160
+ const { label, value, justification } =
161
+ commentParser.parseDirective(this.#getCommentValue(comment));
162
+
163
+ // `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
164
+ if (
165
+ label === "eslint-disable-line" &&
166
+ comment.loc.start.line !== comment.loc.end.line
167
+ ) {
168
+ const message = `${label} comment should not span multiple lines.`;
169
+
170
+ problems.push({
171
+ ruleId: null,
172
+ message,
173
+ loc: comment.loc,
174
+ });
175
+ return;
176
+ }
177
+
178
+ switch (label) {
179
+ case "eslint-disable":
180
+ case "eslint-enable":
181
+ case "eslint-disable-next-line":
182
+ case "eslint-disable-line": {
183
+ const directiveType = label.slice("eslint-".length);
184
+
185
+ directives.push(
186
+ new pluginKit.Directive({
187
+ type: /** @type {DirectiveType} */ (directiveType),
188
+ node: comment,
189
+ value,
190
+ justification,
191
+ }),
192
+ );
193
+ }
194
+
195
+ // no default
196
+ }
197
+ });
198
+
199
+ return { problems, directives };
200
+ }
201
+
202
+ /**
203
+ * Returns inline rule configurations along with any problems
204
+ * encountered while parsing the configurations.
205
+ * @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:SourceLocation}>}} Information
206
+ * that ESLint needs to further process the rule configurations.
207
+ */
208
+ applyInlineConfig() {
209
+ const problems = [];
210
+ const configs = [];
211
+
212
+ this.getInlineConfigNodes().forEach(comment => {
213
+ const { label, value } = commentParser.parseDirective(
214
+ this.#getCommentValue(comment),
215
+ );
216
+
217
+ if (label === "eslint") {
218
+ const parseResult = commentParser.parseJSONLikeConfig(value);
219
+
220
+ if (parseResult.ok) {
221
+ configs.push({
222
+ config: {
223
+ rules: parseResult.config,
224
+ },
225
+ loc: comment.loc,
226
+ });
227
+ } else {
228
+ problems.push({
229
+ ruleId: null,
230
+ message:
231
+ /** @type {{ok: false, error: { message: string }}} */ (
232
+ parseResult
233
+ ).error.message,
234
+ loc: comment.loc,
235
+ });
236
+ }
237
+ }
238
+ });
239
+
240
+ return {
241
+ configs,
242
+ problems,
243
+ };
244
+ }
245
+
102
246
  /**
103
247
  * Returns the parent of the given node.
104
248
  * @param {JSONNode} node The node to get the parent of.
@@ -386,7 +530,7 @@ var noEmptyKeys = {
386
530
  const plugin = {
387
531
  meta: {
388
532
  name: "@eslint/json",
389
- version: "0.4.1", // x-release-please-version
533
+ version: "0.5.0", // x-release-please-version
390
534
  },
391
535
  languages: {
392
536
  json: new JSONLanguage({ mode: "json" }),
@@ -7,6 +7,9 @@ export type File = import("@eslint/core").File;
7
7
  export type TraversalStep = import("@eslint/core").TraversalStep;
8
8
  export type TextSourceCode = import("@eslint/core").TextSourceCode;
9
9
  export type VisitTraversalStep = import("@eslint/core").VisitTraversalStep;
10
+ export type FileProblem = import("@eslint/core").FileProblem;
11
+ export type DirectiveType = import("@eslint/core").DirectiveType;
12
+ export type RulesConfig = import("@eslint/core").RulesConfig;
10
13
  export type Language = import("@eslint/core").Language;
11
14
  export type OkParseResult = import("@eslint/core").OkParseResult<DocumentNode>;
12
15
  export type ParseResult = import("@eslint/core").ParseResult<DocumentNode>;
@@ -101,6 +104,37 @@ export class JSONSourceCode extends TextSourceCodeBase {
101
104
  * @type {Array<JSONToken>|undefined}
102
105
  */
103
106
  comments: Array<JSONToken> | undefined;
107
+ /**
108
+ * Returns an array of all inline configuration nodes found in the
109
+ * source code.
110
+ * @returns {Array<JSONToken>} An array of all inline configuration nodes.
111
+ */
112
+ getInlineConfigNodes(): Array<JSONToken>;
113
+ /**
114
+ * Returns directives that enable or disable rules along with any problems
115
+ * encountered while parsing the directives.
116
+ * @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
117
+ * that ESLint needs to further process the directives.
118
+ */
119
+ getDisableDirectives(): {
120
+ problems: Array<FileProblem>;
121
+ directives: Array<Directive>;
122
+ };
123
+ /**
124
+ * Returns inline rule configurations along with any problems
125
+ * encountered while parsing the configurations.
126
+ * @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:SourceLocation}>}} Information
127
+ * that ESLint needs to further process the rule configurations.
128
+ */
129
+ applyInlineConfig(): {
130
+ problems: Array<FileProblem>;
131
+ configs: Array<{
132
+ config: {
133
+ rules: RulesConfig;
134
+ };
135
+ loc: SourceLocation;
136
+ }>;
137
+ };
104
138
  /**
105
139
  * Returns the parent of the given node.
106
140
  * @param {JSONNode} node The node to get the parent of.
@@ -159,19 +193,7 @@ declare namespace plugin {
159
193
  let configs: {};
160
194
  }
161
195
  import { TextSourceCodeBase } from '@eslint/plugin-kit';
162
- /**
163
- * @fileoverview The JSONSourceCode class.
164
- * @author Nicholas C. Zakas
165
- */
166
- /** @typedef {import("@humanwhocodes/momoa").DocumentNode} DocumentNode */
167
- /** @typedef {import("@humanwhocodes/momoa").Node} JSONNode */
168
- /** @typedef {import("@humanwhocodes/momoa").Token} JSONToken */
169
- /** @typedef {import("@eslint/core").SourceRange} SourceRange */
170
- /** @typedef {import("@eslint/core").SourceLocation} SourceLocation */
171
- /** @typedef {import("@eslint/core").File} File */
172
- /** @typedef {import("@eslint/core").TraversalStep} TraversalStep */
173
- /** @typedef {import("@eslint/core").TextSourceCode} TextSourceCode */
174
- /** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */
196
+ import { Directive } from '@eslint/plugin-kit';
175
197
  /**
176
198
  * A class to represent a step in the traversal process.
177
199
  */
@@ -7,6 +7,9 @@ export type File = import("@eslint/core").File;
7
7
  export type TraversalStep = import("@eslint/core").TraversalStep;
8
8
  export type TextSourceCode = import("@eslint/core").TextSourceCode;
9
9
  export type VisitTraversalStep = import("@eslint/core").VisitTraversalStep;
10
+ export type FileProblem = import("@eslint/core").FileProblem;
11
+ export type DirectiveType = import("@eslint/core").DirectiveType;
12
+ export type RulesConfig = import("@eslint/core").RulesConfig;
10
13
  export type Language = import("@eslint/core").Language;
11
14
  export type OkParseResult = import("@eslint/core").OkParseResult<DocumentNode>;
12
15
  export type ParseResult = import("@eslint/core").ParseResult<DocumentNode>;
@@ -101,6 +104,37 @@ export class JSONSourceCode extends TextSourceCodeBase {
101
104
  * @type {Array<JSONToken>|undefined}
102
105
  */
103
106
  comments: Array<JSONToken> | undefined;
107
+ /**
108
+ * Returns an array of all inline configuration nodes found in the
109
+ * source code.
110
+ * @returns {Array<JSONToken>} An array of all inline configuration nodes.
111
+ */
112
+ getInlineConfigNodes(): Array<JSONToken>;
113
+ /**
114
+ * Returns directives that enable or disable rules along with any problems
115
+ * encountered while parsing the directives.
116
+ * @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
117
+ * that ESLint needs to further process the directives.
118
+ */
119
+ getDisableDirectives(): {
120
+ problems: Array<FileProblem>;
121
+ directives: Array<Directive>;
122
+ };
123
+ /**
124
+ * Returns inline rule configurations along with any problems
125
+ * encountered while parsing the configurations.
126
+ * @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:SourceLocation}>}} Information
127
+ * that ESLint needs to further process the rule configurations.
128
+ */
129
+ applyInlineConfig(): {
130
+ problems: Array<FileProblem>;
131
+ configs: Array<{
132
+ config: {
133
+ rules: RulesConfig;
134
+ };
135
+ loc: SourceLocation;
136
+ }>;
137
+ };
104
138
  /**
105
139
  * Returns the parent of the given node.
106
140
  * @param {JSONNode} node The node to get the parent of.
@@ -159,19 +193,7 @@ declare namespace plugin {
159
193
  let configs: {};
160
194
  }
161
195
  import { TextSourceCodeBase } from '@eslint/plugin-kit';
162
- /**
163
- * @fileoverview The JSONSourceCode class.
164
- * @author Nicholas C. Zakas
165
- */
166
- /** @typedef {import("@humanwhocodes/momoa").DocumentNode} DocumentNode */
167
- /** @typedef {import("@humanwhocodes/momoa").Node} JSONNode */
168
- /** @typedef {import("@humanwhocodes/momoa").Token} JSONToken */
169
- /** @typedef {import("@eslint/core").SourceRange} SourceRange */
170
- /** @typedef {import("@eslint/core").SourceLocation} SourceLocation */
171
- /** @typedef {import("@eslint/core").File} File */
172
- /** @typedef {import("@eslint/core").TraversalStep} TraversalStep */
173
- /** @typedef {import("@eslint/core").TextSourceCode} TextSourceCode */
174
- /** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */
196
+ import { Directive } from '@eslint/plugin-kit';
175
197
  /**
176
198
  * A class to represent a step in the traversal process.
177
199
  */
package/dist/esm/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @ts-self-types="./index.d.ts"
2
2
  import { iterator, visitorKeys, parse } from '@humanwhocodes/momoa';
3
- import { TextSourceCodeBase, VisitNodeStep } from '@eslint/plugin-kit';
3
+ import { ConfigCommentParser, TextSourceCodeBase, Directive, VisitNodeStep } from '@eslint/plugin-kit';
4
4
 
5
5
  /**
6
6
  * @fileoverview The JSONSourceCode class.
@@ -21,11 +21,19 @@ import { TextSourceCodeBase, VisitNodeStep } from '@eslint/plugin-kit';
21
21
  /** @typedef {import("@eslint/core").TraversalStep} TraversalStep */
22
22
  /** @typedef {import("@eslint/core").TextSourceCode} TextSourceCode */
23
23
  /** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */
24
+ /** @typedef {import("@eslint/core").FileProblem} FileProblem */
25
+ /** @typedef {import("@eslint/core").DirectiveType} DirectiveType */
26
+ /** @typedef {import("@eslint/core").RulesConfig} RulesConfig */
24
27
 
25
28
  //-----------------------------------------------------------------------------
26
29
  // Helpers
27
30
  //-----------------------------------------------------------------------------
28
31
 
32
+ const commentParser = new ConfigCommentParser();
33
+
34
+ const INLINE_CONFIG =
35
+ /^\s*(?:eslint(?:-enable|-disable(?:(?:-next)?-line)?)?)(?:\s|$)/u;
36
+
29
37
  /**
30
38
  * A class to represent a step in the traversal process.
31
39
  */
@@ -70,6 +78,12 @@ class JSONSourceCode extends TextSourceCodeBase {
70
78
  */
71
79
  #parents = new WeakMap();
72
80
 
81
+ /**
82
+ * Collection of inline configuration comments.
83
+ * @type {Array<JSONToken>}
84
+ */
85
+ #inlineConfigComments;
86
+
73
87
  /**
74
88
  * The AST of the source code.
75
89
  * @type {DocumentNode}
@@ -96,6 +110,136 @@ class JSONSourceCode extends TextSourceCodeBase {
96
110
  : [];
97
111
  }
98
112
 
113
+ /**
114
+ * Returns the value of the given comment.
115
+ * @param {JSONToken} comment The comment to get the value of.
116
+ * @returns {string} The value of the comment.
117
+ * @throws {Error} When an unexpected comment type is passed.
118
+ */
119
+ #getCommentValue(comment) {
120
+ if (comment.type === "LineComment") {
121
+ return this.getText(comment).slice(2); // strip leading `//`
122
+ }
123
+
124
+ if (comment.type === "BlockComment") {
125
+ return this.getText(comment).slice(2, -2); // strip leading `/*` and trailing `*/`
126
+ }
127
+
128
+ throw new Error(`Unexpected comment type '${comment.type}'`);
129
+ }
130
+
131
+ /**
132
+ * Returns an array of all inline configuration nodes found in the
133
+ * source code.
134
+ * @returns {Array<JSONToken>} An array of all inline configuration nodes.
135
+ */
136
+ getInlineConfigNodes() {
137
+ if (!this.#inlineConfigComments) {
138
+ this.#inlineConfigComments = this.comments.filter(comment =>
139
+ INLINE_CONFIG.test(this.#getCommentValue(comment)),
140
+ );
141
+ }
142
+
143
+ return this.#inlineConfigComments;
144
+ }
145
+
146
+ /**
147
+ * Returns directives that enable or disable rules along with any problems
148
+ * encountered while parsing the directives.
149
+ * @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
150
+ * that ESLint needs to further process the directives.
151
+ */
152
+ getDisableDirectives() {
153
+ const problems = [];
154
+ const directives = [];
155
+
156
+ this.getInlineConfigNodes().forEach(comment => {
157
+ const { label, value, justification } =
158
+ commentParser.parseDirective(this.#getCommentValue(comment));
159
+
160
+ // `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
161
+ if (
162
+ label === "eslint-disable-line" &&
163
+ comment.loc.start.line !== comment.loc.end.line
164
+ ) {
165
+ const message = `${label} comment should not span multiple lines.`;
166
+
167
+ problems.push({
168
+ ruleId: null,
169
+ message,
170
+ loc: comment.loc,
171
+ });
172
+ return;
173
+ }
174
+
175
+ switch (label) {
176
+ case "eslint-disable":
177
+ case "eslint-enable":
178
+ case "eslint-disable-next-line":
179
+ case "eslint-disable-line": {
180
+ const directiveType = label.slice("eslint-".length);
181
+
182
+ directives.push(
183
+ new Directive({
184
+ type: /** @type {DirectiveType} */ (directiveType),
185
+ node: comment,
186
+ value,
187
+ justification,
188
+ }),
189
+ );
190
+ }
191
+
192
+ // no default
193
+ }
194
+ });
195
+
196
+ return { problems, directives };
197
+ }
198
+
199
+ /**
200
+ * Returns inline rule configurations along with any problems
201
+ * encountered while parsing the configurations.
202
+ * @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:SourceLocation}>}} Information
203
+ * that ESLint needs to further process the rule configurations.
204
+ */
205
+ applyInlineConfig() {
206
+ const problems = [];
207
+ const configs = [];
208
+
209
+ this.getInlineConfigNodes().forEach(comment => {
210
+ const { label, value } = commentParser.parseDirective(
211
+ this.#getCommentValue(comment),
212
+ );
213
+
214
+ if (label === "eslint") {
215
+ const parseResult = commentParser.parseJSONLikeConfig(value);
216
+
217
+ if (parseResult.ok) {
218
+ configs.push({
219
+ config: {
220
+ rules: parseResult.config,
221
+ },
222
+ loc: comment.loc,
223
+ });
224
+ } else {
225
+ problems.push({
226
+ ruleId: null,
227
+ message:
228
+ /** @type {{ok: false, error: { message: string }}} */ (
229
+ parseResult
230
+ ).error.message,
231
+ loc: comment.loc,
232
+ });
233
+ }
234
+ }
235
+ });
236
+
237
+ return {
238
+ configs,
239
+ problems,
240
+ };
241
+ }
242
+
99
243
  /**
100
244
  * Returns the parent of the given node.
101
245
  * @param {JSONNode} node The node to get the parent of.
@@ -383,7 +527,7 @@ var noEmptyKeys = {
383
527
  const plugin = {
384
528
  meta: {
385
529
  name: "@eslint/json",
386
- version: "0.4.1", // x-release-please-version
530
+ version: "0.5.0", // x-release-please-version
387
531
  },
388
532
  languages: {
389
533
  json: new JSONLanguage({ mode: "json" }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint/json",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "JSON linting plugin for ESLint",
5
5
  "author": "Nicholas C. Zakas",
6
6
  "type": "module",
@@ -61,14 +61,15 @@
61
61
  ],
62
62
  "license": "Apache-2.0",
63
63
  "dependencies": {
64
- "@eslint/plugin-kit": "^0.1.0",
65
- "@humanwhocodes/momoa": "^3.2.0"
64
+ "@eslint/plugin-kit": "^0.2.0",
65
+ "@humanwhocodes/momoa": "^3.2.1"
66
66
  },
67
67
  "devDependencies": {
68
- "@eslint/core": "^0.3.0",
68
+ "@eslint/core": "^0.6.0",
69
69
  "@types/eslint": "^8.56.10",
70
70
  "c8": "^9.1.0",
71
- "eslint": "^9.6.0",
71
+ "dedent": "^1.5.3",
72
+ "eslint": "^9.11.1",
72
73
  "eslint-config-eslint": "^11.0.0",
73
74
  "lint-staged": "^15.2.7",
74
75
  "mocha": "^10.4.0",