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