@eslint/json 0.4.1 → 0.6.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,65 @@ 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
+
188
+ ## Allowing trailing commas in JSONC
189
+
190
+ 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:
191
+
192
+ ```js
193
+ import json from "@eslint/json";
194
+
195
+ export default [
196
+ // lint JSONC files
197
+ {
198
+ files: ["**/*.jsonc"],
199
+ language: "json/jsonc",
200
+ ...json.configs.recommended,
201
+ },
202
+
203
+ // lint JSONC files and allow trailing commas
204
+ {
205
+ files: ["**/tsconfig.json", ".vscode/*.json"],
206
+ language: "json/jsonc",
207
+ languageOptions: {
208
+ allowTrailingCommas: true,
209
+ },
210
+ ...json.configs.recommended,
211
+ },
212
+ ];
213
+ ```
214
+
215
+ **Note:** The `allowTrailingCommas` option is only valid for the `json/jsonc` language.
216
+
158
217
  ## Frequently Asked Questions
159
218
 
160
219
  ### How does this relate to `eslint-plugin-json` and `eslint-plugin-jsonc`?
@@ -178,11 +237,20 @@ Any other rules that catch potential problems in JSON are welcome to be implemen
178
237
 
179
238
  Apache 2.0
180
239
 
181
- ## Sponsors
182
-
183
240
  <!-- NOTE: This section is autogenerated. Do not manually edit.-->
184
241
  <!--sponsorsstart-->
185
- <!--sponsorsend-->
186
242
 
