@eslint/json 0.1.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.
@@ -0,0 +1,180 @@
1
+ export { plugin as default };
2
+ export type DocumentNode = import("@humanwhocodes/momoa").DocumentNode;
3
+ export type JSONNode = import("@humanwhocodes/momoa").Node;
4
+ export type JSONToken = import("@humanwhocodes/momoa").Token;
5
+ export type SyntaxElement = import("@eslint/core").SyntaxElement;
6
+ export type Language = import("@eslint/core").Language;
7
+ export type File = import("@eslint/core").File;
8
+ export type TraversalStep = import("@eslint/core").TraversalStep;
9
+ export type VisitTraversalStep = import("@eslint/core").VisitTraversalStep;
10
+ export type TextSourceCode = import("@eslint/core").TextSourceCode;
11
+ export type ParseResult = import("@eslint/core").ParseResult;
12
+ declare namespace plugin {
13
+ namespace languages {
14
+ let json: JSONLanguage;
15
+ let jsonc: JSONLanguage;
16
+ }
17
+ let rules: {
18
+ "no-duplicate-keys": {
19
+ meta: {
20
+ type: string;
21
+ docs: {
22
+ description: string;
23
+ };
24
+ messages: {
25
+ duplicateKey: string;
26
+ };
27
+ };
28
+ create(context: any): {
29
+ Object(): void;
30
+ Member(node: any): void;
31
+ "Object:exit"(): void;
32
+ };
33
+ };
34
+ "no-empty-keys": {
35
+ meta: {
36
+ type: string;
37
+ docs: {
38
+ description: string;
39
+ };
40
+ messages: {
41
+ emptyKey: string;
42
+ };
43
+ };
44
+ create(context: any): {
45
+ Member(node: any): void;
46
+ };
47
+ };
48
+ };
49
+ let configs: {};
50
+ }
51
+ /**
52
+ * @filedescription Functions to fix up rules to provide missing methods on the `context` object.
53
+ * @author Nicholas C. Zakas
54
+ */
55
+ /**
56
+ * JSON Language Object
57
+ * @implements {Language}
58
+ */
59
+ declare class JSONLanguage implements Language {
60
+ /**
61
+ * Creates a new instance.
62
+ * @param {Object} options The options to use for this instance.
63
+ * @param {"json"|"jsonc"} options.mode The parser mode to use.
64
+ */
65
+ constructor({ mode }: {
66
+ mode: "json" | "jsonc";
67
+ });
68
+ /**
69
+ * The type of file to read.
70
+ * @type {"text"}
71
+ */
72
+ fileType: "text";
73
+ /**
74
+ * The line number at which the parser starts counting.
75
+ * @type {0|1}
76
+ */
77
+ lineStart: 0 | 1;
78
+ /**
79
+ * The column number at which the parser starts counting.
80
+ * @type {0|1}
81
+ */
82
+ columnStart: 0 | 1;
83
+ /**
84
+ * The name of the key that holds the type of the node.
85
+ * @type {string}
86
+ */
87
+ nodeTypeKey: string;
88
+ /**
89
+ * The visitor keys.
90
+ * @type {Record<string, string[]>}
91
+ */
92
+ visitorKeys: Record<string, string[]>;
93
+ /**
94
+ * Validates the language options.
95
+ * @param {Object} languageOptions The language options to validate.
96
+ * @returns {void}
97
+ * @throws {Error} When the language options are invalid.
98
+ */
99
+ validateLanguageOptions(languageOptions: any): void;
100
+ /**
101
+ * Parses the given file into an AST.
102
+ * @param {File} file The virtual file to parse.
103
+ * @returns {ParseResult} The result of parsing.
104
+ */
105
+ parse(file: File): ParseResult;
106
+ /**
107
+ * Creates a new `JSONSourceCode` object from the given information.
108
+ * @param {File} file The virtual file to create a `JSONSourceCode` object from.
109
+ * @param {ParseResult} parseResult The result returned from `parse()`.
110
+ * @returns {JSONSourceCode} The new `JSONSourceCode` object.
111
+ */
112
+ createSourceCode(file: File, parseResult: ParseResult): JSONSourceCode;
113
+ #private;
114
+ }
115
+ /**
116
+ * JSON Source Code Object
117
+ * @implements {TextSourceCode}
118
+ */
119
+ declare class JSONSourceCode implements TextSourceCode {
120
+ /**
121
+ * Creates a new instance.
122
+ * @param {Object} options The options for the instance.
123
+ * @param {string} options.text The source code text.
124
+ * @param {DocumentNode & SyntaxElement} options.ast The root AST node.
125
+ */
126
+ constructor({ text, ast }: {
127
+ text: string;
128
+ ast: DocumentNode & SyntaxElement;
129
+ });
130
+ /**
131
+ * The AST of the source code.
132
+ * @type {DocumentNode & SyntaxElement}
133
+ */
134
+ ast: DocumentNode & SyntaxElement;
135
+ /**
136
+ * The text of the source code.
137
+ * @type {string}
138
+ */
139
+ text: string;
140
+ /**
141
+ * The comment node in the source code.
142
+ * @type {Array<JSONToken>|undefined}
143
+ */
144
+ comments: Array<JSONToken> | undefined;
145
+ /**
146
+ * Returns the parent of the given node.
147
+ * @param {JSONNode} node The node to get the parent of.
148
+ * @returns {JSONNode|undefined} The parent of the node.
149
+ */
150
+ getParent(node: JSONNode): JSONNode | undefined;
151
+ /**
152
+ * Gets all the ancestors of a given node
153
+ * @param {JSONNode} node The node
154
+ * @returns {Array<JSONNode>} All the ancestor nodes in the AST, not including the provided node, starting
155
+ * from the root node at index 0 and going inwards to the parent node.
156
+ * @throws {TypeError} When `node` is missing.
157
+ */
158
+ getAncestors(node: JSONNode): Array<JSONNode>;
159
+ /**
160
+ * Gets the source code for the given node.
161
+ * @param {JSONNode} [node] The AST node to get the text for.
162
+ * @param {number} [beforeCount] The number of characters before the node to retrieve.
163
+ * @param {number} [afterCount] The number of characters after the node to retrieve.
164
+ * @returns {string} The text representing the AST node.
165
+ * @public
166
+ */
167
+ public getText(node?: JSONNode, beforeCount?: number, afterCount?: number): string;
168
+ /**
169
+ * Gets the entire source text split into an array of lines.
170
+ * @returns {Array} The source text as an array of lines.
171
+ * @public
172
+ */
173
+ public get lines(): any[];
174
+ /**
175
+ * Traverse the source code and return the steps that were taken.
176
+ * @returns {Iterable<TraversalStep>} The steps that were taken while traversing the source code.
177
+ */
178
+ traverse(): Iterable<TraversalStep>;
179
+ #private;
180
+ }
@@ -0,0 +1,490 @@
1
+ // @ts-self-types="./index.d.ts"
2
+ import { iterator, visitorKeys, parse } from '@humanwhocodes/momoa';
3
+
4
+ /**
5
+ * @fileoverview The JSONSourceCode class.
6
+ * @author Nicholas C. Zakas
7
+ */
8
+
9
+
10
+ //-----------------------------------------------------------------------------
11
+ // Types
12
+ //-----------------------------------------------------------------------------
13
+
14
+ /** @typedef {import("@humanwhocodes/momoa").DocumentNode} DocumentNode */
15
+ /** @typedef {import("@humanwhocodes/momoa").Node} JSONNode */
16
+ /** @typedef {import("@humanwhocodes/momoa").Token} JSONToken */
17
+ /** @typedef {import("@eslint/core").SyntaxElement} SyntaxElement */
18
+ /** @typedef {import("@eslint/core").Language} Language */
19
+ /** @typedef {import("@eslint/core").File} File */
20
+ /** @typedef {import("@eslint/core").TraversalStep} TraversalStep */
21
+ /** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */
22
+ /** @typedef {import("@eslint/core").TextSourceCode} TextSourceCode */
23
+ /** @typedef {import("@eslint/core").ParseResult} ParseResult */
24
+
25
+ //-----------------------------------------------------------------------------
26
+ // Helpers
27
+ //-----------------------------------------------------------------------------
28
+
29
+ /**
30
+ * A class to represent a step in the traversal process.
31
+ * @implements {VisitTraversalStep}
32
+ */
33
+ class JSONTraversalStep {
34
+ /**
35
+ * The type of the step.
36
+ * @type {"visit"}
37
+ * @readonly
38
+ */
39
+ type = "visit";
40
+
41
+ /**
42
+ * The kind of the step. Represents the same data as the `type` property
43
+ * but it's a number for performance.
44
+ * @type {1}
45
+ * @readonly
46
+ */
47
+ kind = 1;
48
+
49
+ /**
50
+ * The target of the step.
51
+ * @type {JSONNode & SyntaxElement}
52
+ */
53
+ target;
54
+
55
+ /**
56
+ * The phase of the step.
57
+ * @type {1|2}
58
+ */
59
+ phase;
60
+
61
+ /**
62
+ * The arguments of the step.
63
+ * @type {Array<any>}
64
+ */
65
+ args;
66
+
67
+ /**
68
+ * Creates a new instance.
69
+ * @param {Object} options The options for the step.
70
+ * @param {JSONNode & SyntaxElement} options.target The target of the step.
71
+ * @param {1|2} options.phase The phase of the step.
72
+ * @param {Array<any>} options.args The arguments of the step.
73
+ */
74
+ constructor({ target, phase, args }) {
75
+ this.target = target;
76
+ this.phase = phase;
77
+ this.args = args;
78
+ }
79
+ }
80
+
81
+ //-----------------------------------------------------------------------------
82
+ // Exports
83
+ //-----------------------------------------------------------------------------
84
+
85
+ /**
86
+ * JSON Source Code Object
87
+ * @implements {TextSourceCode}
88
+ */
89
+ class JSONSourceCode {
90
+ /**
91
+ * Cached traversal steps.
92
+ * @type {Array<JSONTraversalStep>|undefined}
93
+ */
94
+ #steps;
95
+
96
+ /**
97
+ * Cache of parent nodes.
98
+ * @type {WeakMap<JSONNode, JSONNode>}
99
+ */
100
+ #parents = new WeakMap();
101
+
102
+ /**
103
+ * The lines of text in the source code.
104
+ * @type {Array<string>}
105
+ */
106
+ #lines;
107
+
108
+ /**
109
+ * The AST of the source code.
110
+ * @type {DocumentNode & SyntaxElement}
111
+ */
112
+ ast;
113
+
114
+ /**
115
+ * The text of the source code.
116
+ * @type {string}
117
+ */
118
+ text;
119
+
120
+ /**
121
+ * The comment node in the source code.
122
+ * @type {Array<JSONToken>|undefined}
123
+ */
124
+ comments;
125
+
126
+ /**
127
+ * Creates a new instance.
128
+ * @param {Object} options The options for the instance.
129
+ * @param {string} options.text The source code text.
130
+ * @param {DocumentNode & SyntaxElement} options.ast The root AST node.
131
+ */
132
+ constructor({ text, ast }) {
133
+ this.ast = ast;
134
+ this.text = text;
135
+ this.comments = ast.tokens.filter(token =>
136
+ token.type.endsWith("Comment"),
137
+ );
138
+ }
139
+
140
+ /**
141
+ * Returns the parent of the given node.
142
+ * @param {JSONNode} node The node to get the parent of.
143
+ * @returns {JSONNode|undefined} The parent of the node.
144
+ */
145
+ getParent(node) {
146
+ return this.#parents.get(node);
147
+ }
148
+
149
+ /**
150
+ * Gets all the ancestors of a given node
151
+ * @param {JSONNode} node The node
152
+ * @returns {Array<JSONNode>} All the ancestor nodes in the AST, not including the provided node, starting
153
+ * from the root node at index 0 and going inwards to the parent node.
154
+ * @throws {TypeError} When `node` is missing.
155
+ */
156
+ getAncestors(node) {
157
+ if (!node) {
158
+ throw new TypeError("Missing required argument: node.");
159
+ }
160
+
161
+ const ancestorsStartingAtParent = [];
162
+
163
+ for (
164
+ let ancestor = this.#parents.get(node);
165
+ ancestor;
166
+ ancestor = this.#parents.get(ancestor)
167
+ ) {
168
+ ancestorsStartingAtParent.push(ancestor);
169
+ }
170
+
171
+ return ancestorsStartingAtParent.reverse();
172
+ }
173
+
174
+ /**
175
+ * Gets the source code for the given node.
176
+ * @param {JSONNode} [node] The AST node to get the text for.
177
+ * @param {number} [beforeCount] The number of characters before the node to retrieve.
178
+ * @param {number} [afterCount] The number of characters after the node to retrieve.
179
+ * @returns {string} The text representing the AST node.
180
+ * @public
181
+ */
182
+ getText(node, beforeCount, afterCount) {
183
+ if (node) {
184
+ return this.text.slice(
185
+ Math.max(node.range[0] - (beforeCount || 0), 0),
186
+ node.range[1] + (afterCount || 0),
187
+ );
188
+ }
189
+ return this.text;
190
+ }
191
+
192
+ /**
193
+ * Gets the entire source text split into an array of lines.
194
+ * @returns {Array} The source text as an array of lines.
195
+ * @public
196
+ */
197
+ get lines() {
198
+ if (!this.#lines) {
199
+ this.#lines = this.text.split(/\r?\n/gu);
200
+ }
201
+ return this.#lines;
202
+ }
203
+
204
+ /**
205
+ * Traverse the source code and return the steps that were taken.
206
+ * @returns {Iterable<TraversalStep>} The steps that were taken while traversing the source code.
207
+ */
208
+ traverse() {
209
+ // Because the AST doesn't mutate, we can cache the steps
210
+ if (this.#steps) {
211
+ return this.#steps.values();
212
+ }
213
+
214
+ const steps = (this.#steps = []);
215
+
216
+ for (const { node, parent, phase } of iterator(
217
+ /** @type {DocumentNode} */ (this.ast),
218
+ )) {
219
+ this.#parents.set(node, parent);
220
+ steps.push(
221
+ new JSONTraversalStep({
222
+ target: /** @type {JSONNode & SyntaxElement} */ (node),
223
+ phase: phase === "enter" ? 1 : 2,
224
+ args: [node, parent],
225
+ }),
226
+ );
227
+ }
228
+
229
+ return steps;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * @filedescription Functions to fix up rules to provide missing methods on the `context` object.
235
+ * @author Nicholas C. Zakas
236
+ */
237
+
238
+
239
+ //-----------------------------------------------------------------------------
240
+ // Types
241
+ //-----------------------------------------------------------------------------
242
+
243
+
244
+ //-----------------------------------------------------------------------------
245
+ // Exports
246
+ //-----------------------------------------------------------------------------
247
+
248
+ /**
249
+ * JSON Language Object
250
+ * @implements {Language}
251
+ */
252
+ class JSONLanguage {
253
+ /**
254
+ * The type of file to read.
255
+ * @type {"text"}
256
+ */
257
+ fileType = "text";
258
+
259
+ /**
260
+ * The line number at which the parser starts counting.
261
+ * @type {0|1}
262
+ */
263
+ lineStart = 1;
264
+
265
+ /**
266
+ * The column number at which the parser starts counting.
267
+ * @type {0|1}
268
+ */
269
+ columnStart = 1;
270
+
271
+ /**
272
+ * The name of the key that holds the type of the node.
273
+ * @type {string}
274
+ */
275
+ nodeTypeKey = "type";
276
+
277
+ /**
278
+ * The parser mode.
279
+ * @type {"json"|"jsonc"}
280
+ */
281
+ #mode = "json";
282
+
283
+ /**
284
+ * The visitor keys.
285
+ * @type {Record<string, string[]>}
286
+ */
287
+ visitorKeys = Object.fromEntries([...visitorKeys]);
288
+
289
+ /**
290
+ * Creates a new instance.
291
+ * @param {Object} options The options to use for this instance.
292
+ * @param {"json"|"jsonc"} options.mode The parser mode to use.
293
+ */
294
+ constructor({ mode }) {
295
+ this.#mode = mode;
296
+ }
297
+
298
+ /* eslint-disable class-methods-use-this, no-unused-vars -- Required to complete interface. */
299
+ /**
300
+ * Validates the language options.
301
+ * @param {Object} languageOptions The language options to validate.
302
+ * @returns {void}
303
+ * @throws {Error} When the language options are invalid.
304
+ */
305
+ validateLanguageOptions(languageOptions) {
306
+ // no-op
307
+ }
308
+ /* eslint-enable class-methods-use-this, no-unused-vars -- Required to complete interface. */
309
+
310
+ /**
311
+ * Parses the given file into an AST.
312
+ * @param {File} file The virtual file to parse.
313
+ * @returns {ParseResult} The result of parsing.
314
+ */
315
+ parse(file) {
316
+ // Note: BOM already removed
317
+ const text = /** @type {string} */ (file.body);
318
+
319
+ /*
320
+ * Check for parsing errors first. If there's a parsing error, nothing
321
+ * else can happen. However, a parsing error does not throw an error
322
+ * from this method - it's just considered a fatal error message, a
323
+ * problem that ESLint identified just like any other.
324
+ */
325
+ try {
326
+ const root = parse(text, {
327
+ mode: this.#mode,
328
+ ranges: true,
329
+ tokens: true,
330
+ });
331
+
332
+ return {
333
+ ok: true,
334
+ ast: /** @type {DocumentNode & SyntaxElement} */ (root),
335
+ };
336
+ } catch (ex) {
337
+ // error messages end with (line:column) so we strip that off for ESLint
338
+ const message = ex.message
339
+ .slice(0, ex.message.lastIndexOf("("))
340
+ .trim();
341
+
342
+ return {
343
+ ok: false,
344
+ errors: [
345
+ {
346
+ ...ex,
347
+ message,
348
+ },
349
+ ],
350
+ };
351
+ }
352
+ }
353
+
354
+ /* eslint-disable class-methods-use-this -- Required to complete interface. */
355
+ /**
356
+ * Creates a new `JSONSourceCode` object from the given information.
357
+ * @param {File} file The virtual file to create a `JSONSourceCode` object from.
358
+ * @param {ParseResult} parseResult The result returned from `parse()`.
359
+ * @returns {JSONSourceCode} The new `JSONSourceCode` object.
360
+ */
361
+ createSourceCode(file, parseResult) {
362
+ return new JSONSourceCode({
363
+ text: /** @type {string} */ (file.body),
364
+ ast: parseResult.ast,
365
+ });
366
+ }
367
+ /* eslint-enable class-methods-use-this -- Required to complete interface. */
368
+ }
369
+
370
+ /**
371
+ * @fileoverview Rule to prevent duplicate keys in JSON.
372
+ * @author Nicholas C. Zakas
373
+ */
374
+
375
+ //-----------------------------------------------------------------------------
376
+ // Type Definitions
377
+ //-----------------------------------------------------------------------------
378
+
379
+ var noDuplicateKeys = {
380
+ meta: {
381
+ type: "problem",
382
+
383
+ docs: {
384
+ description: "Disallow duplicate keys in JSON objects",
385
+ },
386
+
387
+ messages: {
388
+ duplicateKey: 'Duplicate key "{{key}}" found.',
389
+ },
390
+ },
391
+
392
+ create(context) {
393
+ const objectKeys = [];
394
+ let keys;
395
+
396
+ return {
397
+ Object() {
398
+ objectKeys.push(keys);
399
+ keys = new Map();
400
+ },
401
+
402
+ Member(node) {
403
+ const key = node.name.value;
404
+
405
+ if (keys.has(key)) {
406
+ context.report({
407
+ loc: node.name.loc,
408
+ messageId: "duplicateKey",
409
+ data: {
410
+ key,
411
+ },
412
+ });
413
+ } else {
414
+ keys.set(key, node);
415
+ }
416
+ },
417
+ "Object:exit"() {
418
+ keys = objectKeys.pop();
419
+ },
420
+ };
421
+ },
422
+ };
423
+
424
+ /**
425
+ * @fileoverview Rule to prevent empty keys in JSON.
426
+ * @author Nicholas C. Zakas
427
+ */
428
+
429
+ var noEmptyKeys = {
430
+ meta: {
431
+ type: "problem",
432
+
433
+ docs: {
434
+ description: "Disallow empty keys in JSON objects",
435
+ },
436
+
437
+ messages: {
438
+ emptyKey: "Empty key found.",
439
+ },
440
+ },
441
+
442
+ create(context) {
443
+ return {
444
+ Member(node) {
445
+ const key = node.name.value;
446
+
447
+ if (key.trim() === "") {
448
+ context.report({
449
+ loc: node.name.loc,
450
+ messageId: "emptyKey",
451
+ });
452
+ }
453
+ },
454
+ };
455
+ },
456
+ };
457
+
458
+ /**
459
+ * @fileoverview JSON plugin.
460
+ * @author Nicholas C. Zakas
461
+ */
462
+
463
+
464
+ //-----------------------------------------------------------------------------
465
+ // Plugin
466
+ //-----------------------------------------------------------------------------
467
+
468
+ const plugin = {
469
+ languages: {
470
+ json: new JSONLanguage({ mode: "json" }),
471
+ jsonc: new JSONLanguage({ mode: "jsonc" }),
472
+ },
473
+ rules: {
474
+ "no-duplicate-keys": noDuplicateKeys,
475
+ "no-empty-keys": noEmptyKeys,
476
+ },
477
+ configs: {},
478
+ };
479
+
480
+ Object.assign(plugin.configs, {
481
+ recommended: {
482
+ plugins: { json: plugin },
483
+ rules: {
484
+ "json/no-duplicate-keys": "error",
485
+ "json/no-empty-keys": "error",
486
+ },
487
+ },
488
+ });
489
+
490
+ export { plugin as default };