@eslint/json 0.14.0 → 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.
- package/README.md +3 -3
- package/dist/build/recommended-config.d.ts +7 -0
- package/dist/build/recommended-config.js +7 -0
- package/dist/build/rules.d.ts +9 -0
- package/dist/build/rules.js +14 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +41 -0
- package/dist/languages/json-language.d.ts +85 -0
- package/dist/languages/json-language.js +143 -0
- package/dist/languages/json-source-code.d.ts +123 -0
- package/dist/languages/json-source-code.js +327 -0
- package/dist/rules/no-duplicate-keys.d.ts +14 -0
- package/dist/rules/no-duplicate-keys.js +66 -0
- package/dist/rules/no-empty-keys.d.ts +13 -0
- package/dist/rules/no-empty-keys.js +47 -0
- package/dist/rules/no-unnormalized-keys.d.ts +18 -0
- package/dist/rules/no-unnormalized-keys.js +68 -0
- package/dist/rules/no-unsafe-values.d.ts +8 -0
- package/dist/rules/no-unsafe-values.js +139 -0
- package/dist/rules/sort-keys.d.ts +34 -0
- package/dist/rules/sort-keys.js +185 -0
- package/dist/rules/top-level-interop.d.ts +17 -0
- package/dist/rules/top-level-interop.js +44 -0
- package/dist/types.js +5 -0
- package/dist/util.d.ts +23 -0
- package/dist/util.js +33 -0
- package/package.json +18 -36
- package/dist/cjs/index.cjs +0 -1271
- package/dist/cjs/index.d.cts +0 -280
- package/dist/cjs/types.cts +0 -87
- package/dist/esm/index.d.ts +0 -280
- package/dist/esm/index.js +0 -1266
- package/dist/esm/types.ts +0 -87
- /package/dist/{esm/types.d.ts → types.d.ts} +0 -0
package/dist/cjs/index.cjs
DELETED
|
@@ -1,1271 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* @import { DocumentNode, AnyNode, Token, LocationRange, MemberNode } from "@humanwhocodes/momoa"
|
|
4
|
-
* @import { FileProblem, DirectiveType, RulesConfig, Language, OkParseResult, ParseResult, File } from "@eslint/core"
|
|
5
|
-
* @import { JSONSyntaxElement, JSONRuleDefinition } from "./types.ts"
|
|
6
|
-
*/
|
|
7
|
-
'use strict';
|
|
8
|
-
|
|
9
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
10
|
-
|
|
11
|
-
var momoa = require('@humanwhocodes/momoa');
|
|
12
|
-
var pluginKit = require('@eslint/plugin-kit');
|
|
13
|
-
var naturalCompare = require('natural-compare');
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @fileoverview The JSONSourceCode class.
|
|
17
|
-
* @author Nicholas C. Zakas
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//-----------------------------------------------------------------------------
|
|
22
|
-
// Types
|
|
23
|
-
//-----------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
//-----------------------------------------------------------------------------
|
|
29
|
-
// Helpers
|
|
30
|
-
//-----------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
const commentParser = new pluginKit.ConfigCommentParser();
|
|
33
|
-
|
|
34
|
-
const INLINE_CONFIG =
|
|
35
|
-
/^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* A class to represent a step in the traversal process.
|
|
39
|
-
*/
|
|
40
|
-
class JSONTraversalStep extends pluginKit.VisitNodeStep {
|
|
41
|
-
/**
|
|
42
|
-
* The target of the step.
|
|
43
|
-
* @type {AnyNode}
|
|
44
|
-
*/
|
|
45
|
-
target = undefined;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Creates a new instance.
|
|
49
|
-
* @param {Object} options The options for the step.
|
|
50
|
-
* @param {AnyNode} options.target The target of the step.
|
|
51
|
-
* @param {1|2} options.phase The phase of the step.
|
|
52
|
-
* @param {Array<any>} options.args The arguments of the step.
|
|
53
|
-
*/
|
|
54
|
-
constructor({ target, phase, args }) {
|
|
55
|
-
super({ target, phase, args });
|
|
56
|
-
|
|
57
|
-
this.target = target;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Processes tokens to extract comments and their starting tokens.
|
|
63
|
-
* @param {Array<Token>} tokens The tokens to process.
|
|
64
|
-
* @returns {{ comments: Array<Token>, starts: Map<number, number>, ends: Map<number, number>}}
|
|
65
|
-
* An object containing an array of comments, a map of starting token range to token index, and
|
|
66
|
-
* a map of ending token range to token index.
|
|
67
|
-
*/
|
|
68
|
-
function processTokens(tokens) {
|
|
69
|
-
/** @type {Array<Token>} */
|
|
70
|
-
const comments = [];
|
|
71
|
-
|
|
72
|
-
/** @type {Map<number, number>} */
|
|
73
|
-
const starts = new Map();
|
|
74
|
-
|
|
75
|
-
/** @type {Map<number, number>} */
|
|
76
|
-
const ends = new Map();
|
|
77
|
-
|
|
78
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
79
|
-
const token = tokens[i];
|
|
80
|
-
|
|
81
|
-
if (token.type.endsWith("Comment")) {
|
|
82
|
-
comments.push(token);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
starts.set(token.range[0], i);
|
|
86
|
-
ends.set(token.range[1], i);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return { comments, starts, ends };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
//-----------------------------------------------------------------------------
|
|
93
|
-
// Exports
|
|
94
|
-
//-----------------------------------------------------------------------------
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* JSON Source Code Object
|
|
98
|
-
* @extends {TextSourceCodeBase<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
|
|
99
|
-
*/
|
|
100
|
-
class JSONSourceCode extends pluginKit.TextSourceCodeBase {
|
|
101
|
-
/**
|
|
102
|
-
* Cached traversal steps.
|
|
103
|
-
* @type {Array<JSONTraversalStep>|undefined}
|
|
104
|
-
*/
|
|
105
|
-
#steps;
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Cache of parent nodes.
|
|
109
|
-
* @type {WeakMap<AnyNode, AnyNode>}
|
|
110
|
-
*/
|
|
111
|
-
#parents = new WeakMap();
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Collection of inline configuration comments.
|
|
115
|
-
* @type {Array<Token>}
|
|
116
|
-
*/
|
|
117
|
-
#inlineConfigComments;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* The AST of the source code.
|
|
121
|
-
* @type {DocumentNode}
|
|
122
|
-
*/
|
|
123
|
-
ast = undefined;
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* The comment tokens in the source code.
|
|
127
|
-
* @type {Array<Token>|undefined}
|
|
128
|
-
*/
|
|
129
|
-
comments;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* A map of token start positions to their corresponding index.
|
|
133
|
-
* @type {Map<number, number>}
|
|
134
|
-
*/
|
|
135
|
-
#tokenStarts;
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* A map of token end positions to their corresponding index.
|
|
139
|
-
* @type {Map<number, number>}
|
|
140
|
-
*/
|
|
141
|
-
#tokenEnds;
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Creates a new instance.
|
|
145
|
-
* @param {Object} options The options for the instance.
|
|
146
|
-
* @param {string} options.text The source code text.
|
|
147
|
-
* @param {DocumentNode} options.ast The root AST node.
|
|
148
|
-
*/
|
|
149
|
-
constructor({ text, ast }) {
|
|
150
|
-
super({ text, ast, lineEndingPattern: /\r\n|[\r\n]/u });
|
|
151
|
-
this.ast = ast;
|
|
152
|
-
|
|
153
|
-
const { comments, starts, ends } = processTokens(this.ast.tokens ?? []);
|
|
154
|
-
this.comments = comments;
|
|
155
|
-
this.#tokenStarts = starts;
|
|
156
|
-
this.#tokenEnds = ends;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Returns the value of the given comment.
|
|
161
|
-
* @param {Token} comment The comment to get the value of.
|
|
162
|
-
* @returns {string} The value of the comment.
|
|
163
|
-
* @throws {Error} When an unexpected comment type is passed.
|
|
164
|
-
*/
|
|
165
|
-
#getCommentValue(comment) {
|
|
166
|
-
if (comment.type === "LineComment") {
|
|
167
|
-
return this.getText(comment).slice(2); // strip leading `//`
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (comment.type === "BlockComment") {
|
|
171
|
-
return this.getText(comment).slice(2, -2); // strip leading `/*` and trailing `*/`
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
throw new Error(`Unexpected comment type '${comment.type}'`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Returns an array of all inline configuration nodes found in the
|
|
179
|
-
* source code.
|
|
180
|
-
* @returns {Array<Token>} An array of all inline configuration nodes.
|
|
181
|
-
*/
|
|
182
|
-
getInlineConfigNodes() {
|
|
183
|
-
if (!this.#inlineConfigComments) {
|
|
184
|
-
this.#inlineConfigComments = this.comments.filter(comment =>
|
|
185
|
-
INLINE_CONFIG.test(this.#getCommentValue(comment)),
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return this.#inlineConfigComments ?? [];
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Returns directives that enable or disable rules along with any problems
|
|
194
|
-
* encountered while parsing the directives.
|
|
195
|
-
* @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
|
|
196
|
-
* that ESLint needs to further process the directives.
|
|
197
|
-
*/
|
|
198
|
-
getDisableDirectives() {
|
|
199
|
-
/** @type {Array<FileProblem>} */
|
|
200
|
-
const problems = [];
|
|
201
|
-
/** @type {Array<Directive>} */
|
|
202
|
-
const directives = [];
|
|
203
|
-
|
|
204
|
-
this.getInlineConfigNodes().forEach(comment => {
|
|
205
|
-
const { label, value, justification } =
|
|
206
|
-
commentParser.parseDirective(this.#getCommentValue(comment));
|
|
207
|
-
|
|
208
|
-
// `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
|
|
209
|
-
if (
|
|
210
|
-
label === "eslint-disable-line" &&
|
|
211
|
-
comment.loc.start.line !== comment.loc.end.line
|
|
212
|
-
) {
|
|
213
|
-
const message = `${label} comment should not span multiple lines.`;
|
|
214
|
-
|
|
215
|
-
problems.push({
|
|
216
|
-
ruleId: null,
|
|
217
|
-
message,
|
|
218
|
-
loc: comment.loc,
|
|
219
|
-
});
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
switch (label) {
|
|
224
|
-
case "eslint-disable":
|
|
225
|
-
case "eslint-enable":
|
|
226
|
-
case "eslint-disable-next-line":
|
|
227
|
-
case "eslint-disable-line": {
|
|
228
|
-
const directiveType = label.slice("eslint-".length);
|
|
229
|
-
|
|
230
|
-
directives.push(
|
|
231
|
-
new pluginKit.Directive({
|
|
232
|
-
type: /** @type {DirectiveType} */ (directiveType),
|
|
233
|
-
node: comment,
|
|
234
|
-
value,
|
|
235
|
-
justification,
|
|
236
|
-
}),
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// no default
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
return { problems, directives };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Returns inline rule configurations along with any problems
|
|
249
|
-
* encountered while parsing the configurations.
|
|
250
|
-
* @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:LocationRange}>}} Information
|
|
251
|
-
* that ESLint needs to further process the rule configurations.
|
|
252
|
-
*/
|
|
253
|
-
applyInlineConfig() {
|
|
254
|
-
/** @type {Array<FileProblem>} */
|
|
255
|
-
const problems = [];
|
|
256
|
-
/** @type {Array<{config:{rules:RulesConfig},loc:LocationRange}>} */
|
|
257
|
-
const configs = [];
|
|
258
|
-
|
|
259
|
-
this.getInlineConfigNodes().forEach(comment => {
|
|
260
|
-
const { label, value } = commentParser.parseDirective(
|
|
261
|
-
this.#getCommentValue(comment),
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
if (label === "eslint") {
|
|
265
|
-
const parseResult = commentParser.parseJSONLikeConfig(value);
|
|
266
|
-
|
|
267
|
-
if (parseResult.ok) {
|
|
268
|
-
configs.push({
|
|
269
|
-
config: {
|
|
270
|
-
rules: parseResult.config,
|
|
271
|
-
},
|
|
272
|
-
loc: comment.loc,
|
|
273
|
-
});
|
|
274
|
-
} else {
|
|
275
|
-
problems.push({
|
|
276
|
-
ruleId: null,
|
|
277
|
-
message:
|
|
278
|
-
/** @type {{ok: false, error: { message: string }}} */ (
|
|
279
|
-
parseResult
|
|
280
|
-
).error.message,
|
|
281
|
-
loc: comment.loc,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
return {
|
|
288
|
-
configs,
|
|
289
|
-
problems,
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Returns the parent of the given node.
|
|
295
|
-
* @param {AnyNode} node The node to get the parent of.
|
|
296
|
-
* @returns {AnyNode|undefined} The parent of the node.
|
|
297
|
-
*/
|
|
298
|
-
getParent(node) {
|
|
299
|
-
return this.#parents.get(node);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Traverse the source code and return the steps that were taken.
|
|
304
|
-
* @returns {Iterable<JSONTraversalStep>} The steps that were taken while traversing the source code.
|
|
305
|
-
*/
|
|
306
|
-
traverse() {
|
|
307
|
-
// Because the AST doesn't mutate, we can cache the steps
|
|
308
|
-
if (this.#steps) {
|
|
309
|
-
return this.#steps.values();
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/** @type {Array<JSONTraversalStep>} */
|
|
313
|
-
const steps = (this.#steps = []);
|
|
314
|
-
|
|
315
|
-
for (const { node, parent, phase } of momoa.iterator(this.ast)) {
|
|
316
|
-
if (parent) {
|
|
317
|
-
this.#parents.set(
|
|
318
|
-
/** @type {AnyNode} */ (node),
|
|
319
|
-
/** @type {AnyNode} */ (parent),
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
steps.push(
|
|
324
|
-
new JSONTraversalStep({
|
|
325
|
-
target: /** @type {AnyNode} */ (node),
|
|
326
|
-
phase: phase === "enter" ? 1 : 2,
|
|
327
|
-
args: [node, parent],
|
|
328
|
-
}),
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return steps;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Gets the token before the given node or token, optionally including comments.
|
|
337
|
-
* @param {AnyNode|Token} nodeOrToken The node or token to get the previous token for.
|
|
338
|
-
* @param {Object} [options] Options object.
|
|
339
|
-
* @param {boolean} [options.includeComments] If true, return comments when they are present.
|
|
340
|
-
* @returns {Token|null} The previous token or comment, or null if there is none.
|
|
341
|
-
*/
|
|
342
|
-
getTokenBefore(nodeOrToken, { includeComments = false } = {}) {
|
|
343
|
-
const index = this.#tokenStarts.get(nodeOrToken.range[0]);
|
|
344
|
-
|
|
345
|
-
if (index === undefined) {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
let previousIndex = index - 1;
|
|
350
|
-
if (previousIndex < 0) {
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const tokens = this.ast.tokens;
|
|
355
|
-
let tokenOrComment = tokens[previousIndex];
|
|
356
|
-
|
|
357
|
-
if (includeComments) {
|
|
358
|
-
return tokenOrComment;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// skip comments
|
|
362
|
-
while (tokenOrComment?.type.endsWith("Comment")) {
|
|
363
|
-
previousIndex--;
|
|
364
|
-
|
|
365
|
-
if (previousIndex < 0) {
|
|
366
|
-
return null;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
tokenOrComment = tokens[previousIndex];
|
|
370
|
-
}
|
|
371
|
-
return tokenOrComment;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Gets the token after the given node or token, skipping any comments unless includeComments is true.
|
|
376
|
-
* @param {AnyNode|Token} nodeOrToken The node or token to get the next token for.
|
|
377
|
-
* @param {Object} [options] Options object.
|
|
378
|
-
* @param {boolean} [options.includeComments=false] If true, return comments when they are present.
|
|
379
|
-
* @returns {Token|null} The next token or comment, or null if there is none.
|
|
380
|
-
*/
|
|
381
|
-
getTokenAfter(nodeOrToken, { includeComments = false } = {}) {
|
|
382
|
-
const index = this.#tokenEnds.get(nodeOrToken.range[1]);
|
|
383
|
-
|
|
384
|
-
if (index === undefined) {
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
let nextIndex = index + 1;
|
|
389
|
-
const tokens = this.ast.tokens;
|
|
390
|
-
if (nextIndex >= tokens.length) {
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
let tokenOrComment = tokens[nextIndex];
|
|
395
|
-
|
|
396
|
-
if (includeComments) {
|
|
397
|
-
return tokenOrComment;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// skip comments
|
|
401
|
-
while (tokenOrComment?.type.endsWith("Comment")) {
|
|
402
|
-
nextIndex++;
|
|
403
|
-
|
|
404
|
-
if (nextIndex >= tokens.length) {
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
tokenOrComment = tokens[nextIndex];
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return tokenOrComment;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* @fileoverview The JSONLanguage class.
|
|
417
|
-
* @author Nicholas C. Zakas
|
|
418
|
-
*/
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
//-----------------------------------------------------------------------------
|
|
422
|
-
// Types
|
|
423
|
-
//-----------------------------------------------------------------------------
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
*
|
|
427
|
-
* @typedef {OkParseResult<DocumentNode>} JSONOkParseResult
|
|
428
|
-
* @typedef {ParseResult<DocumentNode>} JSONParseResult
|
|
429
|
-
*
|
|
430
|
-
* @typedef {Object} JSONLanguageOptions
|
|
431
|
-
* @property {boolean} [allowTrailingCommas] Whether to allow trailing commas in JSONC mode.
|
|
432
|
-
*/
|
|
433
|
-
|
|
434
|
-
//-----------------------------------------------------------------------------
|
|
435
|
-
// Exports
|
|
436
|
-
//-----------------------------------------------------------------------------
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* JSON Language Object
|
|
440
|
-
* @implements {Language<{ LangOptions: JSONLanguageOptions; Code: JSONSourceCode; RootNode: DocumentNode; Node: AnyNode }>}
|
|
441
|
-
*/
|
|
442
|
-
class JSONLanguage {
|
|
443
|
-
/**
|
|
444
|
-
* The type of file to read.
|
|
445
|
-
* @type {"text"}
|
|
446
|
-
*/
|
|
447
|
-
fileType = "text";
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* The line number at which the parser starts counting.
|
|
451
|
-
* @type {0|1}
|
|
452
|
-
*/
|
|
453
|
-
lineStart = 1;
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* The column number at which the parser starts counting.
|
|
457
|
-
* @type {0|1}
|
|
458
|
-
*/
|
|
459
|
-
columnStart = 1;
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* The name of the key that holds the type of the node.
|
|
463
|
-
* @type {string}
|
|
464
|
-
*/
|
|
465
|
-
nodeTypeKey = "type";
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* The parser mode.
|
|
469
|
-
* @type {"json"|"jsonc"|"json5"}
|
|
470
|
-
*/
|
|
471
|
-
#mode = "json";
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* The visitor keys.
|
|
475
|
-
* @type {Record<string, string[]>}
|
|
476
|
-
*/
|
|
477
|
-
visitorKeys = Object.fromEntries([...momoa.visitorKeys]);
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Creates a new instance.
|
|
481
|
-
* @param {Object} options The options to use for this instance.
|
|
482
|
-
* @param {"json"|"jsonc"|"json5"} options.mode The parser mode to use.
|
|
483
|
-
*/
|
|
484
|
-
constructor({ mode }) {
|
|
485
|
-
this.#mode = mode;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Validates the language options.
|
|
490
|
-
* @param {JSONLanguageOptions} languageOptions The language options to validate.
|
|
491
|
-
* @returns {void}
|
|
492
|
-
* @throws {Error} When the language options are invalid.
|
|
493
|
-
*/
|
|
494
|
-
validateLanguageOptions(languageOptions) {
|
|
495
|
-
if (languageOptions.allowTrailingCommas !== undefined) {
|
|
496
|
-
if (typeof languageOptions.allowTrailingCommas !== "boolean") {
|
|
497
|
-
throw new Error(
|
|
498
|
-
"allowTrailingCommas must be a boolean if provided.",
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// we know that allowTrailingCommas is a boolean here
|
|
503
|
-
|
|
504
|
-
// only allowed in JSONC mode
|
|
505
|
-
if (this.#mode !== "jsonc") {
|
|
506
|
-
throw new Error(
|
|
507
|
-
"allowTrailingCommas option is only available in JSONC.",
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Parses the given file into an AST.
|
|
515
|
-
* @param {File} file The virtual file to parse.
|
|
516
|
-
* @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing.
|
|
517
|
-
* @returns {JSONParseResult} The result of parsing.
|
|
518
|
-
*/
|
|
519
|
-
parse(file, context) {
|
|
520
|
-
// Note: BOM already removed
|
|
521
|
-
const text = /** @type {string} */ (file.body);
|
|
522
|
-
const allowTrailingCommas =
|
|
523
|
-
context?.languageOptions?.allowTrailingCommas;
|
|
524
|
-
|
|
525
|
-
/*
|
|
526
|
-
* Check for parsing errors first. If there's a parsing error, nothing
|
|
527
|
-
* else can happen. However, a parsing error does not throw an error
|
|
528
|
-
* from this method - it's just considered a fatal error message, a
|
|
529
|
-
* problem that ESLint identified just like any other.
|
|
530
|
-
*/
|
|
531
|
-
try {
|
|
532
|
-
const root = momoa.parse(text, {
|
|
533
|
-
mode: this.#mode,
|
|
534
|
-
ranges: true,
|
|
535
|
-
tokens: true,
|
|
536
|
-
allowTrailingCommas,
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
return {
|
|
540
|
-
ok: true,
|
|
541
|
-
ast: root,
|
|
542
|
-
};
|
|
543
|
-
} catch (ex) {
|
|
544
|
-
// error messages end with (line:column) so we strip that off for ESLint
|
|
545
|
-
const message = ex.message
|
|
546
|
-
.slice(0, ex.message.lastIndexOf("("))
|
|
547
|
-
.trim();
|
|
548
|
-
|
|
549
|
-
return {
|
|
550
|
-
ok: false,
|
|
551
|
-
errors: [
|
|
552
|
-
{
|
|
553
|
-
...ex,
|
|
554
|
-
message,
|
|
555
|
-
},
|
|
556
|
-
],
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/* eslint-disable class-methods-use-this -- Required to complete interface. */
|
|
562
|
-
/**
|
|
563
|
-
* Creates a new `JSONSourceCode` object from the given information.
|
|
564
|
-
* @param {File} file The virtual file to create a `JSONSourceCode` object from.
|
|
565
|
-
* @param {JSONOkParseResult} parseResult The result returned from `parse()`.
|
|
566
|
-
* @returns {JSONSourceCode} The new `JSONSourceCode` object.
|
|
567
|
-
*/
|
|
568
|
-
createSourceCode(file, parseResult) {
|
|
569
|
-
return new JSONSourceCode({
|
|
570
|
-
text: /** @type {string} */ (file.body),
|
|
571
|
-
ast: parseResult.ast,
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
/* eslint-enable class-methods-use-this -- Required to complete interface. */
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const rules$1 = /** @type {const} */ ({
|
|
578
|
-
"json/no-duplicate-keys": "error",
|
|
579
|
-
"json/no-empty-keys": "error",
|
|
580
|
-
"json/no-unnormalized-keys": "error",
|
|
581
|
-
"json/no-unsafe-values": "error"
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* @fileoverview Rule to prevent duplicate keys in JSON.
|
|
586
|
-
* @author Nicholas C. Zakas
|
|
587
|
-
*/
|
|
588
|
-
|
|
589
|
-
//-----------------------------------------------------------------------------
|
|
590
|
-
// Type Definitions
|
|
591
|
-
//-----------------------------------------------------------------------------
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
*
|
|
595
|
-
* @typedef {"duplicateKey"} NoDuplicateKeysMessageIds
|
|
596
|
-
* @typedef {JSONRuleDefinition<{ MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateKeysRuleDefinition
|
|
597
|
-
*/
|
|
598
|
-
|
|
599
|
-
//-----------------------------------------------------------------------------
|
|
600
|
-
// Rule Definition
|
|
601
|
-
//-----------------------------------------------------------------------------
|
|
602
|
-
|
|
603
|
-
/** @type {NoDuplicateKeysRuleDefinition} */
|
|
604
|
-
const rule$5 = {
|
|
605
|
-
meta: {
|
|
606
|
-
type: "problem",
|
|
607
|
-
|
|
608
|
-
docs: {
|
|
609
|
-
recommended: true,
|
|
610
|
-
description: "Disallow duplicate keys in JSON objects",
|
|
611
|
-
url: "https://github.com/eslint/json/tree/main/docs/rules/no-duplicate-keys.md",
|
|
612
|
-
},
|
|
613
|
-
|
|
614
|
-
messages: {
|
|
615
|
-
duplicateKey: 'Duplicate key "{{key}}" found.',
|
|
616
|
-
},
|
|
617
|
-
},
|
|
618
|
-
|
|
619
|
-
create(context) {
|
|
620
|
-
/** @type {Array<Map<string, MemberNode>|undefined>} */
|
|
621
|
-
const objectKeys = [];
|
|
622
|
-
|
|
623
|
-
/** @type {Map<string, MemberNode>|undefined} */
|
|
624
|
-
let keys;
|
|
625
|
-
|
|
626
|
-
return {
|
|
627
|
-
Object() {
|
|
628
|
-
objectKeys.push(keys);
|
|
629
|
-
keys = new Map();
|
|
630
|
-
},
|
|
631
|
-
|
|
632
|
-
Member(node) {
|
|
633
|
-
const key =
|
|
634
|
-
node.name.type === "String"
|
|
635
|
-
? node.name.value
|
|
636
|
-
: node.name.name;
|
|
637
|
-
|
|
638
|
-
if (keys.has(key)) {
|
|
639
|
-
context.report({
|
|
640
|
-
loc: node.name.loc,
|
|
641
|
-
messageId: "duplicateKey",
|
|
642
|
-
data: {
|
|
643
|
-
key,
|
|
644
|
-
},
|
|
645
|
-
});
|
|
646
|
-
} else {
|
|
647
|
-
keys.set(key, node);
|
|
648
|
-
}
|
|
649
|
-
},
|
|
650
|
-
"Object:exit"() {
|
|
651
|
-
keys = objectKeys.pop();
|
|
652
|
-
},
|
|
653
|
-
};
|
|
654
|
-
},
|
|
655
|
-
};
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* @fileoverview Rule to prevent empty keys in JSON.
|
|
659
|
-
* @author Nicholas C. Zakas
|
|
660
|
-
*/
|
|
661
|
-
|
|
662
|
-
//-----------------------------------------------------------------------------
|
|
663
|
-
// Type Definitions
|
|
664
|
-
//-----------------------------------------------------------------------------
|
|
665
|
-
|
|
666
|
-
/**
|
|
667
|
-
*
|
|
668
|
-
* @typedef {"emptyKey"} NoEmptyKeysMessageIds
|
|
669
|
-
* @typedef {JSONRuleDefinition<{ MessageIds: NoEmptyKeysMessageIds }>} NoEmptyKeysRuleDefinition
|
|
670
|
-
*/
|
|
671
|
-
|
|
672
|
-
//-----------------------------------------------------------------------------
|
|
673
|
-
// Rule Definition
|
|
674
|
-
//-----------------------------------------------------------------------------
|
|
675
|
-
|
|
676
|
-
/** @type {NoEmptyKeysRuleDefinition} */
|
|
677
|
-
const rule$4 = {
|
|
678
|
-
meta: {
|
|
679
|
-
type: "problem",
|
|
680
|
-
|
|
681
|
-
docs: {
|
|
682
|
-
recommended: true,
|
|
683
|
-
description: "Disallow empty keys in JSON objects",
|
|
684
|
-
url: "https://github.com/eslint/json/tree/main/docs/rules/no-empty-keys.md",
|
|
685
|
-
},
|
|
686
|
-
|
|
687
|
-
messages: {
|
|
688
|
-
emptyKey: "Empty key found.",
|
|
689
|
-
},
|
|
690
|
-
},
|
|
691
|
-
|
|
692
|
-
create(context) {
|
|
693
|
-
return {
|
|
694
|
-
Member(node) {
|
|
695
|
-
const key =
|
|
696
|
-
node.name.type === "String"
|
|
697
|
-
? node.name.value
|
|
698
|
-
: node.name.name;
|
|
699
|
-
|
|
700
|
-
if (key.trim() === "") {
|
|
701
|
-
context.report({
|
|
702
|
-
loc: node.name.loc,
|
|
703
|
-
messageId: "emptyKey",
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
},
|
|
707
|
-
};
|
|
708
|
-
},
|
|
709
|
-
};
|
|
710
|
-
|
|
711
|
-
/**
|
|
712
|
-
* @fileoverview Rule to detect unnormalized keys in JSON.
|
|
713
|
-
* @author Bradley Meck Farias
|
|
714
|
-
*/
|
|
715
|
-
|
|
716
|
-
//-----------------------------------------------------------------------------
|
|
717
|
-
// Type Definitions
|
|
718
|
-
//-----------------------------------------------------------------------------
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
*
|
|
722
|
-
* @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
|
|
723
|
-
* @typedef {{ form: string }} NoUnnormalizedKeysOptions
|
|
724
|
-
* @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
|
|
725
|
-
*/
|
|
726
|
-
|
|
727
|
-
//-----------------------------------------------------------------------------
|
|
728
|
-
// Rule Definition
|
|
729
|
-
//-----------------------------------------------------------------------------
|
|
730
|
-
|
|
731
|
-
/** @type {NoUnnormalizedKeysRuleDefinition} */
|
|
732
|
-
const rule$3 = {
|
|
733
|
-
meta: {
|
|
734
|
-
type: "problem",
|
|
735
|
-
|
|
736
|
-
docs: {
|
|
737
|
-
recommended: true,
|
|
738
|
-
description: "Disallow JSON keys that are not normalized",
|
|
739
|
-
url: "https://github.com/eslint/json/tree/main/docs/rules/no-unnormalized-keys.md",
|
|
740
|
-
},
|
|
741
|
-
|
|
742
|
-
messages: {
|
|
743
|
-
unnormalizedKey: "Unnormalized key '{{key}}' found.",
|
|
744
|
-
},
|
|
745
|
-
|
|
746
|
-
schema: [
|
|
747
|
-
{
|
|
748
|
-
type: "object",
|
|
749
|
-
properties: {
|
|
750
|
-
form: {
|
|
751
|
-
enum: ["NFC", "NFD", "NFKC", "NFKD"],
|
|
752
|
-
},
|
|
753
|
-
},
|
|
754
|
-
additionalProperties: false,
|
|
755
|
-
},
|
|
756
|
-
],
|
|
757
|
-
|
|
758
|
-
defaultOptions: [
|
|
759
|
-
{
|
|
760
|
-
form: "NFC",
|
|
761
|
-
},
|
|
762
|
-
],
|
|
763
|
-
},
|
|
764
|
-
|
|
765
|
-
create(context) {
|
|
766
|
-
const [{ form }] = context.options;
|
|
767
|
-
|
|
768
|
-
return {
|
|
769
|
-
Member(node) {
|
|
770
|
-
const key =
|
|
771
|
-
node.name.type === "String"
|
|
772
|
-
? node.name.value
|
|
773
|
-
: node.name.name;
|
|
774
|
-
|
|
775
|
-
if (key.normalize(form) !== key) {
|
|
776
|
-
context.report({
|
|
777
|
-
loc: node.name.loc,
|
|
778
|
-
messageId: "unnormalizedKey",
|
|
779
|
-
data: {
|
|
780
|
-
key,
|
|
781
|
-
},
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
},
|
|
785
|
-
};
|
|
786
|
-
},
|
|
787
|
-
};
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* @fileoverview Rule to detect unsafe values in JSON.
|
|
791
|
-
* @author Bradley Meck Farias
|
|
792
|
-
*/
|
|
793
|
-
|
|
794
|
-
//-----------------------------------------------------------------------------
|
|
795
|
-
// Type Definitions
|
|
796
|
-
//-----------------------------------------------------------------------------
|
|
797
|
-
|
|
798
|
-
/**
|
|
799
|
-
*
|
|
800
|
-
* @typedef {"unsafeNumber"|"unsafeInteger"|"unsafeZero"|"subnormal"|"loneSurrogate"} NoUnsafeValuesMessageIds
|
|
801
|
-
* @typedef {JSONRuleDefinition<{ MessageIds: NoUnsafeValuesMessageIds }>} NoUnsafeValuesRuleDefinition
|
|
802
|
-
*/
|
|
803
|
-
|
|
804
|
-
//-----------------------------------------------------------------------------
|
|
805
|
-
// Helpers
|
|
806
|
-
//-----------------------------------------------------------------------------
|
|
807
|
-
|
|
808
|
-
/*
|
|
809
|
-
* This rule is based on the JSON grammar from RFC 8259, section 6.
|
|
810
|
-
* https://tools.ietf.org/html/rfc8259#section-6
|
|
811
|
-
*
|
|
812
|
-
* We separately capture the integer and fractional parts of a number, so that
|
|
813
|
-
* we can check for unsafe numbers that will evaluate to Infinity.
|
|
814
|
-
*/
|
|
815
|
-
const NUMBER = /^-?(?<int>0|([1-9]\d*))(?:\.(?<frac>\d+))?(?:e[+-]?\d+)?$/iu;
|
|
816
|
-
const NON_ZERO = /[1-9]/u;
|
|
817
|
-
|
|
818
|
-
//-----------------------------------------------------------------------------
|
|
819
|
-
// Rule Definition
|
|
820
|
-
//-----------------------------------------------------------------------------
|
|
821
|
-
|
|
822
|
-
/** @type {NoUnsafeValuesRuleDefinition} */
|
|
823
|
-
const rule$2 = {
|
|
824
|
-
meta: {
|
|
825
|
-
type: "problem",
|
|
826
|
-
|
|
827
|
-
docs: {
|
|
828
|
-
recommended: true,
|
|
829
|
-
description: "Disallow JSON values that are unsafe for interchange",
|
|
830
|
-
url: "https://github.com/eslint/json/tree/main/docs/rules/no-unsafe-values.md",
|
|
831
|
-
},
|
|
832
|
-
|
|
833
|
-
messages: {
|
|
834
|
-
unsafeNumber: "The number '{{ value }}' will evaluate to Infinity.",
|
|
835
|
-
unsafeInteger:
|
|
836
|
-
"The integer '{{ value }}' is outside the safe integer range.",
|
|
837
|
-
unsafeZero: "The number '{{ value }}' will evaluate to zero.",
|
|
838
|
-
subnormal:
|
|
839
|
-
"Unexpected subnormal number '{{ value }}' found, which may cause interoperability issues.",
|
|
840
|
-
loneSurrogate: "Lone surrogate '{{ surrogate }}' found.",
|
|
841
|
-
},
|
|
842
|
-
},
|
|
843
|
-
|
|
844
|
-
create(context) {
|
|
845
|
-
return {
|
|
846
|
-
Number(node) {
|
|
847
|
-
const value = context.sourceCode.getText(node);
|
|
848
|
-
|
|
849
|
-
if (Number.isFinite(node.value) !== true) {
|
|
850
|
-
context.report({
|
|
851
|
-
loc: node.loc,
|
|
852
|
-
messageId: "unsafeNumber",
|
|
853
|
-
data: { value },
|
|
854
|
-
});
|
|
855
|
-
} else {
|
|
856
|
-
// Also matches -0, intentionally
|
|
857
|
-
if (node.value === 0) {
|
|
858
|
-
// If the value has been rounded down to 0, but there was some
|
|
859
|
-
// fraction or non-zero part before the e-, this is a very small
|
|
860
|
-
// number that doesn't fit inside an f64.
|
|
861
|
-
const match = value.match(NUMBER);
|
|
862
|
-
// assert(match, "If the regex is right, match is always truthy")
|
|
863
|
-
|
|
864
|
-
// If any part of the number other than the exponent has a
|
|
865
|
-
// non-zero digit in it, this number was not intended to be
|
|
866
|
-
// evaluated down to a zero.
|
|
867
|
-
if (
|
|
868
|
-
NON_ZERO.test(match.groups.int) ||
|
|
869
|
-
NON_ZERO.test(match.groups.frac)
|
|
870
|
-
) {
|
|
871
|
-
context.report({
|
|
872
|
-
loc: node.loc,
|
|
873
|
-
messageId: "unsafeZero",
|
|
874
|
-
data: { value },
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
} else if (!/[.e]/iu.test(value)) {
|
|
878
|
-
// Intended to be an integer
|
|
879
|
-
if (
|
|
880
|
-
node.value > Number.MAX_SAFE_INTEGER ||
|
|
881
|
-
node.value < Number.MIN_SAFE_INTEGER
|
|
882
|
-
) {
|
|
883
|
-
context.report({
|
|
884
|
-
loc: node.loc,
|
|
885
|
-
messageId: "unsafeInteger",
|
|
886
|
-
data: { value },
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
} else {
|
|
890
|
-
// Floating point. Check for subnormal.
|
|
891
|
-
const buffer = new ArrayBuffer(8);
|
|
892
|
-
const view = new DataView(buffer);
|
|
893
|
-
view.setFloat64(0, node.value, false);
|
|
894
|
-
const asBigInt = view.getBigUint64(0, false);
|
|
895
|
-
// Subnormals have an 11-bit exponent of 0 and a non-zero mantissa.
|
|
896
|
-
if ((asBigInt & 0x7ff0000000000000n) === 0n) {
|
|
897
|
-
context.report({
|
|
898
|
-
loc: node.loc,
|
|
899
|
-
messageId: "subnormal",
|
|
900
|
-
// Value included so that it's seen in scientific notation
|
|
901
|
-
data: {
|
|
902
|
-
value,
|
|
903
|
-
},
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
},
|
|
909
|
-
String(node) {
|
|
910
|
-
if (node.value.isWellFormed) {
|
|
911
|
-
if (node.value.isWellFormed()) {
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
// match any high surrogate and, if it exists, a paired low surrogate
|
|
916
|
-
// match any low surrogate not already matched
|
|
917
|
-
const surrogatePattern =
|
|
918
|
-
/[\uD800-\uDBFF][\uDC00-\uDFFF]?|[\uDC00-\uDFFF]/gu;
|
|
919
|
-
let match = surrogatePattern.exec(node.value);
|
|
920
|
-
while (match) {
|
|
921
|
-
// only need to report non-paired surrogates
|
|
922
|
-
if (match[0].length < 2) {
|
|
923
|
-
context.report({
|
|
924
|
-
loc: node.loc,
|
|
925
|
-
messageId: "loneSurrogate",
|
|
926
|
-
data: {
|
|
927
|
-
surrogate: JSON.stringify(match[0]).slice(
|
|
928
|
-
1,
|
|
929
|
-
-1,
|
|
930
|
-
),
|
|
931
|
-
},
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
match = surrogatePattern.exec(node.value);
|
|
935
|
-
}
|
|
936
|
-
},
|
|
937
|
-
};
|
|
938
|
-
},
|
|
939
|
-
};
|
|
940
|
-
|
|
941
|
-
/**
|
|
942
|
-
* @fileoverview Rule to require JSON object keys to be sorted.
|
|
943
|
-
* Copied largely from https://github.com/eslint/eslint/blob/main/lib/rules/sort-keys.js
|
|
944
|
-
* @author Robin Thomas
|
|
945
|
-
*/
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
//-----------------------------------------------------------------------------
|
|
949
|
-
// Type Definitions
|
|
950
|
-
//-----------------------------------------------------------------------------
|
|
951
|
-
|
|
952
|
-
/**
|
|
953
|
-
*
|
|
954
|
-
* @typedef {Object} SortOptions
|
|
955
|
-
* @property {boolean} caseSensitive
|
|
956
|
-
* @property {boolean} natural
|
|
957
|
-
* @property {number} minKeys
|
|
958
|
-
* @property {boolean} allowLineSeparatedGroups
|
|
959
|
-
*
|
|
960
|
-
* @typedef {"sortKeys"} SortKeysMessageIds
|
|
961
|
-
* @typedef {"asc"|"desc"} SortDirection
|
|
962
|
-
* @typedef {[SortDirection, SortOptions]} SortKeysRuleOptions
|
|
963
|
-
* @typedef {JSONRuleDefinition<{ RuleOptions: SortKeysRuleOptions, MessageIds: SortKeysMessageIds }>} SortKeysRuleDefinition
|
|
964
|
-
* @typedef {(a:string,b:string) => boolean} Comparator
|
|
965
|
-
*/
|
|
966
|
-
|
|
967
|
-
//-----------------------------------------------------------------------------
|
|
968
|
-
// Helpers
|
|
969
|
-
//-----------------------------------------------------------------------------
|
|
970
|
-
|
|
971
|
-
const hasNonWhitespace = /\S/u;
|
|
972
|
-
|
|
973
|
-
const comparators = {
|
|
974
|
-
ascending: {
|
|
975
|
-
alphanumeric: {
|
|
976
|
-
/** @type {Comparator} */
|
|
977
|
-
sensitive: (a, b) => a <= b,
|
|
978
|
-
|
|
979
|
-
/** @type {Comparator} */
|
|
980
|
-
insensitive: (a, b) => a.toLowerCase() <= b.toLowerCase(),
|
|
981
|
-
},
|
|
982
|
-
natural: {
|
|
983
|
-
/** @type {Comparator} */
|
|
984
|
-
sensitive: (a, b) => naturalCompare(a, b) <= 0,
|
|
985
|
-
|
|
986
|
-
/** @type {Comparator} */
|
|
987
|
-
insensitive: (a, b) =>
|
|
988
|
-
naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0,
|
|
989
|
-
},
|
|
990
|
-
},
|
|
991
|
-
descending: {
|
|
992
|
-
alphanumeric: {
|
|
993
|
-
/** @type {Comparator} */
|
|
994
|
-
sensitive: (a, b) =>
|
|
995
|
-
comparators.ascending.alphanumeric.sensitive(b, a),
|
|
996
|
-
|
|
997
|
-
/** @type {Comparator} */
|
|
998
|
-
insensitive: (a, b) =>
|
|
999
|
-
comparators.ascending.alphanumeric.insensitive(b, a),
|
|
1000
|
-
},
|
|
1001
|
-
natural: {
|
|
1002
|
-
/** @type {Comparator} */
|
|
1003
|
-
sensitive: (a, b) => comparators.ascending.natural.sensitive(b, a),
|
|
1004
|
-
|
|
1005
|
-
/** @type {Comparator} */
|
|
1006
|
-
insensitive: (a, b) =>
|
|
1007
|
-
comparators.ascending.natural.insensitive(b, a),
|
|
1008
|
-
},
|
|
1009
|
-
},
|
|
1010
|
-
};
|
|
1011
|
-
|
|
1012
|
-
/**
|
|
1013
|
-
* Gets the MemberNode's string key value.
|
|
1014
|
-
* @param {MemberNode} member
|
|
1015
|
-
* @return {string}
|
|
1016
|
-
*/
|
|
1017
|
-
function getKey(member) {
|
|
1018
|
-
return member.name.type === "Identifier"
|
|
1019
|
-
? member.name.name
|
|
1020
|
-
: member.name.value;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
//-----------------------------------------------------------------------------
|
|
1024
|
-
// Rule Definition
|
|
1025
|
-
//-----------------------------------------------------------------------------
|
|
1026
|
-
|
|
1027
|
-
/** @type {SortKeysRuleDefinition} */
|
|
1028
|
-
const rule$1 = {
|
|
1029
|
-
meta: {
|
|
1030
|
-
type: "suggestion",
|
|
1031
|
-
|
|
1032
|
-
defaultOptions: [
|
|
1033
|
-
"asc",
|
|
1034
|
-
{
|
|
1035
|
-
allowLineSeparatedGroups: false,
|
|
1036
|
-
caseSensitive: true,
|
|
1037
|
-
minKeys: 2,
|
|
1038
|
-
natural: false,
|
|
1039
|
-
},
|
|
1040
|
-
],
|
|
1041
|
-
|
|
1042
|
-
docs: {
|
|
1043
|
-
recommended: false,
|
|
1044
|
-
description: `Require JSON object keys to be sorted`,
|
|
1045
|
-
url: "https://github.com/eslint/json/tree/main/docs/rules/sort-keys.md",
|
|
1046
|
-
},
|
|
1047
|
-
|
|
1048
|
-
messages: {
|
|
1049
|
-
sortKeys:
|
|
1050
|
-
"Expected object keys to be in {{sortName}} case-{{sensitivity}} {{direction}} order. '{{thisName}}' should be before '{{prevName}}'.",
|
|
1051
|
-
},
|
|
1052
|
-
|
|
1053
|
-
schema: [
|
|
1054
|
-
{
|
|
1055
|
-
enum: ["asc", "desc"],
|
|
1056
|
-
},
|
|
1057
|
-
{
|
|
1058
|
-
type: "object",
|
|
1059
|
-
properties: {
|
|
1060
|
-
caseSensitive: {
|
|
1061
|
-
type: "boolean",
|
|
1062
|
-
},
|
|
1063
|
-
natural: {
|
|
1064
|
-
type: "boolean",
|
|
1065
|
-
},
|
|
1066
|
-
minKeys: {
|
|
1067
|
-
type: "integer",
|
|
1068
|
-
minimum: 2,
|
|
1069
|
-
},
|
|
1070
|
-
allowLineSeparatedGroups: {
|
|
1071
|
-
type: "boolean",
|
|
1072
|
-
},
|
|
1073
|
-
},
|
|
1074
|
-
additionalProperties: false,
|
|
1075
|
-
},
|
|
1076
|
-
],
|
|
1077
|
-
},
|
|
1078
|
-
|
|
1079
|
-
create(context) {
|
|
1080
|
-
const [
|
|
1081
|
-
directionShort,
|
|
1082
|
-
{ allowLineSeparatedGroups, caseSensitive, natural, minKeys },
|
|
1083
|
-
] = context.options;
|
|
1084
|
-
|
|
1085
|
-
const direction = directionShort === "asc" ? "ascending" : "descending";
|
|
1086
|
-
const sortName = natural ? "natural" : "alphanumeric";
|
|
1087
|
-
const sensitivity = caseSensitive ? "sensitive" : "insensitive";
|
|
1088
|
-
const isValidOrder = comparators[direction][sortName][sensitivity];
|
|
1089
|
-
|
|
1090
|
-
// Note that @humanwhocodes/momoa doesn't include comments in the object.members tree, so we can't just see if a member is preceded by a comment
|
|
1091
|
-
const commentLineNums = new Set();
|
|
1092
|
-
for (const comment of context.sourceCode.comments) {
|
|
1093
|
-
for (
|
|
1094
|
-
let lineNum = comment.loc.start.line;
|
|
1095
|
-
lineNum <= comment.loc.end.line;
|
|
1096
|
-
lineNum += 1
|
|
1097
|
-
) {
|
|
1098
|
-
commentLineNums.add(lineNum);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
/**
|
|
1103
|
-
* Checks if two members are line-separated.
|
|
1104
|
-
* @param {MemberNode} prevMember The previous member.
|
|
1105
|
-
* @param {MemberNode} member The current member.
|
|
1106
|
-
* @return {boolean}
|
|
1107
|
-
*/
|
|
1108
|
-
function isLineSeparated(prevMember, member) {
|
|
1109
|
-
// Note that there can be comments *inside* members, e.g. `{"foo: /* comment *\/ "bar"}`, but these are ignored when calculating line-separated groups
|
|
1110
|
-
const prevMemberEndLine = prevMember.loc.end.line;
|
|
1111
|
-
const thisStartLine = member.loc.start.line;
|
|
1112
|
-
if (thisStartLine - prevMemberEndLine < 2) {
|
|
1113
|
-
return false;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
for (
|
|
1117
|
-
let lineNum = prevMemberEndLine + 1;
|
|
1118
|
-
lineNum < thisStartLine;
|
|
1119
|
-
lineNum += 1
|
|
1120
|
-
) {
|
|
1121
|
-
if (
|
|
1122
|
-
!commentLineNums.has(lineNum) &&
|
|
1123
|
-
!hasNonWhitespace.test(
|
|
1124
|
-
context.sourceCode.lines[lineNum - 1],
|
|
1125
|
-
)
|
|
1126
|
-
) {
|
|
1127
|
-
return true;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
return false;
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
return {
|
|
1135
|
-
Object(node) {
|
|
1136
|
-
let prevMember;
|
|
1137
|
-
let prevName;
|
|
1138
|
-
|
|
1139
|
-
if (node.members.length < minKeys) {
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
for (const member of node.members) {
|
|
1144
|
-
const thisName = getKey(member);
|
|
1145
|
-
|
|
1146
|
-
if (
|
|
1147
|
-
prevMember &&
|
|
1148
|
-
!isValidOrder(prevName, thisName) &&
|
|
1149
|
-
(!allowLineSeparatedGroups ||
|
|
1150
|
-
!isLineSeparated(prevMember, member))
|
|
1151
|
-
) {
|
|
1152
|
-
context.report({
|
|
1153
|
-
loc: member.name.loc,
|
|
1154
|
-
messageId: "sortKeys",
|
|
1155
|
-
data: {
|
|
1156
|
-
thisName,
|
|
1157
|
-
prevName,
|
|
1158
|
-
direction,
|
|
1159
|
-
sensitivity,
|
|
1160
|
-
sortName,
|
|
1161
|
-
},
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
prevMember = member;
|
|
1166
|
-
prevName = thisName;
|
|
1167
|
-
}
|
|
1168
|
-
},
|
|
1169
|
-
};
|
|
1170
|
-
},
|
|
1171
|
-
};
|
|
1172
|
-
|
|
1173
|
-
/**
|
|
1174
|
-
* @fileoverview Rule to ensure top-level items are either an array or object.
|
|
1175
|
-
* @author Joe Hildebrand
|
|
1176
|
-
*/
|
|
1177
|
-
|
|
1178
|
-
//-----------------------------------------------------------------------------
|
|
1179
|
-
// Type Definitions
|
|
1180
|
-
//-----------------------------------------------------------------------------
|
|
1181
|
-
|
|
1182
|
-
/**
|
|
1183
|
-
*
|
|
1184
|
-
* @typedef {"topLevel"} TopLevelInteropMessageIds
|
|
1185
|
-
* @typedef {JSONRuleDefinition<{ MessageIds: TopLevelInteropMessageIds }>} TopLevelInteropRuleDefinition
|
|
1186
|
-
*/
|
|
1187
|
-
|
|
1188
|
-
//-----------------------------------------------------------------------------
|
|
1189
|
-
// Rule Definition
|
|
1190
|
-
//-----------------------------------------------------------------------------
|
|
1191
|
-
|
|
1192
|
-
/** @type {TopLevelInteropRuleDefinition} */
|
|
1193
|
-
const rule = {
|
|
1194
|
-
meta: {
|
|
1195
|
-
type: "problem",
|
|
1196
|
-
|
|
1197
|
-
docs: {
|
|
1198
|
-
recommended: false,
|
|
1199
|
-
description:
|
|
1200
|
-
"Require the JSON top-level value to be an array or object",
|
|
1201
|
-
url: "https://github.com/eslint/json/tree/main/docs/rules/top-level-interop.md",
|
|
1202
|
-
},
|
|
1203
|
-
|
|
1204
|
-
messages: {
|
|
1205
|
-
topLevel:
|
|
1206
|
-
"Top level item should be array or object, got '{{type}}'.",
|
|
1207
|
-
},
|
|
1208
|
-
},
|
|
1209
|
-
|
|
1210
|
-
create(context) {
|
|
1211
|
-
return {
|
|
1212
|
-
Document(node) {
|
|
1213
|
-
const { type } = node.body;
|
|
1214
|
-
if (type !== "Object" && type !== "Array") {
|
|
1215
|
-
context.report({
|
|
1216
|
-
loc: node.loc,
|
|
1217
|
-
messageId: "topLevel",
|
|
1218
|
-
data: { type },
|
|
1219
|
-
});
|
|
1220
|
-
}
|
|
1221
|
-
},
|
|
1222
|
-
};
|
|
1223
|
-
},
|
|
1224
|
-
};
|
|
1225
|
-
|
|
1226
|
-
var rules = {
|
|
1227
|
-
"no-duplicate-keys": rule$5,
|
|
1228
|
-
"no-empty-keys": rule$4,
|
|
1229
|
-
"no-unnormalized-keys": rule$3,
|
|
1230
|
-
"no-unsafe-values": rule$2,
|
|
1231
|
-
"sort-keys": rule$1,
|
|
1232
|
-
"top-level-interop": rule,
|
|
1233
|
-
};
|
|
1234
|
-
|
|
1235
|
-
/**
|
|
1236
|
-
* @fileoverview JSON plugin.
|
|
1237
|
-
* @author Nicholas C. Zakas
|
|
1238
|
-
*/
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
//-----------------------------------------------------------------------------
|
|
1242
|
-
// Plugin
|
|
1243
|
-
//-----------------------------------------------------------------------------
|
|
1244
|
-
|
|
1245
|
-
const plugin = {
|
|
1246
|
-
meta: {
|
|
1247
|
-
name: "@eslint/json",
|
|
1248
|
-
version: "0.14.0", // x-release-please-version
|
|
1249
|
-
},
|
|
1250
|
-
languages: {
|
|
1251
|
-
json: new JSONLanguage({ mode: "json" }),
|
|
1252
|
-
jsonc: new JSONLanguage({ mode: "jsonc" }),
|
|
1253
|
-
json5: new JSONLanguage({ mode: "json5" }),
|
|
1254
|
-
},
|
|
1255
|
-
rules,
|
|
1256
|
-
configs: {
|
|
1257
|
-
recommended: {
|
|
1258
|
-
plugins: {},
|
|
1259
|
-
rules: rules$1,
|
|
1260
|
-
},
|
|
1261
|
-
},
|
|
1262
|
-
};
|
|
1263
|
-
|
|
1264
|
-
// eslint-disable-next-line no-lone-blocks -- The block syntax { ... } ensures that TypeScript does not get confused about the type of `plugin`.
|
|
1265
|
-
{
|
|
1266
|
-
plugin.configs.recommended.plugins.json = plugin;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
exports.JSONLanguage = JSONLanguage;
|
|
1270
|
-
exports.JSONSourceCode = JSONSourceCode;
|
|
1271
|
-
exports.default = plugin;
|