187
- <!--techsponsorsstart-->
188
- <!--techsponsorsend-->
243
+ ## Sponsors
244
+
245
+ The following companies, organizations, and individuals support ESLint's ongoing maintenance and development. [Become a Sponsor](https://eslint.org/donate)
246
+ to get your logo on our READMEs and [website](https://eslint.org/sponsors).
247
+
248
+ <h3>Platinum Sponsors</h3>
249
+ <p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="128"></a></p><h3>Gold Sponsors</h3>
250
+ <p><a href="https://trunk.io/"><img src="https://images.opencollective.com/trunkio/fb92d60/avatar.png" alt="trunk.io" height="96"></a></p><h3>Silver Sponsors</h3>
251
+ <p><a href="https://www.serptriumph.com/"><img src="https://images.opencollective.com/serp-triumph5/fea3074/logo.png" alt="SERP Triumph" height="64"></a> <a href="https://www.jetbrains.com/"><img src="https://images.opencollective.com/jetbrains/fe76f99/logo.png" alt="JetBrains" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a> <a href="https://www.workleap.com"><img src="https://avatars.githubusercontent.com/u/53535748?u=d1e55d7661d724bf2281c1bfd33cb8f99fe2465f&v=4" alt="Workleap" height="64"></a></p><h3>Bronze Sponsors</h3>
252
+ <p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://www.wordhint.net/"><img src="https://images.opencollective.com/wordhint/be86813/avatar.png" alt="WordHint" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340?v=4" alt="GitBook" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104?v=4" alt="Nx" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a></p>
253
+ <h3>Technology Sponsors</h3>
254
+ Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.
255
+ <p><a href="https://netlify.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/netlify-icon.svg" alt="Netlify" height="32"></a> <a href="https://algolia.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/algolia-icon.svg" alt="Algolia" height="32"></a> <a href="https://1password.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/1password-icon.svg" alt="1Password" height="32"></a></p>
256
+ <!--sponsorsend-->
@@ -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.
@@ -152,6 +296,10 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
152
296
  /** @typedef {import("@eslint/core").Language} Language */
153
297
  /** @typedef {import("@eslint/core").OkParseResult<DocumentNode>} OkParseResult */
154
298
  /** @typedef {import("@eslint/core").ParseResult<DocumentNode>} ParseResult */
299
+ /**
300
+ * @typedef {Object} JSONLanguageOptions
301
+ * @property {boolean} [allowTrailingCommas] Whether to allow trailing commas.
302
+ */
155
303
 
156
304
  //-----------------------------------------------------------------------------
157
305
  // Exports
@@ -207,26 +355,42 @@ class JSONLanguage {
207
355
  this.#mode = mode;
208
356
  }
209
357
 
210
- /* eslint-disable class-methods-use-this, no-unused-vars -- Required to complete interface. */
211
358
  /**
212
359
  * Validates the language options.
213
- * @param {Object} languageOptions The language options to validate.
360
+ * @param {JSONLanguageOptions} languageOptions The language options to validate.
214
361
  * @returns {void}
215
362
  * @throws {Error} When the language options are invalid.
216
363
  */
217
364
  validateLanguageOptions(languageOptions) {
218
- // no-op
365
+ if (languageOptions.allowTrailingCommas !== undefined) {
366
+ if (typeof languageOptions.allowTrailingCommas !== "boolean") {
367
+ throw new Error(
368
+ "allowTrailingCommas must be a boolean if provided.",
369
+ );
370
+ }
371
+
372
+ // we know that allowTrailingCommas is a boolean here
373
+
374
+ // only allowed in JSONC mode
375
+ if (this.#mode !== "jsonc") {
376
+ throw new Error(
377
+ "allowTrailingCommas option is only available in JSONC.",
378
+ );
379
+ }
380
+ }
219
381
  }
220
- /* eslint-enable class-methods-use-this, no-unused-vars -- Required to complete interface. */
221
382
 
222
383
  /**
223
384
  * Parses the given file into an AST.
224
385
  * @param {File} file The virtual file to parse.
386
+ * @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing.
225
387
  * @returns {ParseResult} The result of parsing.
226
388
  */
227
- parse(file) {
389
+ parse(file, context) {
228
390
  // Note: BOM already removed
229
391
  const text = /** @type {string} */ (file.body);
392
+ const allowTrailingCommas =
393
+ context?.languageOptions?.allowTrailingCommas;
230
394
 
231
395
  /*
232
396
  * Check for parsing errors first. If there's a parsing error, nothing
@@ -239,6 +403,7 @@ class JSONLanguage {
239
403
  mode: this.#mode,
240
404
  ranges: true,
241
405
  tokens: true,
406
+ allowTrailingCommas,
242
407
  });
243
408
 
244
409
  return {
@@ -386,7 +551,7 @@ var noEmptyKeys = {
386
551
  const plugin = {
387
552
  meta: {
388
553
  name: "@eslint/json",
389
- version: "0.4.1", // x-release-please-version
554
+ version: "0.6.0", // x-release-please-version
390
555
  },
391
556
  languages: {
392
557
  json: new JSONLanguage({ mode: "json" }),
@@ -7,9 +7,18 @@ 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>;
16
+ export type JSONLanguageOptions = {
17
+ /**
18
+ * Whether to allow trailing commas.
19
+ */
20
+ allowTrailingCommas?: boolean;
21
+ };
13
22
  /**
14
23
  * @filedescription Functions to fix up rules to provide missing methods on the `context` object.
15
24
  * @author Nicholas C. Zakas
@@ -17,6 +26,10 @@ export type ParseResult = import("@eslint/core").ParseResult<DocumentNode>;
17
26
  /** @typedef {import("@eslint/core").Language} Language */
18
27
  /** @typedef {import("@eslint/core").OkParseResult<DocumentNode>} OkParseResult */
19
28
  /** @typedef {import("@eslint/core").ParseResult<DocumentNode>} ParseResult */
29
+ /**
30
+ * @typedef {Object} JSONLanguageOptions
31
+ * @property {boolean} [allowTrailingCommas] Whether to allow trailing commas.
32
+ */
20
33
  /**
21
34
  * JSON Language Object
22
35
  * @implements {Language}
@@ -57,17 +70,20 @@ export class JSONLanguage implements Language {
57
70
  visitorKeys: Record<string, string[]>;
58
71
  /**
59
72
  * Validates the language options.
60
- * @param {Object} languageOptions The language options to validate.
73
+ * @param {JSONLanguageOptions} languageOptions The language options to validate.
61
74
  * @returns {void}
62
75
  * @throws {Error} When the language options are invalid.
63
76
  */
64
- validateLanguageOptions(languageOptions: any): void;
77
+ validateLanguageOptions(languageOptions: JSONLanguageOptions): void;
65
78
  /**
66
79
  * Parses the given file into an AST.
67
80
  * @param {File} file The virtual file to parse.
81
+ * @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing.
68
82
  * @returns {ParseResult} The result of parsing.
69
83
  */
70
- parse(file: File): ParseResult;
84
+ parse(file: File, context: {
85
+ languageOptions: JSONLanguageOptions;
86
+ }): ParseResult;
71
87
  /**
72
88
  * Creates a new `JSONSourceCode` object from the given information.
73
89
  * @param {File} file The virtual file to create a `JSONSourceCode` object from.
@@ -101,6 +117,37 @@ export class JSONSourceCode extends TextSourceCodeBase {
101
117
  * @type {Array<JSONToken>|undefined}
102
118
  */
103
119
  comments: Array<JSONToken> | undefined;
120
+ /**
121
+ * Returns an array of all inline configuration nodes found in the
122
+ * source code.
123
+ * @returns {Array<JSONToken>} An array of all inline configuration nodes.
124
+ */
125
+ getInlineConfigNodes(): Array<JSONToken>;
126
+ /**
127
+ * Returns directives that enable or disable rules along with any problems
128
+ * encountered while parsing the directives.
129
+ * @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
130
+ * that ESLint needs to further process the directives.
131
+ */
132
+ getDisableDirectives(): {
133
+ problems: Array<FileProblem>;
134
+ directives: Array<Directive>;
135
+ };
136
+ /**
137
+ * Returns inline rule configurations along with any problems
138
+ * encountered while parsing the configurations.
139
+ * @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:SourceLocation}>}} Information
140
+ * that ESLint needs to further process the rule configurations.
141
+ */
142
+ applyInlineConfig(): {
143
+ problems: Array<FileProblem>;
144
+ configs: Array<{
145
+ config: {
146
+ rules: RulesConfig;
147
+ };
148
+ loc: SourceLocation;
149
+ }>;
150
+ };
104
151
  /**
105
152
  * Returns the parent of the given node.
106
153
  * @param {JSONNode} node The node to get the parent of.
@@ -159,19 +206,7 @@ declare namespace plugin {
159
206
  let configs: {};
160
207
  }
161
208
  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 */
209
+ import { Directive } from '@eslint/plugin-kit';
175
210
  /**
176
211
  * A class to represent a step in the traversal process.
177
212
  */
@@ -7,9 +7,18 @@ 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>;
16
+ export type JSONLanguageOptions = {
17
+ /**
18
+ * Whether to allow trailing commas.
19
+ */
20
+ allowTrailingCommas?: boolean;
21
+ };
13
22
  /**
14
23
  * @filedescription Functions to fix up rules to provide missing methods on the `context` object.
15
24
  * @author Nicholas C. Zakas
@@ -17,6 +26,10 @@ export type ParseResult = import("@eslint/core").ParseResult<DocumentNode>;
17
26
  /** @typedef {import("@eslint/core").Language} Language */
18
27
  /** @typedef {import("@eslint/core").OkParseResult<DocumentNode>} OkParseResult */
19
28
  /** @typedef {import("@eslint/core").ParseResult<DocumentNode>} ParseResult */
29
+ /**
30
+ * @typedef {Object} JSONLanguageOptions
31
+ * @property {boolean} [allowTrailingCommas] Whether to allow trailing commas.
32
+ */
20
33
  /**
21
34
  * JSON Language Object
22
35
  * @implements {Language}
@@ -57,17 +70,20 @@ export class JSONLanguage implements Language {
57
70
  visitorKeys: Record<string, string[]>;
58
71
  /**
59
72
  * Validates the language options.
60
- * @param {Object} languageOptions The language options to validate.
73
+ * @param {JSONLanguageOptions} languageOptions The language options to validate.
61
74
  * @returns {void}
62
75
  * @throws {Error} When the language options are invalid.
63
76
  */
64
- validateLanguageOptions(languageOptions: any): void;
77
+ validateLanguageOptions(languageOptions: JSONLanguageOptions): void;
65
78
  /**
66
79
  * Parses the given file into an AST.
67
80
  * @param {File} file The virtual file to parse.
81
+ * @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing.
68
82
  * @returns {ParseResult} The result of parsing.
69
83
  */
70
- parse(file: File): ParseResult;
84
+ parse(file: File, context: {
85
+ languageOptions: JSONLanguageOptions;
86
+ }): ParseResult;
71
87
  /**
72
88
  * Creates a new `JSONSourceCode` object from the given information.
73
89
  * @param {File} file The virtual file to create a `JSONSourceCode` object from.
@@ -101,6 +117,37 @@ export class JSONSourceCode extends TextSourceCodeBase {
101
117
  * @type {Array<JSONToken>|undefined}
102
118
  */
103
119
  comments: Array<JSONToken> | undefined;
120
+ /**
121
+ * Returns an array of all inline configuration nodes found in the
122
+ * source code.
123
+ * @returns {Array<JSONToken>} An array of all inline configuration nodes.
124
+ */
125
+ getInlineConfigNodes(): Array<JSONToken>;
126
+ /**
127
+ * Returns directives that enable or disable rules along with any problems
128
+ * encountered while parsing the directives.
129
+ * @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
130
+ * that ESLint needs to further process the directives.
131
+ */
132
+ getDisableDirectives(): {
133
+ problems: Array<FileProblem>;
134
+ directives: Array<Directive>;
135
+ };
136
+ /**
137
+ * Returns inline rule configurations along with any problems
138
+ * encountered while parsing the configurations.
139
+ * @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:SourceLocation}>}} Information
140
+ * that ESLint needs to further process the rule configurations.
141
+ */
142
+ applyInlineConfig(): {
143
+ problems: Array<FileProblem>;
144
+ configs: Array<{
145
+ config: {
146
+ rules: RulesConfig;
147
+ };
148
+ loc: SourceLocation;
149
+ }>;
150
+ };
104
151
  /**
105
152
  * Returns the parent of the given node.
106
153
  * @param {JSONNode} node The node to get the parent of.
@@ -159,19 +206,7 @@ declare namespace plugin {
159
206
  let configs: {};
160
207
  }
161
208
  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 */
209
+ import { Directive } from '@eslint/plugin-kit';
175
210
  /**
176
211
  * A class to represent a step in the traversal process.
177
212
  */
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.
@@ -149,6 +293,10 @@ class JSONSourceCode extends TextSourceCodeBase {
149
293
  /** @typedef {import("@eslint/core").Language} Language */
150
294
  /** @typedef {import("@eslint/core").OkParseResult<DocumentNode>} OkParseResult */
151
295
  /** @typedef {import("@eslint/core").ParseResult<DocumentNode>} ParseResult */
296
+ /**
297
+ * @typedef {Object} JSONLanguageOptions
298
+ * @property {boolean} [allowTrailingCommas] Whether to allow trailing commas.
299
+ */
152
300
 
153
301
  //-----------------------------------------------------------------------------
154
302
  // Exports
@@ -204,26 +352,42 @@ class JSONLanguage {
204
352
  this.#mode = mode;
205
353
  }
206
354
 
207
- /* eslint-disable class-methods-use-this, no-unused-vars -- Required to complete interface. */
208
355
  /**
209
356
  * Validates the language options.
210
- * @param {Object} languageOptions The language options to validate.
357
+ * @param {JSONLanguageOptions} languageOptions The language options to validate.
211
358
  * @returns {void}
212
359
  * @throws {Error} When the language options are invalid.
213
360
  */
214
361
  validateLanguageOptions(languageOptions) {
215
- // no-op
362
+ if (languageOptions.allowTrailingCommas !== undefined) {
363
+ if (typeof languageOptions.allowTrailingCommas !== "boolean") {
364
+ throw new Error(
365
+ "allowTrailingCommas must be a boolean if provided.",
366
+ );
367
+ }
368
+
369
+ // we know that allowTrailingCommas is a boolean here
370
+
371
+ // only allowed in JSONC mode
372
+ if (this.#mode !== "jsonc") {
373
+ throw new Error(
374
+ "allowTrailingCommas option is only available in JSONC.",
375
+ );
376
+ }
377
+ }
216
378
  }
217
- /* eslint-enable class-methods-use-this, no-unused-vars -- Required to complete interface. */
218
379
 
219
380
  /**
220
381
  * Parses the given file into an AST.
221
382
  * @param {File} file The virtual file to parse.
383
+ * @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing.
222
384
  * @returns {ParseResult} The result of parsing.
223
385
  */
224
- parse(file) {
386
+ parse(file, context) {
225
387
  // Note: BOM already removed
226
388
  const text = /** @type {string} */ (file.body);
389
+ const allowTrailingCommas =
390
+ context?.languageOptions?.allowTrailingCommas;
227
391
 
228
392
  /*
229
393
  * Check for parsing errors first. If there's a parsing error, nothing
@@ -236,6 +400,7 @@ class JSONLanguage {
236
400
  mode: this.#mode,
237
401
  ranges: true,
238
402
  tokens: true,
403
+ allowTrailingCommas,
239
404
  });
240
405
 
241
406
  return {
@@ -383,7 +548,7 @@ var noEmptyKeys = {
383
548
  const plugin = {
384
549
  meta: {
385
550
  name: "@eslint/json",
386
- version: "0.4.1", // x-release-please-version
551
+ version: "0.6.0", // x-release-please-version
387
552
  },
388
553
  languages: {
389
554
  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.6.0",
4
4
  "description": "JSON linting plugin for ESLint",
5
5
  "author": "Nicholas C. Zakas",
6
6
  "type": "module",
@@ -44,6 +44,7 @@
44
44
  "build:dedupe-types": "node tools/dedupe-types.js dist/cjs/index.cjs dist/esm/index.js",
45
45
  "build:cts": "node -e \"fs.copyFileSync('dist/esm/index.d.ts', 'dist/cjs/index.d.cts')\"",
46
46
  "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts",
47
+ "build:readme": "node tools/update-readme.js",
47
48
  "test:jsr": "npx jsr@latest publish --dry-run",
48
49
  "pretest": "npm run build",
49
50
  "lint": "eslint",
@@ -61,15 +62,17 @@
61
62
  ],
62
63
  "license": "Apache-2.0",
63
64
  "dependencies": {
64
- "@eslint/plugin-kit": "^0.1.0",
65
- "@humanwhocodes/momoa": "^3.2.0"
65
+ "@eslint/plugin-kit": "^0.2.0",
66
+ "@humanwhocodes/momoa": "^3.3.0"
66
67
  },
67
68
  "devDependencies": {
68
- "@eslint/core": "^0.3.0",
69
+ "@eslint/core": "^0.6.0",
69
70
  "@types/eslint": "^8.56.10",
70
71
  "c8": "^9.1.0",
71
- "eslint": "^9.6.0",
72
+ "dedent": "^1.5.3",
73
+ "eslint": "^9.11.1",
72
74
  "eslint-config-eslint": "^11.0.0",
75
+ "got": "^14.4.2",
73
76
  "lint-staged": "^15.2.7",
74
77
  "mocha": "^10.4.0",
75
78
  "prettier": "^3.3.2",