@eslint/json 0.13.2 → 1.0.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,327 @@
1
+ /**
2
+ * @fileoverview The JSONSourceCode class.
3
+ * @author Nicholas C. Zakas
4
+ */
5
+ //-----------------------------------------------------------------------------
6
+ // Imports
7
+ //-----------------------------------------------------------------------------
8
+ import { iterator } from "@humanwhocodes/momoa";
9
+ import { VisitNodeStep, TextSourceCodeBase, ConfigCommentParser, Directive, } from "@eslint/plugin-kit";
10
+ //-----------------------------------------------------------------------------
11
+ // Types
12
+ //-----------------------------------------------------------------------------
13
+ /**
14
+ * @import { DocumentNode, AnyNode, Token, LocationRange } from "@humanwhocodes/momoa";
15
+ * @import { FileProblem, DirectiveType, RulesConfig } from "@eslint/core";
16
+ * @import { JSONSyntaxElement } from "../types.js";
17
+ * @import { JSONLanguageOptions } from "./json-language.js";
18
+ */
19
+ //-----------------------------------------------------------------------------
20
+ // Helpers
21
+ //-----------------------------------------------------------------------------
22
+ const commentParser = new ConfigCommentParser();
23
+ const INLINE_CONFIG = /^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u;
24
+ /**
25
+ * A class to represent a step in the traversal process.
26
+ */
27
+ class JSONTraversalStep extends VisitNodeStep {
28
+ /**
29
+ * The target of the step.
30
+ * @type {AnyNode}
31
+ */
32
+ target = undefined;
33
+ /**
34
+ * Creates a new instance.
35
+ * @param {Object} options The options for the step.
36
+ * @param {AnyNode} options.target The target of the step.
37
+ * @param {1|2} options.phase The phase of the step.
38
+ * @param {Array<any>} options.args The arguments of the step.
39
+ */
40
+ constructor({ target, phase, args }) {
41
+ super({ target, phase, args });
42
+ this.target = target;
43
+ }
44
+ }
45
+ /**
46
+ * Processes tokens to extract comments and their starting tokens.
47
+ * @param {Array<Token>} tokens The tokens to process.
48
+ * @returns {{ comments: Array<Token>, starts: Map<number, number>, ends: Map<number, number>}}
49
+ * An object containing an array of comments, a map of starting token range to token index, and
50
+ * a map of ending token range to token index.
51
+ */
52
+ function processTokens(tokens) {
53
+ /** @type {Array<Token>} */
54
+ const comments = [];
55
+ /** @type {Map<number, number>} */
56
+ const starts = new Map();
57
+ /** @type {Map<number, number>} */
58
+ const ends = new Map();
59
+ for (let i = 0; i < tokens.length; i++) {
60
+ const token = tokens[i];
61
+ if (token.type.endsWith("Comment")) {
62
+ comments.push(token);
63
+ }
64
+ starts.set(token.range[0], i);
65
+ ends.set(token.range[1], i);
66
+ }
67
+ return { comments, starts, ends };
68
+ }
69
+ //-----------------------------------------------------------------------------
70
+ // Exports
71
+ //-----------------------------------------------------------------------------
72
+ /**
73
+ * JSON Source Code Object
74
+ * @extends {TextSourceCodeBase<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
75
+ */
76
+ export class JSONSourceCode extends TextSourceCodeBase {
77
+ /**
78
+ * Cached traversal steps.
79
+ * @type {Array<JSONTraversalStep>|undefined}
80
+ */
81
+ #steps;
82
+ /**
83
+ * Cache of parent nodes.
84
+ * @type {WeakMap<AnyNode, AnyNode>}
85
+ */
86
+ #parents = new WeakMap();
87
+ /**
88
+ * Collection of inline configuration comments.
89
+ * @type {Array<Token>}
90
+ */
91
+ #inlineConfigComments;
92
+ /**
93
+ * The AST of the source code.
94
+ * @type {DocumentNode}
95
+ */
96
+ ast = undefined;
97
+ /**
98
+ * The comment tokens in the source code.
99
+ * @type {Array<Token>|undefined}
100
+ */
101
+ comments;
102
+ /**
103
+ * A map of token start positions to their corresponding index.
104
+ * @type {Map<number, number>}
105
+ */
106
+ #tokenStarts;
107
+ /**
108
+ * A map of token end positions to their corresponding index.
109
+ * @type {Map<number, number>}
110
+ */
111
+ #tokenEnds;
112
+ /**
113
+ * Creates a new instance.
114
+ * @param {Object} options The options for the instance.
115
+ * @param {string} options.text The source code text.
116
+ * @param {DocumentNode} options.ast The root AST node.
117
+ */
118
+ constructor({ text, ast }) {
119
+ super({ text, ast, lineEndingPattern: /\r\n|[\r\n]/u });
120
+ this.ast = ast;
121
+ const { comments, starts, ends } = processTokens(this.ast.tokens ?? []);
122
+ this.comments = comments;
123
+ this.#tokenStarts = starts;
124
+ this.#tokenEnds = ends;
125
+ }
126
+ /**
127
+ * Returns the value of the given comment.
128
+ * @param {Token} comment The comment to get the value of.
129
+ * @returns {string} The value of the comment.
130
+ * @throws {Error} When an unexpected comment type is passed.
131
+ */
132
+ #getCommentValue(comment) {
133
+ if (comment.type === "LineComment") {
134
+ return this.getText(comment).slice(2); // strip leading `//`
135
+ }
136
+ if (comment.type === "BlockComment") {
137
+ return this.getText(comment).slice(2, -2); // strip leading `/*` and trailing `*/`
138
+ }
139
+ throw new Error(`Unexpected comment type '${comment.type}'`);
140
+ }
141
+ /**
142
+ * Returns an array of all inline configuration nodes found in the
143
+ * source code.
144
+ * @returns {Array<Token>} An array of all inline configuration nodes.
145
+ */
146
+ getInlineConfigNodes() {
147
+ if (!this.#inlineConfigComments) {
148
+ this.#inlineConfigComments = this.comments.filter(comment => INLINE_CONFIG.test(this.#getCommentValue(comment)));
149
+ }
150
+ return this.#inlineConfigComments ?? [];
151
+ }
152
+ /**
153
+ * Returns directives that enable or disable rules along with any problems
154
+ * encountered while parsing the directives.
155
+ * @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
156
+ * that ESLint needs to further process the directives.
157
+ */
158
+ getDisableDirectives() {
159
+ /** @type {Array<FileProblem>} */
160
+ const problems = [];
161
+ /** @type {Array<Directive>} */
162
+ const directives = [];
163
+ this.getInlineConfigNodes().forEach(comment => {
164
+ const { label, value, justification } = commentParser.parseDirective(this.#getCommentValue(comment));
165
+ // `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
166
+ if (label === "eslint-disable-line" &&
167
+ comment.loc.start.line !== comment.loc.end.line) {
168
+ const message = `${label} comment should not span multiple lines.`;
169
+ problems.push({
170
+ ruleId: null,
171
+ message,
172
+ loc: comment.loc,
173
+ });
174
+ return;
175
+ }
176
+ switch (label) {
177
+ case "eslint-disable":
178
+ case "eslint-enable":
179
+ case "eslint-disable-next-line":
180
+ case "eslint-disable-line": {
181
+ const directiveType = label.slice("eslint-".length);
182
+ directives.push(new Directive({
183
+ type: /** @type {DirectiveType} */ (directiveType),
184
+ node: comment,
185
+ value,
186
+ justification,
187
+ }));
188
+ }
189
+ // no default
190
+ }
191
+ });
192
+ return { problems, directives };
193
+ }
194
+ /**
195
+ * Returns inline rule configurations along with any problems
196
+ * encountered while parsing the configurations.
197
+ * @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:LocationRange}>}} Information
198
+ * that ESLint needs to further process the rule configurations.
199
+ */
200
+ applyInlineConfig() {
201
+ /** @type {Array<FileProblem>} */
202
+ const problems = [];
203
+ /** @type {Array<{config:{rules:RulesConfig},loc:LocationRange}>} */
204
+ const configs = [];
205
+ this.getInlineConfigNodes().forEach(comment => {
206
+ const { label, value } = commentParser.parseDirective(this.#getCommentValue(comment));
207
+ if (label === "eslint") {
208
+ const parseResult = commentParser.parseJSONLikeConfig(value);
209
+ if (parseResult.ok) {
210
+ configs.push({
211
+ config: {
212
+ rules: parseResult.config,
213
+ },
214
+ loc: comment.loc,
215
+ });
216
+ }
217
+ else {
218
+ problems.push({
219
+ ruleId: null,
220
+ message:
221
+ /** @type {{ok: false, error: { message: string }}} */ (parseResult).error.message,
222
+ loc: comment.loc,
223
+ });
224
+ }
225
+ }
226
+ });
227
+ return {
228
+ configs,
229
+ problems,
230
+ };
231
+ }
232
+ /**
233
+ * Returns the parent of the given node.
234
+ * @param {AnyNode} node The node to get the parent of.
235
+ * @returns {AnyNode|undefined} The parent of the node.
236
+ */
237
+ getParent(node) {
238
+ return this.#parents.get(node);
239
+ }
240
+ /**
241
+ * Traverse the source code and return the steps that were taken.
242
+ * @returns {Iterable<JSONTraversalStep>} The steps that were taken while traversing the source code.
243
+ */
244
+ traverse() {
245
+ // Because the AST doesn't mutate, we can cache the steps
246
+ if (this.#steps) {
247
+ return this.#steps.values();
248
+ }
249
+ /** @type {Array<JSONTraversalStep>} */
250
+ const steps = (this.#steps = []);
251
+ for (const { node, parent, phase } of iterator(this.ast)) {
252
+ if (parent) {
253
+ this.#parents.set(
254
+ /** @type {AnyNode} */ (node),
255
+ /** @type {AnyNode} */ (parent));
256
+ }
257
+ steps.push(new JSONTraversalStep({
258
+ target: /** @type {AnyNode} */ (node),
259
+ phase: phase === "enter" ? 1 : 2,
260
+ args: [node, parent],
261
+ }));
262
+ }
263
+ return steps;
264
+ }
265
+ /**
266
+ * Gets the token before the given node or token, optionally including comments.
267
+ * @param {AnyNode|Token} nodeOrToken The node or token to get the previous token for.
268
+ * @param {Object} [options] Options object.
269
+ * @param {boolean} [options.includeComments] If true, return comments when they are present.
270
+ * @returns {Token|null} The previous token or comment, or null if there is none.
271
+ */
272
+ getTokenBefore(nodeOrToken, { includeComments = false } = {}) {
273
+ const index = this.#tokenStarts.get(nodeOrToken.range[0]);
274
+ if (index === undefined) {
275
+ return null;
276
+ }
277
+ let previousIndex = index - 1;
278
+ if (previousIndex < 0) {
279
+ return null;
280
+ }
281
+ const tokens = this.ast.tokens;
282
+ let tokenOrComment = tokens[previousIndex];
283
+ if (includeComments) {
284
+ return tokenOrComment;
285
+ }
286
+ // skip comments
287
+ while (tokenOrComment?.type.endsWith("Comment")) {
288
+ previousIndex--;
289
+ if (previousIndex < 0) {
290
+ return null;
291
+ }
292
+ tokenOrComment = tokens[previousIndex];
293
+ }
294
+ return tokenOrComment;
295
+ }
296
+ /**
297
+ * Gets the token after the given node or token, skipping any comments unless includeComments is true.
298
+ * @param {AnyNode|Token} nodeOrToken The node or token to get the next token for.
299
+ * @param {Object} [options] Options object.
300
+ * @param {boolean} [options.includeComments=false] If true, return comments when they are present.
301
+ * @returns {Token|null} The next token or comment, or null if there is none.
302
+ */
303
+ getTokenAfter(nodeOrToken, { includeComments = false } = {}) {
304
+ const index = this.#tokenEnds.get(nodeOrToken.range[1]);
305
+ if (index === undefined) {
306
+ return null;
307
+ }
308
+ let nextIndex = index + 1;
309
+ const tokens = this.ast.tokens;
310
+ if (nextIndex >= tokens.length) {
311
+ return null;
312
+ }
313
+ let tokenOrComment = tokens[nextIndex];
314
+ if (includeComments) {
315
+ return tokenOrComment;
316
+ }
317
+ // skip comments
318
+ while (tokenOrComment?.type.endsWith("Comment")) {
319
+ nextIndex++;
320
+ if (nextIndex >= tokens.length) {
321
+ return null;
322
+ }
323
+ tokenOrComment = tokens[nextIndex];
324
+ }
325
+ return tokenOrComment;
326
+ }
327
+ }
@@ -0,0 +1,14 @@
1
+ export default rule;
2
+ export type NoDuplicateKeysMessageIds = "duplicateKey";
3
+ export type NoDuplicateKeysRuleDefinition = JSONRuleDefinition<{
4
+ MessageIds: NoDuplicateKeysMessageIds;
5
+ }>;
6
+ /**
7
+ * @import { MemberNode } from "@humanwhocodes/momoa";
8
+ * @import { JSONRuleDefinition } from "../types.js";
9
+ * @typedef {"duplicateKey"} NoDuplicateKeysMessageIds
10
+ * @typedef {JSONRuleDefinition<{ MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateKeysRuleDefinition
11
+ */
12
+ /** @type {NoDuplicateKeysRuleDefinition} */
13
+ declare const rule: NoDuplicateKeysRuleDefinition;
14
+ import type { JSONRuleDefinition } from "../types.js";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @fileoverview Rule to prevent duplicate keys in JSON.
3
+ * @author Nicholas C. Zakas
4
+ */
5
+ //-----------------------------------------------------------------------------
6
+ // Imports
7
+ //-----------------------------------------------------------------------------
8
+ import { getKey, getRawKey } from "../util.js";
9
+ //-----------------------------------------------------------------------------
10
+ // Type Definitions
11
+ //-----------------------------------------------------------------------------
12
+ /**
13
+ * @import { MemberNode } from "@humanwhocodes/momoa";
14
+ * @import { JSONRuleDefinition } from "../types.js";
15
+ * @typedef {"duplicateKey"} NoDuplicateKeysMessageIds
16
+ * @typedef {JSONRuleDefinition<{ MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateKeysRuleDefinition
17
+ */
18
+ //-----------------------------------------------------------------------------
19
+ // Rule Definition
20
+ //-----------------------------------------------------------------------------
21
+ /** @type {NoDuplicateKeysRuleDefinition} */
22
+ const rule = {
23
+ meta: {
24
+ type: "problem",
25
+ docs: {
26
+ recommended: true,
27
+ description: "Disallow duplicate keys in JSON objects",
28
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-duplicate-keys.md",
29
+ },
30
+ messages: {
31
+ duplicateKey: 'Duplicate key "{{key}}" found.',
32
+ },
33
+ },
34
+ create(context) {
35
+ /** @type {Array<Map<string, MemberNode>|undefined>} */
36
+ const objectKeys = [];
37
+ /** @type {Map<string, MemberNode>|undefined} */
38
+ let keys;
39
+ return {
40
+ Object() {
41
+ objectKeys.push(keys);
42
+ keys = new Map();
43
+ },
44
+ Member(node) {
45
+ const key = getKey(node);
46
+ const rawKey = getRawKey(node, context.sourceCode);
47
+ if (keys.has(key)) {
48
+ context.report({
49
+ loc: node.name.loc,
50
+ messageId: "duplicateKey",
51
+ data: {
52
+ key: rawKey,
53
+ },
54
+ });
55
+ }
56
+ else {
57
+ keys.set(key, node);
58
+ }
59
+ },
60
+ "Object:exit"() {
61
+ keys = objectKeys.pop();
62
+ },
63
+ };
64
+ },
65
+ };
66
+ export default rule;
@@ -0,0 +1,13 @@
1
+ export default rule;
2
+ export type NoEmptyKeysMessageIds = "emptyKey";
3
+ export type NoEmptyKeysRuleDefinition = JSONRuleDefinition<{
4
+ MessageIds: NoEmptyKeysMessageIds;
5
+ }>;
6
+ /**
7
+ * @import { JSONRuleDefinition } from "../types.js";
8
+ * @typedef {"emptyKey"} NoEmptyKeysMessageIds
9
+ * @typedef {JSONRuleDefinition<{ MessageIds: NoEmptyKeysMessageIds }>} NoEmptyKeysRuleDefinition
10
+ */
11
+ /** @type {NoEmptyKeysRuleDefinition} */
12
+ declare const rule: NoEmptyKeysRuleDefinition;
13
+ import type { JSONRuleDefinition } from "../types.js";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @fileoverview Rule to prevent empty keys in JSON.
3
+ * @author Nicholas C. Zakas
4
+ */
5
+ //-----------------------------------------------------------------------------
6
+ // Imports
7
+ //-----------------------------------------------------------------------------
8
+ import { getKey } from "../util.js";
9
+ //-----------------------------------------------------------------------------
10
+ // Type Definitions
11
+ //-----------------------------------------------------------------------------
12
+ /**
13
+ * @import { JSONRuleDefinition } from "../types.js";
14
+ * @typedef {"emptyKey"} NoEmptyKeysMessageIds
15
+ * @typedef {JSONRuleDefinition<{ MessageIds: NoEmptyKeysMessageIds }>} NoEmptyKeysRuleDefinition
16
+ */
17
+ //-----------------------------------------------------------------------------
18
+ // Rule Definition
19
+ //-----------------------------------------------------------------------------
20
+ /** @type {NoEmptyKeysRuleDefinition} */
21
+ const rule = {
22
+ meta: {
23
+ type: "problem",
24
+ docs: {
25
+ recommended: true,
26
+ description: "Disallow empty keys in JSON objects",
27
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-empty-keys.md",
28
+ },
29
+ messages: {
30
+ emptyKey: "Empty key found.",
31
+ },
32
+ },
33
+ create(context) {
34
+ return {
35
+ Member(node) {
36
+ const key = getKey(node);
37
+ if (key.trim() === "") {
38
+ context.report({
39
+ loc: node.name.loc,
40
+ messageId: "emptyKey",
41
+ });
42
+ }
43
+ },
44
+ };
45
+ },
46
+ };
47
+ export default rule;
@@ -0,0 +1,18 @@
1
+ export default rule;
2
+ export type NoUnnormalizedKeysMessageIds = "unnormalizedKey";
3
+ export type NoUnnormalizedKeysOptions = {
4
+ form: string;
5
+ };
6
+ export type NoUnnormalizedKeysRuleDefinition = JSONRuleDefinition<{
7
+ RuleOptions: [NoUnnormalizedKeysOptions];
8
+ MessageIds: NoUnnormalizedKeysMessageIds;
9
+ }>;
10
+ /**
11
+ * @import { JSONRuleDefinition } from "../types.js";
12
+ * @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
13
+ * @typedef {{ form: string }} NoUnnormalizedKeysOptions
14
+ * @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
15
+ */
16
+ /** @type {NoUnnormalizedKeysRuleDefinition} */
17
+ declare const rule: NoUnnormalizedKeysRuleDefinition;
18
+ import type { JSONRuleDefinition } from "../types.js";
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @fileoverview Rule to detect unnormalized keys in JSON.
3
+ * @author Bradley Meck Farias
4
+ */
5
+ //-----------------------------------------------------------------------------
6
+ // Imports
7
+ //-----------------------------------------------------------------------------
8
+ import { getKey } from "../util.js";
9
+ //-----------------------------------------------------------------------------
10
+ // Type Definitions
11
+ //-----------------------------------------------------------------------------
12
+ /**
13
+ * @import { JSONRuleDefinition } from "../types.js";
14
+ * @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
15
+ * @typedef {{ form: string }} NoUnnormalizedKeysOptions
16
+ * @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
17
+ */
18
+ //-----------------------------------------------------------------------------
19
+ // Rule Definition
20
+ //-----------------------------------------------------------------------------
21
+ /** @type {NoUnnormalizedKeysRuleDefinition} */
22
+ const rule = {
23
+ meta: {
24
+ type: "problem",
25
+ docs: {
26
+ recommended: true,
27
+ description: "Disallow JSON keys that are not normalized",
28
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-unnormalized-keys.md",
29
+ },
30
+ messages: {
31
+ unnormalizedKey: "Unnormalized key '{{key}}' found.",
32
+ },
33
+ schema: [
34
+ {
35
+ type: "object",
36
+ properties: {
37
+ form: {
38
+ enum: ["NFC", "NFD", "NFKC", "NFKD"],
39
+ },
40
+ },
41
+ additionalProperties: false,
42
+ },
43
+ ],
44
+ defaultOptions: [
45
+ {
46
+ form: "NFC",
47
+ },
48
+ ],
49
+ },
50
+ create(context) {
51
+ const [{ form }] = context.options;
52
+ return {
53
+ Member(node) {
54
+ const key = getKey(node);
55
+ if (key.normalize(form) !== key) {
56
+ context.report({
57
+ loc: node.name.loc,
58
+ messageId: "unnormalizedKey",
59
+ data: {
60
+ key,
61
+ },
62
+ });
63
+ }
64
+ },
65
+ };
66
+ },
67
+ };
68
+ export default rule;
@@ -0,0 +1,8 @@
1
+ export default rule;
2
+ export type NoUnsafeValuesMessageIds = "unsafeNumber" | "unsafeInteger" | "unsafeZero" | "subnormal" | "loneSurrogate";
3
+ export type NoUnsafeValuesRuleDefinition = JSONRuleDefinition<{
4
+ MessageIds: NoUnsafeValuesMessageIds;
5
+ }>;
6
+ /** @type {NoUnsafeValuesRuleDefinition} */
7
+ declare const rule: NoUnsafeValuesRuleDefinition;
8
+ import type { JSONRuleDefinition } from "../types.js";