@cldmv/eslint-plugin-jsonv 1.0.12 → 1.0.13
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 +57 -27
- package/dist/index.d.mts +409 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +429 -63
- package/package.json +21 -5
package/dist/index.mjs
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* - Reports parse errors with accurate source locations
|
|
13
13
|
* - Supports all ES2011-2025 features (JSON5, binary/octal literals, BigInt, numeric separators, etc.)
|
|
14
14
|
* - Detects internal reference errors (circular refs, undefined refs)
|
|
15
|
-
* - Configurable year
|
|
15
|
+
* - Configurable language options: target ES year, parse mode, strict BigInt and octal checks,
|
|
16
|
+
* and internal reference resolution
|
|
16
17
|
* - Exposes a positioned AST (objects, arrays, properties, literals, references, templates),
|
|
17
18
|
* comments and tokens, so rules can select nodes and inline `eslint-disable` comments work
|
|
18
19
|
*
|
|
@@ -38,6 +39,254 @@ import { createRequire } from "node:module";
|
|
|
38
39
|
import { dirname, join } from "node:path";
|
|
39
40
|
import { fileURLToPath } from "node:url";
|
|
40
41
|
|
|
42
|
+
//-----------------------------------------------------------------------------
|
|
43
|
+
// Types
|
|
44
|
+
//
|
|
45
|
+
// The language, its SourceCode and the plugin are typed against @eslint/core's
|
|
46
|
+
// generics (`Language`, `TextSourceCode`, `OkParseResult`, `Plugin`, ...), the same
|
|
47
|
+
// way @eslint/json types its JSON language. `tsc -p .configs/tsconfig.types.json`
|
|
48
|
+
// type-checks this JSDoc (`checkJs`) and emits it as `dist/index.d.mts`.
|
|
49
|
+
//-----------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @import { ConfigObject, File, FileError, FileProblem, Language, OkParseResult, ParseResult, Plugin, RulesConfig, RuleVisitor, SourceLocation, SourceRange } from "@eslint/core";
|
|
53
|
+
* @import { CustomRuleDefinitionType, CustomRuleTypeDefinitions, CustomRuleVisitorWithExit, DirectiveType } from "@eslint/plugin-kit";
|
|
54
|
+
* @import { AstResult, SourceLocation as JsonvParserLocation, TokenType } from "@cldmv/jsonv/parser";
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The ES years the `year` language option accepts, matching @cldmv/jsonv's `ParseOptions["year"]`.
|
|
59
|
+
*
|
|
60
|
+
* @public
|
|
61
|
+
* @typedef {2011 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025} JsonvYear
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The parse modes the `mode` language option accepts, matching @cldmv/jsonv's `ParseOptions["mode"]`:
|
|
66
|
+
* `"jsonv"` (every jsonv feature of the selected year), `"json5"` (JSON5 only) or `"json"` (strict JSON).
|
|
67
|
+
*
|
|
68
|
+
* @public
|
|
69
|
+
* @typedef {"jsonv" | "json5" | "json"} JsonvMode
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Language options for `language: "jsonv/jsonv"`. Each one is forwarded to @cldmv/jsonv's
|
|
74
|
+
* `parseWithOptions()` and `parseToAst()`. Any other key is rejected by `validateLanguageOptions`,
|
|
75
|
+
* so the type is closed.
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
* @typedef {object} JsonvLanguageOptions
|
|
79
|
+
* @property {JsonvYear} [year] The ES year whose jsonv features are allowed. Defaults to `2025`.
|
|
80
|
+
* @property {JsonvMode} [mode] The parse mode. Defaults to `"jsonv"`.
|
|
81
|
+
* @property {boolean} [strictBigInt] When `true`, an integer outside the safe range without an `n` suffix is an error instead of
|
|
82
|
+
* being converted to a BigInt. Defaults to `false`.
|
|
83
|
+
* @property {boolean} [strictOctal] When `true`, a legacy octal literal such as `0755` is an error (`0o755` is still allowed).
|
|
84
|
+
* Defaults to `false`.
|
|
85
|
+
* @property {boolean} [allowInternalReferences] When `false`, internal references are left unresolved, so an undefined or
|
|
86
|
+
* circular reference is no longer reported. Defaults to `true`.
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A `Literal`: string, number, BigInt, boolean, `null`, `Infinity` or `NaN`, with its source text in `raw`.
|
|
91
|
+
*
|
|
92
|
+
* @public
|
|
93
|
+
* @typedef {object} JsonvLiteral
|
|
94
|
+
* @property {"Literal"} type The node type.
|
|
95
|
+
* @property {string | number | bigint | boolean | null} value The evaluated value.
|
|
96
|
+
* @property {string} raw The source text.
|
|
97
|
+
* @property {string} [bigint] The BigInt digits, for a BigInt literal.
|
|
98
|
+
* @property {SourceLocation} loc The location (1-based lines and columns).
|
|
99
|
+
* @property {SourceRange} range The `[start, end)` source offsets.
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* An `Identifier`: an unquoted key, or an internal reference such as `backup: port`.
|
|
104
|
+
*
|
|
105
|
+
* @public
|
|
106
|
+
* @typedef {object} JsonvIdentifier
|
|
107
|
+
* @property {"Identifier"} type The node type.
|
|
108
|
+
* @property {string} name The identifier text.
|
|
109
|
+
* @property {SourceLocation} loc The location (1-based lines and columns).
|
|
110
|
+
* @property {SourceRange} range The `[start, end)` source offsets.
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* An `ObjectExpression`: `{ ... }`.
|
|
115
|
+
*
|
|
116
|
+
* @public
|
|
117
|
+
* @typedef {object} JsonvObjectExpression
|
|
118
|
+
* @property {"ObjectExpression"} type The node type.
|
|
119
|
+
* @property {JsonvProperty[]} properties The properties, in source order.
|
|
120
|
+
* @property {SourceLocation} loc The location (1-based lines and columns).
|
|
121
|
+
* @property {SourceRange} range The `[start, end)` source offsets.
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A `Property`: `key: value`. The key is a `Literal` (quoted or numeric key) or an `Identifier` (unquoted key).
|
|
126
|
+
*
|
|
127
|
+
* @public
|
|
128
|
+
* @typedef {object} JsonvProperty
|
|
129
|
+
* @property {"Property"} type The node type.
|
|
130
|
+
* @property {JsonvLiteral | JsonvIdentifier} key The key.
|
|
131
|
+
* @property {JsonvExpression} value The value.
|
|
132
|
+
* @property {boolean} computed Always `false` in jsonv.
|
|
133
|
+
* @property {SourceLocation} loc The location (1-based lines and columns).
|
|
134
|
+
* @property {SourceRange} range The `[start, end)` source offsets.
|
|
135
|
+
*/
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* An `ArrayExpression`: `[ ... ]`.
|
|
139
|
+
*
|
|
140
|
+
* @public
|
|
141
|
+
* @typedef {object} JsonvArrayExpression
|
|
142
|
+
* @property {"ArrayExpression"} type The node type.
|
|
143
|
+
* @property {JsonvExpression[]} elements The elements, in source order.
|
|
144
|
+
* @property {SourceLocation} loc The location (1-based lines and columns).
|
|
145
|
+
* @property {SourceRange} range The `[start, end)` source offsets.
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A `MemberExpression`: a dotted internal reference such as `server.port`.
|
|
150
|
+
*
|
|
151
|
+
* @public
|
|
152
|
+
* @typedef {object} JsonvMemberExpression
|
|
153
|
+
* @property {"MemberExpression"} type The node type.
|
|
154
|
+
* @property {JsonvExpression} object The object being accessed.
|
|
155
|
+
* @property {JsonvIdentifier} property The accessed property.
|
|
156
|
+
* @property {false} computed Always `false`.
|
|
157
|
+
* @property {SourceLocation} loc The location (1-based lines and columns).
|
|
158
|
+
* @property {SourceRange} range The `[start, end)` source offsets.
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A `TemplateElement`: one string segment of a template literal.
|
|
163
|
+
*
|
|
164
|
+
* @public
|
|
165
|
+
* @typedef {object} JsonvTemplateElement
|
|
166
|
+
* @property {"TemplateElement"} type The node type.
|
|
167
|
+
* @property {{raw: string, cooked: string}} value The raw and cooked segment text.
|
|
168
|
+
* @property {boolean} tail Whether this is the last segment.
|
|
169
|
+
* @property {SourceLocation} loc The location (1-based lines and columns).
|
|
170
|
+
* @property {SourceRange} range The `[start, end)` source offsets.
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A `TemplateLiteral`: a backtick string and its `${...}` interpolations.
|
|
175
|
+
*
|
|
176
|
+
* @public
|
|
177
|
+
* @typedef {object} JsonvTemplateLiteral
|
|
178
|
+
* @property {"TemplateLiteral"} type The node type.
|
|
179
|
+
* @property {JsonvTemplateElement[]} quasis The string segments.
|
|
180
|
+
* @property {JsonvExpression[]} expressions The interpolated expressions.
|
|
181
|
+
* @property {SourceLocation} loc The location (1-based lines and columns).
|
|
182
|
+
* @property {SourceRange} range The `[start, end)` source offsets.
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Any value node.
|
|
187
|
+
*
|
|
188
|
+
* @public
|
|
189
|
+
* @typedef {JsonvLiteral | JsonvObjectExpression | JsonvArrayExpression | JsonvIdentifier | JsonvTemplateLiteral | JsonvMemberExpression} JsonvExpression
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A `Line` (`// ...`) or `Block` (`/* ... *\/`) comment. `value` excludes the delimiters; `loc` and `range` include them.
|
|
194
|
+
*
|
|
195
|
+
* @public
|
|
196
|
+
* @typedef {object} JsonvComment
|
|
197
|
+
* @property {"Line" | "Block"} type The comment kind.
|
|
198
|
+
* @property {string} value The comment text without its delimiters.
|
|
199
|
+
* @property {SourceLocation} loc The location (1-based lines and columns).
|
|
200
|
+
* @property {SourceRange} range The `[start, end)` source offsets.
|
|
201
|
+
*/
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A token (comments and the end-of-file token are not included).
|
|
205
|
+
*
|
|
206
|
+
* @public
|
|
207
|
+
* @typedef {object} JsonvToken
|
|
208
|
+
* @property {`${TokenType}`} type The token type, e.g. `"{"`, `"String"`, `"Identifier"`, `"TemplateHead"`.
|
|
209
|
+
* @property {string | number | bigint | boolean | null} value The token's value.
|
|
210
|
+
* @property {string} raw The source text.
|
|
211
|
+
* @property {SourceLocation} loc The location (1-based lines and columns).
|
|
212
|
+
* @property {SourceRange} range The `[start, end)` source offsets.
|
|
213
|
+
*/
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The `Program` root: `body` is the root value; `comments` and `tokens` are in source order.
|
|
217
|
+
*
|
|
218
|
+
* @public
|
|
219
|
+
* @typedef {object} JsonvProgram
|
|
220
|
+
* @property {"Program"} type The node type.
|
|
221
|
+
* @property {JsonvExpression} body The root value.
|
|
222
|
+
* @property {JsonvComment[]} comments The comments.
|
|
223
|
+
* @property {JsonvToken[]} tokens The tokens, excluding comments.
|
|
224
|
+
* @property {SourceLocation} loc The location of the whole document (1-based lines and columns).
|
|
225
|
+
* @property {SourceRange} range The `[start, end)` source offsets of the whole document.
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Any AST node.
|
|
230
|
+
*
|
|
231
|
+
* @public
|
|
232
|
+
* @typedef {JsonvProgram | JsonvExpression | JsonvProperty | JsonvTemplateElement} JsonvNode
|
|
233
|
+
*/
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Anything with a location: a node, a token or a comment.
|
|
237
|
+
*
|
|
238
|
+
* @public
|
|
239
|
+
* @typedef {JsonvNode | JsonvToken | JsonvComment} JsonvSyntaxElement
|
|
240
|
+
*/
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The jsonv language, typed with @eslint/core's `Language` generics.
|
|
244
|
+
*
|
|
245
|
+
* @public
|
|
246
|
+
* @typedef {Language<{LangOptions: JsonvLanguageOptions, Code: JsonvSourceCode, RootNode: JsonvProgram, Node: JsonvNode}>} JsonvLanguage
|
|
247
|
+
*/
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* A successful `parse()` result. Besides `ast` it carries `jsonvValue`, the evaluated document
|
|
251
|
+
* (references resolved).
|
|
252
|
+
*
|
|
253
|
+
* @public
|
|
254
|
+
* @typedef {OkParseResult<JsonvProgram>} JsonvOkParseResult
|
|
255
|
+
*/
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* A `parse()` result: {@link JsonvOkParseResult}, or a failure carrying `errors`.
|
|
259
|
+
*
|
|
260
|
+
* @public
|
|
261
|
+
* @typedef {ParseResult<JsonvProgram>} JsonvParseResult
|
|
262
|
+
*/
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The visitor a jsonv rule returns: a handler per node type (plus `:exit` variants), each called
|
|
266
|
+
* with the node and its parent.
|
|
267
|
+
*
|
|
268
|
+
* @public
|
|
269
|
+
* @typedef {RuleVisitor & CustomRuleVisitorWithExit<{
|
|
270
|
+
* Program?: (node: JsonvProgram) => void,
|
|
271
|
+
* ObjectExpression?: (node: JsonvObjectExpression, parent?: JsonvNode) => void,
|
|
272
|
+
* Property?: (node: JsonvProperty, parent?: JsonvObjectExpression) => void,
|
|
273
|
+
* ArrayExpression?: (node: JsonvArrayExpression, parent?: JsonvNode) => void,
|
|
274
|
+
* Literal?: (node: JsonvLiteral, parent?: JsonvNode) => void,
|
|
275
|
+
* Identifier?: (node: JsonvIdentifier, parent?: JsonvNode) => void,
|
|
276
|
+
* MemberExpression?: (node: JsonvMemberExpression, parent?: JsonvNode) => void,
|
|
277
|
+
* TemplateLiteral?: (node: JsonvTemplateLiteral, parent?: JsonvNode) => void,
|
|
278
|
+
* TemplateElement?: (node: JsonvTemplateElement, parent?: JsonvTemplateLiteral) => void
|
|
279
|
+
* }>} JsonvRuleVisitor
|
|
280
|
+
*/
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* A rule for jsonv files. `Options` sets `RuleOptions`, `MessageIds` and `ExtRuleDocs`.
|
|
284
|
+
*
|
|
285
|
+
* @public
|
|
286
|
+
* @template {Partial<CustomRuleTypeDefinitions>} [Options={}]
|
|
287
|
+
* @typedef {CustomRuleDefinitionType<{LangOptions: JsonvLanguageOptions, Code: JsonvSourceCode, Visitor: JsonvRuleVisitor, Node: JsonvNode}, Options>} JsonvRuleDefinition
|
|
288
|
+
*/
|
|
289
|
+
|
|
41
290
|
/**
|
|
42
291
|
* Reads this package's own `name` and `version` from its `package.json`.
|
|
43
292
|
*
|
|
@@ -61,14 +310,43 @@ function readPackageMeta() {
|
|
|
61
310
|
}
|
|
62
311
|
|
|
63
312
|
/**
|
|
64
|
-
*
|
|
65
|
-
* forwards to
|
|
66
|
-
* `
|
|
313
|
+
* The default of every language option `jsonvLanguage#parse` reads from `context.languageOptions`
|
|
314
|
+
* and forwards to @cldmv/jsonv's `parseWithOptions()` and `parseToAst()`. The defaults are
|
|
315
|
+
* @cldmv/jsonv's own (`ParseOptions` in its `src/api-types.mts`), except `year`, which jsonv
|
|
316
|
+
* derives from the current date and the plugin pins to the latest supported year.
|
|
317
|
+
*
|
|
318
|
+
* @public
|
|
319
|
+
* @type {Readonly<Required<JsonvLanguageOptions>>}
|
|
320
|
+
*/
|
|
321
|
+
const DEFAULT_LANGUAGE_OPTIONS = Object.freeze({
|
|
322
|
+
year: 2025,
|
|
323
|
+
mode: "jsonv",
|
|
324
|
+
strictBigInt: false,
|
|
325
|
+
strictOctal: false,
|
|
326
|
+
allowInternalReferences: true
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The language options the plugin accepts: exactly the keys of {@link DEFAULT_LANGUAGE_OPTIONS}.
|
|
331
|
+
* Any other key is rejected by `validateLanguageOptions`, since it would silently do nothing.
|
|
67
332
|
*
|
|
68
333
|
* @public
|
|
69
334
|
* @type {Set<string>}
|
|
70
335
|
*/
|
|
71
|
-
const SUPPORTED_LANGUAGE_OPTIONS = new Set(
|
|
336
|
+
const SUPPORTED_LANGUAGE_OPTIONS = new Set(Object.keys(DEFAULT_LANGUAGE_OPTIONS));
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* @cldmv/jsonv parse options the plugin deliberately does not accept, each with the reason
|
|
340
|
+
* `validateLanguageOptions` gives when a config sets it.
|
|
341
|
+
*
|
|
342
|
+
* @public
|
|
343
|
+
* @type {Readonly<Record<string, string>>}
|
|
344
|
+
*/
|
|
345
|
+
const UNSUPPORTED_JSONV_OPTIONS = Object.freeze({
|
|
346
|
+
reviver: "a reviver only transforms the evaluated value, which linting does not use",
|
|
347
|
+
preserveComments: "the AST always carries the comments",
|
|
348
|
+
tolerant: "the plugin controls how parse errors are collected"
|
|
349
|
+
});
|
|
72
350
|
|
|
73
351
|
/**
|
|
74
352
|
* Valid `year` language option values, matching @cldmv/jsonv's `ParseOptions["year"]`
|
|
@@ -79,6 +357,22 @@ const SUPPORTED_LANGUAGE_OPTIONS = new Set(["year", "strictBigInt"]);
|
|
|
79
357
|
*/
|
|
80
358
|
const VALID_YEARS = new Set([2011, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]);
|
|
81
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Valid `mode` language option values, matching @cldmv/jsonv's `ParseOptions["mode"]` union.
|
|
362
|
+
*
|
|
363
|
+
* @public
|
|
364
|
+
* @type {Set<string>}
|
|
365
|
+
*/
|
|
366
|
+
const VALID_MODES = new Set(["jsonv", "json5", "json"]);
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The language options that take a boolean.
|
|
370
|
+
*
|
|
371
|
+
* @public
|
|
372
|
+
* @type {ReadonlyArray<"strictBigInt" | "strictOctal" | "allowInternalReferences">}
|
|
373
|
+
*/
|
|
374
|
+
const BOOLEAN_LANGUAGE_OPTIONS = Object.freeze(["strictBigInt", "strictOctal", "allowInternalReferences"]);
|
|
375
|
+
|
|
82
376
|
/**
|
|
83
377
|
* Child keys of every node type in the AST `jsonvLanguage#parse` produces, in source order
|
|
84
378
|
* where that is defined. The node types and their shapes come from @cldmv/jsonv's own AST
|
|
@@ -138,19 +432,26 @@ const commentParser = new ConfigCommentParser();
|
|
|
138
432
|
* Tests whether a value is a positioned @cldmv/jsonv AST node.
|
|
139
433
|
* @public
|
|
140
434
|
* @param {unknown} value - Any property value of a node.
|
|
141
|
-
* @returns {
|
|
435
|
+
* @returns {value is {type: string, [key: string]: unknown}} `true` for an object with a string `type` and a `loc`.
|
|
142
436
|
* @example
|
|
143
437
|
* isJsonvNode({ type: "Literal", loc: { start: {}, end: {} } }); // true
|
|
144
438
|
*/
|
|
145
439
|
function isJsonvNode(value) {
|
|
146
|
-
return
|
|
440
|
+
return (
|
|
441
|
+
value !== null &&
|
|
442
|
+
typeof value === "object" &&
|
|
443
|
+
"type" in value &&
|
|
444
|
+
typeof value.type === "string" &&
|
|
445
|
+
"loc" in value &&
|
|
446
|
+
typeof value.loc === "object"
|
|
447
|
+
);
|
|
147
448
|
}
|
|
148
449
|
|
|
149
450
|
/**
|
|
150
451
|
* Returns the child keys to visit for a node: its entry in {@link VISITOR_KEYS}, or — for a
|
|
151
452
|
* node type @cldmv/jsonv may add later — every key holding a node or an array of nodes.
|
|
152
453
|
* @public
|
|
153
|
-
* @param {
|
|
454
|
+
* @param {{type: string, [key: string]: unknown}} node - An AST node.
|
|
154
455
|
* @returns {ReadonlyArray<string>} Child property names.
|
|
155
456
|
* @example
|
|
156
457
|
* getChildKeys({ type: "Property", key, value }); // ["key", "value"]
|
|
@@ -170,8 +471,8 @@ function getChildKeys(node) {
|
|
|
170
471
|
* ESLint `loc`/`range` pair for this language. The language declares `lineStart: 1` and
|
|
171
472
|
* `columnStart: 1`, so columns are shifted to 1-based; `range` comes from the offsets.
|
|
172
473
|
* @public
|
|
173
|
-
* @param {
|
|
174
|
-
* @returns {{loc:
|
|
474
|
+
* @param {JsonvParserLocation} loc - A jsonv location.
|
|
475
|
+
* @returns {{loc: SourceLocation, range: SourceRange}} The ESLint location and range.
|
|
175
476
|
* @example
|
|
176
477
|
* toEslintPosition({ start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 3, offset: 3 } });
|
|
177
478
|
* // { loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 4 } }, range: [0, 3] }
|
|
@@ -191,28 +492,36 @@ function toEslintPosition(loc) {
|
|
|
191
492
|
* Tokens are never walked for children: some token types share a name with a node type
|
|
192
493
|
* (`Identifier`, `TemplateLiteral`) but carry no child nodes.
|
|
193
494
|
* @public
|
|
194
|
-
* @
|
|
195
|
-
* @
|
|
495
|
+
* @template {{loc: JsonvParserLocation}} T
|
|
496
|
+
* @param {T} element - A jsonv comment or token.
|
|
497
|
+
* @returns {{[K in keyof T]: K extends "loc" ? SourceLocation : T[K]} & {range: SourceRange}} The converted copy.
|
|
196
498
|
* @example
|
|
197
499
|
* convertLeaf(parseToAst("// note\n1").comments[0]);
|
|
198
500
|
*/
|
|
199
501
|
function convertLeaf(element) {
|
|
200
|
-
|
|
502
|
+
// TypeScript types a spread of a generic as an intersection (`T & {loc, range}`), which keeps
|
|
503
|
+
// the jsonv `loc` type alongside the ESLint one; the cast states the actual replacement.
|
|
504
|
+
return /** @type {{[K in keyof T]: K extends "loc" ? SourceLocation : T[K]} & {range: SourceRange}} */ ({
|
|
505
|
+
...element,
|
|
506
|
+
...toEslintPosition(element.loc)
|
|
507
|
+
});
|
|
201
508
|
}
|
|
202
509
|
|
|
203
510
|
/**
|
|
204
511
|
* Deep-copies a jsonv AST node with ESLint positions: every node gets a 1-based `loc` and a
|
|
205
512
|
* `range`, and child nodes are converted recursively.
|
|
206
513
|
* @public
|
|
207
|
-
* @param {
|
|
208
|
-
* @returns {
|
|
514
|
+
* @param {{type: string, loc: JsonvParserLocation, [key: string]: unknown}} node - A jsonv AST node.
|
|
515
|
+
* @returns {{type: string, [key: string]: unknown}} The converted copy.
|
|
209
516
|
* @example
|
|
210
517
|
* convertNode(parseToAst("{ a: 1 }").program.body);
|
|
211
518
|
*/
|
|
212
519
|
function convertNode(node) {
|
|
520
|
+
/** @type {{type: string, [key: string]: unknown}} */
|
|
213
521
|
const converted = convertLeaf(node);
|
|
214
522
|
for (const key of getChildKeys(node)) {
|
|
215
|
-
|
|
523
|
+
// getChildKeys() only returns keys that hold a node or an array of nodes.
|
|
524
|
+
const child = /** @type {Parameters<typeof convertNode>[0] | Parameters<typeof convertNode>[0][]} */ (node[key]);
|
|
216
525
|
converted[key] = Array.isArray(child) ? child.map(convertNode) : convertNode(child);
|
|
217
526
|
}
|
|
218
527
|
return converted;
|
|
@@ -222,14 +531,19 @@ function convertNode(node) {
|
|
|
222
531
|
* Builds the ESLint AST for a document from @cldmv/jsonv's `parseToAst()` result: the
|
|
223
532
|
* `Program` root (whose `body` is the root value) with positioned `comments` and `tokens`.
|
|
224
533
|
* @public
|
|
225
|
-
* @param {
|
|
226
|
-
* @returns {
|
|
534
|
+
* @param {AstResult} astResult - The `parseToAst()` result.
|
|
535
|
+
* @returns {JsonvProgram} The `Program` node.
|
|
227
536
|
* @example
|
|
228
537
|
* buildProgram(parseToAst("// note\n{ a: 1 }"));
|
|
229
538
|
*/
|
|
230
539
|
function buildProgram({ program, comments, tokens }) {
|
|
540
|
+
// @cldmv/jsonv positions every node it produces (`ASTNode#loc` is only optional in its type), and
|
|
541
|
+
// convertNode() keeps each node's shape while swapping in ESLint positions, so the copy is a
|
|
542
|
+
// JsonvProgram. The generic, runtime-keyed walk cannot express that per node type, hence the casts.
|
|
543
|
+
const rawProgram = /** @type {Parameters<typeof convertNode>[0]} */ (/** @type {unknown} */ (program));
|
|
544
|
+
const root = /** @type {Omit<JsonvProgram, "comments" | "tokens">} */ (convertNode(rawProgram));
|
|
231
545
|
return {
|
|
232
|
-
...
|
|
546
|
+
...root,
|
|
233
547
|
comments: comments.map(convertLeaf),
|
|
234
548
|
tokens: tokens.map(convertLeaf)
|
|
235
549
|
};
|
|
@@ -242,6 +556,7 @@ function buildProgram({ program, comments, tokens }) {
|
|
|
242
556
|
* directives and `/* eslint rule: ... *\/` config comments).
|
|
243
557
|
*
|
|
244
558
|
* @public
|
|
559
|
+
* @extends {TextSourceCodeBase<{LangOptions: JsonvLanguageOptions, RootNode: JsonvProgram, SyntaxElementWithLoc: JsonvSyntaxElement, ConfigNode: JsonvComment}>}
|
|
245
560
|
*/
|
|
246
561
|
class JsonvSourceCode extends TextSourceCodeBase {
|
|
247
562
|
/**
|
|
@@ -252,13 +567,13 @@ class JsonvSourceCode extends TextSourceCodeBase {
|
|
|
252
567
|
|
|
253
568
|
/**
|
|
254
569
|
* Parent of every non-root node.
|
|
255
|
-
* @type {WeakMap<
|
|
570
|
+
* @type {WeakMap<object, JsonvNode>}
|
|
256
571
|
*/
|
|
257
572
|
#parents = new WeakMap();
|
|
258
573
|
|
|
259
574
|
/**
|
|
260
575
|
* Cached inline configuration comments.
|
|
261
|
-
* @type {
|
|
576
|
+
* @type {JsonvComment[]|undefined}
|
|
262
577
|
*/
|
|
263
578
|
#inlineConfigComments;
|
|
264
579
|
|
|
@@ -266,13 +581,13 @@ class JsonvSourceCode extends TextSourceCodeBase {
|
|
|
266
581
|
* Creates a SourceCode object.
|
|
267
582
|
* @param {Object} options - Options.
|
|
268
583
|
* @param {string} options.text - The source text.
|
|
269
|
-
* @param {
|
|
584
|
+
* @param {JsonvProgram} options.ast - The `Program` node built by `jsonvLanguage#parse`.
|
|
270
585
|
*/
|
|
271
586
|
constructor({ text, ast }) {
|
|
272
587
|
super({ text, ast, lineEndingPattern: LINE_ENDING_PATTERN });
|
|
273
|
-
/** @type {
|
|
588
|
+
/** @type {JsonvComment[]} Comments in source order. */
|
|
274
589
|
this.comments = ast.comments;
|
|
275
|
-
/** @type {
|
|
590
|
+
/** @type {JsonvToken[]} Tokens in source order, excluding comments. */
|
|
276
591
|
this.tokens = ast.tokens;
|
|
277
592
|
/** @type {boolean} Whether the text starts with a byte order mark. */
|
|
278
593
|
this.hasBOM = text.charCodeAt(0) === 0xfeff;
|
|
@@ -293,12 +608,18 @@ class JsonvSourceCode extends TextSourceCodeBase {
|
|
|
293
608
|
*/
|
|
294
609
|
#ensureTraversal() {
|
|
295
610
|
if (!this.#steps) {
|
|
611
|
+
/** @type {VisitNodeStep[]} */
|
|
296
612
|
const steps = [];
|
|
613
|
+
/**
|
|
614
|
+
* @param {JsonvNode} node - The node to visit.
|
|
615
|
+
* @param {JsonvNode|undefined} parent - Its parent (`undefined` for the root).
|
|
616
|
+
*/
|
|
297
617
|
const visit = (node, parent) => {
|
|
298
618
|
if (parent) this.#parents.set(node, parent);
|
|
299
619
|
steps.push(new VisitNodeStep({ target: node, phase: 1, args: [node, parent] }));
|
|
300
620
|
for (const key of getChildKeys(node)) {
|
|
301
|
-
|
|
621
|
+
// getChildKeys() only returns keys that hold a node or an array of nodes.
|
|
622
|
+
const child = /** @type {JsonvNode | JsonvNode[]} */ (/** @type {{type: string, [key: string]: unknown}} */ (node)[key]);
|
|
302
623
|
for (const item of Array.isArray(child) ? child : [child]) visit(item, node);
|
|
303
624
|
}
|
|
304
625
|
steps.push(new VisitNodeStep({ target: node, phase: 2, args: [node, parent] }));
|
|
@@ -312,7 +633,7 @@ class JsonvSourceCode extends TextSourceCodeBase {
|
|
|
312
633
|
/**
|
|
313
634
|
* Returns the enter/exit visit steps for every node, depth first in source order.
|
|
314
635
|
* @public
|
|
315
|
-
* @returns {
|
|
636
|
+
* @returns {IterableIterator<VisitNodeStep>} The traversal steps.
|
|
316
637
|
*/
|
|
317
638
|
traverse() {
|
|
318
639
|
return this.#ensureTraversal().values();
|
|
@@ -321,8 +642,8 @@ class JsonvSourceCode extends TextSourceCodeBase {
|
|
|
321
642
|
/**
|
|
322
643
|
* Returns the parent of a node, or `undefined` for the `Program` root.
|
|
323
644
|
* @public
|
|
324
|
-
* @param {
|
|
325
|
-
* @returns {
|
|
645
|
+
* @param {JsonvSyntaxElement} node - An AST node.
|
|
646
|
+
* @returns {JsonvNode|undefined} The parent node.
|
|
326
647
|
*/
|
|
327
648
|
getParent(node) {
|
|
328
649
|
this.#ensureTraversal();
|
|
@@ -333,7 +654,7 @@ class JsonvSourceCode extends TextSourceCodeBase {
|
|
|
333
654
|
* Returns the comments that hold inline configuration (`eslint`, `eslint-enable`,
|
|
334
655
|
* `eslint-disable`, `eslint-disable-line`, `eslint-disable-next-line`).
|
|
335
656
|
* @public
|
|
336
|
-
* @returns {
|
|
657
|
+
* @returns {JsonvComment[]} The inline configuration comments.
|
|
337
658
|
*/
|
|
338
659
|
getInlineConfigNodes() {
|
|
339
660
|
if (!this.#inlineConfigComments) {
|
|
@@ -346,14 +667,20 @@ class JsonvSourceCode extends TextSourceCodeBase {
|
|
|
346
667
|
* Returns the `eslint-disable*` / `eslint-enable` directives, plus problems for malformed
|
|
347
668
|
* ones (an `eslint-disable-line` spanning several lines).
|
|
348
669
|
* @public
|
|
349
|
-
* @returns {{problems:
|
|
670
|
+
* @returns {{problems: FileProblem[], directives: Directive[]}} Directives and problems.
|
|
350
671
|
*/
|
|
351
672
|
getDisableDirectives() {
|
|
673
|
+
/** @type {FileProblem[]} */
|
|
352
674
|
const problems = [];
|
|
675
|
+
/** @type {Directive[]} */
|
|
353
676
|
const directives = [];
|
|
354
677
|
|
|
355
678
|
for (const comment of this.getInlineConfigNodes()) {
|
|
356
|
-
|
|
679
|
+
// parseDirective() returns undefined only for text that doesn't start with a directive label;
|
|
680
|
+
// every comment here matched INLINE_CONFIG, which requires one.
|
|
681
|
+
const { label, value, justification } = /** @type {NonNullable<ReturnType<ConfigCommentParser["parseDirective"]>>} */ (
|
|
682
|
+
commentParser.parseDirective(comment.value)
|
|
683
|
+
);
|
|
357
684
|
|
|
358
685
|
if (label === "eslint-disable-line" && comment.loc.start.line !== comment.loc.end.line) {
|
|
359
686
|
problems.push({ ruleId: null, message: `${label} comment should not span multiple lines.`, loc: comment.loc });
|
|
@@ -361,7 +688,10 @@ class JsonvSourceCode extends TextSourceCodeBase {
|
|
|
361
688
|
}
|
|
362
689
|
|
|
363
690
|
if (label !== "eslint") {
|
|
364
|
-
|
|
691
|
+
// The comment matched INLINE_CONFIG, so any label other than "eslint" is an
|
|
692
|
+
// `eslint-disable*` / `eslint-enable` label, i.e. a DirectiveType once unprefixed.
|
|
693
|
+
const type = /** @type {DirectiveType} */ (label.slice("eslint-".length));
|
|
694
|
+
directives.push(new Directive({ type, node: comment, value, justification }));
|
|
365
695
|
}
|
|
366
696
|
}
|
|
367
697
|
|
|
@@ -372,14 +702,19 @@ class JsonvSourceCode extends TextSourceCodeBase {
|
|
|
372
702
|
* Returns the rule configurations from `/* eslint rule: ... *\/` comments, plus problems for
|
|
373
703
|
* ones that fail to parse.
|
|
374
704
|
* @public
|
|
375
|
-
* @returns {{problems:
|
|
705
|
+
* @returns {{problems: FileProblem[], configs: Array<{config: {rules: RulesConfig}, loc: SourceLocation}>}} Configs and problems.
|
|
376
706
|
*/
|
|
377
707
|
applyInlineConfig() {
|
|
708
|
+
/** @type {FileProblem[]} */
|
|
378
709
|
const problems = [];
|
|
710
|
+
/** @type {Array<{config: {rules: RulesConfig}, loc: SourceLocation}>} */
|
|
379
711
|
const configs = [];
|
|
380
712
|
|
|
381
713
|
for (const comment of this.getInlineConfigNodes()) {
|
|
382
|
-
|
|
714
|
+
// Defined for the same reason as in getDisableDirectives().
|
|
715
|
+
const { label, value } = /** @type {NonNullable<ReturnType<ConfigCommentParser["parseDirective"]>>} */ (
|
|
716
|
+
commentParser.parseDirective(comment.value)
|
|
717
|
+
);
|
|
383
718
|
if (label !== "eslint") continue;
|
|
384
719
|
|
|
385
720
|
const parseResult = commentParser.parseJSONLikeConfig(value);
|
|
@@ -398,7 +733,7 @@ class JsonvSourceCode extends TextSourceCodeBase {
|
|
|
398
733
|
* Language definition for jsonv files
|
|
399
734
|
*
|
|
400
735
|
* @public
|
|
401
|
-
* @type {
|
|
736
|
+
* @type {JsonvLanguage}
|
|
402
737
|
*/
|
|
403
738
|
const jsonvLanguage = {
|
|
404
739
|
/** File type - must be "text" for text-based languages */
|
|
@@ -409,28 +744,38 @@ const jsonvLanguage = {
|
|
|
409
744
|
columnStart: 1,
|
|
410
745
|
/** Property name for node types */
|
|
411
746
|
nodeTypeKey: "type",
|
|
412
|
-
/**
|
|
413
|
-
|
|
747
|
+
/**
|
|
748
|
+
* Child keys of each AST node type, used for selector matching. `Language#visitorKeys` is typed
|
|
749
|
+
* with mutable arrays; ESLint only reads them, and VISITOR_KEYS stays frozen at runtime.
|
|
750
|
+
*/
|
|
751
|
+
visitorKeys: /** @type {Record<string, string[]>} */ (VISITOR_KEYS),
|
|
414
752
|
|
|
415
753
|
/**
|
|
416
754
|
* Parse jsonv source code - ESLint Language API.
|
|
417
755
|
*
|
|
418
756
|
* The file is first parsed and evaluated with `parseWithOptions` — that is what reports
|
|
419
|
-
* syntax errors, year-gated features, `strictBigInt`
|
|
420
|
-
* internal references
|
|
421
|
-
* with
|
|
757
|
+
* syntax errors, year-gated features, `mode`, `strictBigInt` and `strictOctal` violations
|
|
758
|
+
* and unresolved/circular internal references (unless `allowInternalReferences` is `false`),
|
|
759
|
+
* each with its source position. A file that passes is then parsed with `parseToAst` to
|
|
760
|
+
* build the positioned AST, comments and tokens ESLint traverses. Both calls receive every
|
|
761
|
+
* language option, with the defaults filled in.
|
|
422
762
|
*
|
|
423
|
-
* @param {
|
|
424
|
-
* @param {
|
|
425
|
-
* @returns {
|
|
763
|
+
* @param {File} file - File object with body property containing source text
|
|
764
|
+
* @param {{languageOptions: JsonvLanguageOptions}} context - Context with languageOptions
|
|
765
|
+
* @returns {JsonvParseResult} Parse result {ok, ast, jsonvValue} or {ok: false, errors}
|
|
426
766
|
*/
|
|
427
767
|
parse(file, context) {
|
|
428
|
-
|
|
768
|
+
// `fileType: "text"` means ESLint reads the file as text, so `body` is a string.
|
|
769
|
+
const text = /** @type {string} */ (file.body);
|
|
770
|
+
/** @type {JsonvLanguageOptions} */
|
|
429
771
|
const options = context.languageOptions || {};
|
|
430
772
|
const parseOptions = {
|
|
431
|
-
year
|
|
432
|
-
|
|
433
|
-
mode:
|
|
773
|
+
// `||`, not `??`: a falsy year (e.g. `0` from a direct `parse()` call) has always meant the default.
|
|
774
|
+
year: options.year || DEFAULT_LANGUAGE_OPTIONS.year,
|
|
775
|
+
mode: options.mode ?? DEFAULT_LANGUAGE_OPTIONS.mode,
|
|
776
|
+
strictBigInt: options.strictBigInt ?? DEFAULT_LANGUAGE_OPTIONS.strictBigInt,
|
|
777
|
+
strictOctal: options.strictOctal ?? DEFAULT_LANGUAGE_OPTIONS.strictOctal,
|
|
778
|
+
allowInternalReferences: options.allowInternalReferences ?? DEFAULT_LANGUAGE_OPTIONS.allowInternalReferences
|
|
434
779
|
};
|
|
435
780
|
|
|
436
781
|
try {
|
|
@@ -454,9 +799,18 @@ const jsonvLanguage = {
|
|
|
454
799
|
// nesting) has no position — fall back to line 1, column 1.
|
|
455
800
|
const hasPosition = error instanceof JsonvSyntaxError || error instanceof JsonvReferenceError;
|
|
456
801
|
|
|
802
|
+
// Everything the parser and buildProgram() throw is an Error instance (the RangeError
|
|
803
|
+
// above included), hence the cast in the fallback branch.
|
|
804
|
+
/** @type {FileError} */
|
|
457
805
|
const errorInfo = hasPosition
|
|
458
|
-
? {
|
|
459
|
-
|
|
806
|
+
? {
|
|
807
|
+
message: error.message,
|
|
808
|
+
line: error.line,
|
|
809
|
+
column: error.column + 1,
|
|
810
|
+
endLine: error.loc.end.line,
|
|
811
|
+
endColumn: error.loc.end.column + 1
|
|
812
|
+
}
|
|
813
|
+
: { message: /** @type {Error} */ (error).message, line: 1, column: 1 };
|
|
460
814
|
|
|
461
815
|
return {
|
|
462
816
|
ok: false,
|
|
@@ -469,8 +823,8 @@ const jsonvLanguage = {
|
|
|
469
823
|
* Create the SourceCode object ESLint hands to rules
|
|
470
824
|
*
|
|
471
825
|
* @public
|
|
472
|
-
* @param {
|
|
473
|
-
* @param {
|
|
826
|
+
* @param {File} file - The file object
|
|
827
|
+
* @param {JsonvOkParseResult} parseResult - The successful result from parse()
|
|
474
828
|
* @returns {JsonvSourceCode} SourceCode object
|
|
475
829
|
*/
|
|
476
830
|
createSourceCode(file, parseResult) {
|
|
@@ -485,16 +839,21 @@ const jsonvLanguage = {
|
|
|
485
839
|
* reflects the effective options that will reach `parse()`.
|
|
486
840
|
*
|
|
487
841
|
* @public
|
|
488
|
-
* @param {
|
|
842
|
+
* @param {JsonvLanguageOptions} languageOptions - The (already-merged) language options to validate
|
|
489
843
|
* @returns {void}
|
|
490
844
|
* @throws {TypeError} When an unsupported key is present, or a supported key holds an invalid value
|
|
491
845
|
*/
|
|
492
846
|
validateLanguageOptions(languageOptions) {
|
|
493
847
|
const options = languageOptions ?? {};
|
|
494
848
|
|
|
849
|
+
const supported = `Supported options are: ${[...SUPPORTED_LANGUAGE_OPTIONS].join(", ")}.`;
|
|
850
|
+
|
|
495
851
|
for (const key of Object.keys(options)) {
|
|
852
|
+
if (Object.hasOwn(UNSUPPORTED_JSONV_OPTIONS, key)) {
|
|
853
|
+
throw new TypeError(`Language option "${key}" is not supported: ${UNSUPPORTED_JSONV_OPTIONS[key]}. ${supported}`);
|
|
854
|
+
}
|
|
496
855
|
if (!SUPPORTED_LANGUAGE_OPTIONS.has(key)) {
|
|
497
|
-
throw new TypeError(`Unknown language option "${key}".
|
|
856
|
+
throw new TypeError(`Unknown language option "${key}". ${supported}`);
|
|
498
857
|
}
|
|
499
858
|
}
|
|
500
859
|
|
|
@@ -504,8 +863,16 @@ const jsonvLanguage = {
|
|
|
504
863
|
);
|
|
505
864
|
}
|
|
506
865
|
|
|
507
|
-
if (options.
|
|
508
|
-
throw new TypeError(
|
|
866
|
+
if (options.mode !== undefined && !VALID_MODES.has(options.mode)) {
|
|
867
|
+
throw new TypeError(
|
|
868
|
+
`Invalid "mode" language option: ${JSON.stringify(options.mode)}. Supported modes are: ${[...VALID_MODES].join(", ")}.`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
for (const key of BOOLEAN_LANGUAGE_OPTIONS) {
|
|
873
|
+
if (options[key] !== undefined && typeof options[key] !== "boolean") {
|
|
874
|
+
throw new TypeError(`Invalid "${key}" language option: expected a boolean, got ${typeof options[key]}.`);
|
|
875
|
+
}
|
|
509
876
|
}
|
|
510
877
|
},
|
|
511
878
|
|
|
@@ -516,12 +883,9 @@ const jsonvLanguage = {
|
|
|
516
883
|
* calling `validateLanguageOptions` and `parse()`.
|
|
517
884
|
*
|
|
518
885
|
* @public
|
|
519
|
-
* @type {
|
|
886
|
+
* @type {JsonvLanguageOptions}
|
|
520
887
|
*/
|
|
521
|
-
defaultLanguageOptions: {
|
|
522
|
-
year: 2025,
|
|
523
|
-
strictBigInt: false
|
|
524
|
-
}
|
|
888
|
+
defaultLanguageOptions: { ...DEFAULT_LANGUAGE_OPTIONS }
|
|
525
889
|
};
|
|
526
890
|
|
|
527
891
|
/**
|
|
@@ -532,7 +896,7 @@ const jsonvLanguage = {
|
|
|
532
896
|
* `language` such as `jsonv/jsonv` never consults it, so setting it here was inert.
|
|
533
897
|
*
|
|
534
898
|
* @public
|
|
535
|
-
* @
|
|
899
|
+
* @satisfies {ConfigObject}
|
|
536
900
|
*/
|
|
537
901
|
const recommendedConfig = {
|
|
538
902
|
rules: {
|
|
@@ -542,10 +906,11 @@ const recommendedConfig = {
|
|
|
542
906
|
};
|
|
543
907
|
|
|
544
908
|
/**
|
|
545
|
-
* ESLint plugin export
|
|
909
|
+
* ESLint plugin export. `@satisfies` checks it against @eslint/core's `Plugin` (which ESLint's
|
|
910
|
+
* own `ESLint.Plugin` is) while keeping its literal keys (`languages.jsonv`, `configs.recommended`).
|
|
546
911
|
*
|
|
547
912
|
* @public
|
|
548
|
-
* @
|
|
913
|
+
* @satisfies {Plugin}
|
|
549
914
|
*/
|
|
550
915
|
const plugin = {
|
|
551
916
|
meta: readPackageMeta(),
|
|
@@ -566,3 +931,4 @@ const plugin = {
|
|
|
566
931
|
};
|
|
567
932
|
|
|
568
933
|
export default plugin;
|
|
934
|
+
export { JsonvSourceCode };
|