@cldmv/eslint-plugin-jsonv 1.0.12 → 1.0.14

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