@cldmv/eslint-plugin-jsonv 1.0.11 → 1.0.12

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 CHANGED
@@ -6,6 +6,22 @@
6
6
 
7
7
  ESLint plugin for validating `.jsonv` files using the [@cldmv/jsonv](https://github.com/CLDMV/jsonv) parser.
8
8
 
9
+ ## ✨ What's New
10
+
11
+ ### Latest: v1.0.12 (September 2026)
12
+
13
+ - **A real ESLint AST for `.jsonv` files** — the `jsonv/jsonv` language now builds a positioned AST from `@cldmv/jsonv`'s `parseToAst()` instead of an empty `Program`. Rules can select objects, arrays, properties, literals, references and templates; reports land on the node they target instead of `1:0`; and inline `eslint-disable` / `eslint-enable` / `eslint-disable-next-line` comments now work. Requires `@cldmv/jsonv` 1.1.0 or later (#26).
14
+ - [View full v1.0.12 Changelog](https://github.com/CLDMV/jsonv-eslint-plugin-jsonv/blob/master/docs/changelog/v1/v1.0.12.md)
15
+
16
+ ### Recent Releases
17
+
18
+ - **v1.0.11** (September 2026) — language options are validated, parse errors report at their real position, and the plugin gained a vitest suite with a CI coverage badge (#16, #17, #18, #23, #24) ([Changelog](https://github.com/CLDMV/jsonv-eslint-plugin-jsonv/blob/master/docs/changelog/v1/v1.0.11.md))
19
+ - **v1.0.10** (September 2026) — the README links back to the jsonv repo (#15) ([Changelog](https://github.com/CLDMV/jsonv-eslint-plugin-jsonv/blob/master/docs/changelog/v1/v1.0.10.md))
20
+ - **v1.0.9** (September 2026) — corrected package name, badge row and license badges (#12) ([Changelog](https://github.com/CLDMV/jsonv-eslint-plugin-jsonv/blob/master/docs/changelog/v1/v1.0.9.md))
21
+ - **v1.0.8** (September 2026) — Node CI matrix updated for vitest 5 (max Node 26, min Node 22.12.0) (#10) ([Changelog](https://github.com/CLDMV/jsonv-eslint-plugin-jsonv/blob/master/docs/changelog/v1/v1.0.8.md))
22
+
23
+ 📚 **For complete version history, see [docs/changelog/](https://github.com/CLDMV/jsonv-eslint-plugin-jsonv/tree/master/docs/changelog/) and the [GitHub Releases](https://github.com/CLDMV/jsonv-eslint-plugin-jsonv/releases).**
24
+
9
25
  ## Features
10
26
 
11
27
  - **Syntax Validation**: Validates jsonv syntax using the actual jsonv parser
@@ -75,6 +91,39 @@ The parser supports the following options:
75
91
  }
76
92
  ```
77
93
 
94
+ ### Inline Configuration
95
+
96
+ The usual ESLint comments work inside `.jsonv` files: `// eslint-disable`, `/* eslint-disable <rule> */`, `/* eslint-enable */`, `// eslint-disable-line`, `// eslint-disable-next-line`, and `/* eslint <rule>: "off" */` rule configuration.
97
+
98
+ ### Writing Rules
99
+
100
+ The `jsonv/jsonv` language exposes the document as an AST built from `@cldmv/jsonv`'s `parseToAst()`. Every node has a `loc` (1-based lines and columns) and a `range`, so reports on a node land on that node, and rules can use node types and selectors:
101
+
102
+ | Node type | Children | Notes |
103
+ |---|---|---|
104
+ | `Program` | `body` | The document; `body` is the root value. `comments` and `tokens` hang off it. |
105
+ | `ObjectExpression` | `properties` | `{ ... }` |
106
+ | `Property` | `key`, `value` | `key` is a `Literal` (quoted or numeric key) or an `Identifier` (unquoted key). |
107
+ | `ArrayExpression` | `elements` | `[ ... ]` |
108
+ | `Literal` | — | Strings, numbers, BigInt (`bigint` holds the digits), booleans, `null`, `Infinity`, `NaN`; `raw` is the source text. |
109
+ | `Identifier` | — | An unquoted key, or an internal reference such as `backup: port`. |
110
+ | `MemberExpression` | `object`, `property` | A dotted internal reference such as `server.port`. |
111
+ | `TemplateLiteral` | `quasis`, `expressions` | A backtick string with `${...}` interpolation (a plain backtick string is a `Literal`). |
112
+ | `TemplateElement` | — | A literal segment of a template. |
113
+
114
+ ```javascript
115
+ // Report every string value containing "TODO", on the string itself
116
+ create(context) {
117
+ return {
118
+ "Property > Literal.value"(node) {
119
+ if (typeof node.value === "string" && node.value.includes("TODO")) {
120
+ context.report({ node, message: "Unexpected TODO." });
121
+ }
122
+ }
123
+ };
124
+ }
125
+ ```
126
+
78
127
  ## Supported Features
79
128
 
80
129
  ### ES2011 (JSON5 Base)
package/dist/index.d.mts CHANGED
@@ -1,4 +1,34 @@
1
- export default plugin;
1
+ /**
2
+ * @fileoverview ESLint plugin for @cldmv/jsonv files
3
+ * @module eslint-plugin-jsonv
4
+ * @public
5
+ *
6
+ * @description
7
+ * Custom ESLint plugin for validating .jsonv files using the @cldmv/jsonv parser.
8
+ * Provides language definition, parser, and recommended rules for jsonv files.
9
+ *
10
+ * Features:
11
+ * - Validates jsonv syntax using the actual jsonv parser
12
+ * - Reports parse errors with accurate source locations
13
+ * - Supports all ES2011-2025 features (JSON5, binary/octal literals, BigInt, numeric separators, etc.)
14
+ * - Detects internal reference errors (circular refs, undefined refs)
15
+ * - Configurable year-based feature detection
16
+ * - Exposes a positioned AST (objects, arrays, properties, literals, references, templates),
17
+ * comments and tokens, so rules can select nodes and inline `eslint-disable` comments work
18
+ *
19
+ * @example
20
+ * // In eslint.config.mjs
21
+ * import jsonv from 'eslint-plugin-jsonv';
22
+ *
23
+ * export default [
24
+ * {
25
+ * files: ["**\/*.jsonv"],
26
+ * plugins: { jsonv },
27
+ * language: "jsonv/jsonv",
28
+ * extends: ["jsonv/recommended"]
29
+ * }
30
+ * ];
31
+ */
2
32
  /**
3
33
  * ESLint plugin export
4
34
  *
@@ -6,4 +36,5 @@ export default plugin;
6
36
  * @type {Object}
7
37
  */
8
38
  declare const plugin: Object;
39
+ export default plugin;
9
40
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../index.mjs"],"names":[],"mappings":";AAsRA;;;;;GAKG;AACH,sBAFU,MAAM,CAkBd"}
1
+ {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../index.mjs"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAigBH;;;;;GAKG;AACH,QAAA,MAAM,MAAM,EAFF,MAkBT,CAAC;eAEa,MAAM"}
package/dist/index.mjs CHANGED
@@ -13,6 +13,8 @@
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
15
  * - Configurable year-based feature detection
16
+ * - Exposes a positioned AST (objects, arrays, properties, literals, references, templates),
17
+ * comments and tokens, so rules can select nodes and inline `eslint-disable` comments work
16
18
  *
17
19
  * @example
18
20
  * // In eslint.config.mjs
@@ -28,8 +30,8 @@
28
30
  * ];
29
31
  */
30
32
 
31
- // Use proper package import - package is copied to node_modules during build
32
- import { JsonvSyntaxError, parseWithOptions } from "@cldmv/jsonv/parser";
33
+ import { JsonvReferenceError, JsonvSyntaxError, parseToAst, parseWithOptions } from "@cldmv/jsonv/parser";
34
+ import { ConfigCommentParser, Directive, TextSourceCodeBase, VisitNodeStep } from "@eslint/plugin-kit";
33
35
 
34
36
  import { existsSync } from "node:fs";
35
37
  import { createRequire } from "node:module";
@@ -77,6 +79,321 @@ const SUPPORTED_LANGUAGE_OPTIONS = new Set(["year", "strictBigInt"]);
77
79
  */
78
80
  const VALID_YEARS = new Set([2011, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]);
79
81
 
82
+ /**
83
+ * Child keys of every node type in the AST `jsonvLanguage#parse` produces, in source order
84
+ * where that is defined. The node types and their shapes come from @cldmv/jsonv's own AST
85
+ * (`@cldmv/jsonv/parser`'s `Program`/`Expression` types); the plugin only re-positions them.
86
+ *
87
+ * - `Program` — the document; `body` is the single root value.
88
+ * - `ObjectExpression` — `{ ... }`, with `properties` (`Property` nodes).
89
+ * - `Property` — `key: value`; `key` is a `Literal` (quoted or numeric key) or an `Identifier` (unquoted key).
90
+ * - `ArrayExpression` — `[ ... ]`, with `elements`.
91
+ * - `Literal` — string, number, BigInt, boolean, null, `Infinity`, `NaN` (`raw` holds the source text).
92
+ * - `Identifier` — an unquoted key, or an internal reference such as `backup: port`.
93
+ * - `MemberExpression` — a dotted internal reference such as `server.port`.
94
+ * - `TemplateLiteral` / `TemplateElement` — backtick strings and their `${...}` interpolations.
95
+ *
96
+ * @public
97
+ * @type {Readonly<Record<string, ReadonlyArray<string>>>}
98
+ */
99
+ const VISITOR_KEYS = Object.freeze({
100
+ Program: Object.freeze(["body"]),
101
+ ObjectExpression: Object.freeze(["properties"]),
102
+ Property: Object.freeze(["key", "value"]),
103
+ ArrayExpression: Object.freeze(["elements"]),
104
+ Literal: Object.freeze([]),
105
+ Identifier: Object.freeze([]),
106
+ MemberExpression: Object.freeze(["object", "property"]),
107
+ TemplateLiteral: Object.freeze(["quasis", "expressions"]),
108
+ TemplateElement: Object.freeze([])
109
+ });
110
+
111
+ /**
112
+ * Line terminators, matching how @cldmv/jsonv advances its line counter (`\r\n` as one
113
+ * break, plus lone `\r`, `\n`, U+2028 and U+2029), so `sourceCode.lines` and
114
+ * `getLocFromIndex()` agree with the node positions.
115
+ *
116
+ * @public
117
+ * @type {RegExp}
118
+ */
119
+ const LINE_ENDING_PATTERN = /\r\n|[\r\n\u2028\u2029]/u;
120
+
121
+ /**
122
+ * Matches the comment bodies ESLint treats as inline configuration (same pattern as @eslint/json).
123
+ *
124
+ * @public
125
+ * @type {RegExp}
126
+ */
127
+ const INLINE_CONFIG = /^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u;
128
+
129
+ /**
130
+ * Shared parser for `eslint-*` directive comments.
131
+ *
132
+ * @public
133
+ * @type {ConfigCommentParser}
134
+ */
135
+ const commentParser = new ConfigCommentParser();
136
+
137
+ /**
138
+ * Tests whether a value is a positioned @cldmv/jsonv AST node.
139
+ * @public
140
+ * @param {unknown} value - Any property value of a node.
141
+ * @returns {boolean} `true` for an object with a string `type` and a `loc`.
142
+ * @example
143
+ * isJsonvNode({ type: "Literal", loc: { start: {}, end: {} } }); // true
144
+ */
145
+ function isJsonvNode(value) {
146
+ return value !== null && typeof value === "object" && typeof value.type === "string" && typeof value.loc === "object";
147
+ }
148
+
149
+ /**
150
+ * Returns the child keys to visit for a node: its entry in {@link VISITOR_KEYS}, or — for a
151
+ * node type @cldmv/jsonv may add later — every key holding a node or an array of nodes.
152
+ * @public
153
+ * @param {Object} node - An AST node.
154
+ * @returns {ReadonlyArray<string>} Child property names.
155
+ * @example
156
+ * getChildKeys({ type: "Property", key, value }); // ["key", "value"]
157
+ */
158
+ function getChildKeys(node) {
159
+ return (
160
+ VISITOR_KEYS[node.type] ??
161
+ Object.keys(node).filter((key) => {
162
+ const value = node[key];
163
+ return Array.isArray(value) ? value.some(isJsonvNode) : isJsonvNode(value);
164
+ })
165
+ );
166
+ }
167
+
168
+ /**
169
+ * Converts a @cldmv/jsonv `loc` (1-based lines, 0-based columns, 0-based `offset`s) into the
170
+ * ESLint `loc`/`range` pair for this language. The language declares `lineStart: 1` and
171
+ * `columnStart: 1`, so columns are shifted to 1-based; `range` comes from the offsets.
172
+ * @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.
175
+ * @example
176
+ * toEslintPosition({ start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 3, offset: 3 } });
177
+ * // { loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 4 } }, range: [0, 3] }
178
+ */
179
+ function toEslintPosition(loc) {
180
+ return {
181
+ loc: {
182
+ start: { line: loc.start.line, column: loc.start.column + 1 },
183
+ end: { line: loc.end.line, column: loc.end.column + 1 }
184
+ },
185
+ range: [loc.start.offset, loc.end.offset]
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Copies a jsonv comment or token with ESLint positions (a 1-based `loc` and a `range`).
191
+ * Tokens are never walked for children: some token types share a name with a node type
192
+ * (`Identifier`, `TemplateLiteral`) but carry no child nodes.
193
+ * @public
194
+ * @param {Object} element - A jsonv comment or token.
195
+ * @returns {Object} The converted copy.
196
+ * @example
197
+ * convertLeaf(parseToAst("// note\n1").comments[0]);
198
+ */
199
+ function convertLeaf(element) {
200
+ return { ...element, ...toEslintPosition(element.loc) };
201
+ }
202
+
203
+ /**
204
+ * Deep-copies a jsonv AST node with ESLint positions: every node gets a 1-based `loc` and a
205
+ * `range`, and child nodes are converted recursively.
206
+ * @public
207
+ * @param {Object} node - A jsonv AST node.
208
+ * @returns {Object} The converted copy.
209
+ * @example
210
+ * convertNode(parseToAst("{ a: 1 }").program.body);
211
+ */
212
+ function convertNode(node) {
213
+ const converted = convertLeaf(node);
214
+ for (const key of getChildKeys(node)) {
215
+ const child = node[key];
216
+ converted[key] = Array.isArray(child) ? child.map(convertNode) : convertNode(child);
217
+ }
218
+ return converted;
219
+ }
220
+
221
+ /**
222
+ * Builds the ESLint AST for a document from @cldmv/jsonv's `parseToAst()` result: the
223
+ * `Program` root (whose `body` is the root value) with positioned `comments` and `tokens`.
224
+ * @public
225
+ * @param {{program: Object, comments: Object[], tokens: Object[]}} astResult - The `parseToAst()` result.
226
+ * @returns {Object} The `Program` node.
227
+ * @example
228
+ * buildProgram(parseToAst("// note\n{ a: 1 }"));
229
+ */
230
+ function buildProgram({ program, comments, tokens }) {
231
+ return {
232
+ ...convertNode(program),
233
+ comments: comments.map(convertLeaf),
234
+ tokens: tokens.map(convertLeaf)
235
+ };
236
+ }
237
+
238
+ /**
239
+ * SourceCode for a parsed `.jsonv` file, modelled on @eslint/json's `JSONSourceCode`:
240
+ * positions, lines and text helpers come from `TextSourceCodeBase`; this class adds AST
241
+ * traversal, parent lookup and inline configuration (`eslint-disable*` / `eslint-enable`
242
+ * directives and `/* eslint rule: ... *\/` config comments).
243
+ *
244
+ * @public
245
+ */
246
+ class JsonvSourceCode extends TextSourceCodeBase {
247
+ /**
248
+ * Cached traversal steps.
249
+ * @type {VisitNodeStep[]|undefined}
250
+ */
251
+ #steps;
252
+
253
+ /**
254
+ * Parent of every non-root node.
255
+ * @type {WeakMap<Object, Object>}
256
+ */
257
+ #parents = new WeakMap();
258
+
259
+ /**
260
+ * Cached inline configuration comments.
261
+ * @type {Object[]|undefined}
262
+ */
263
+ #inlineConfigComments;
264
+
265
+ /**
266
+ * Creates a SourceCode object.
267
+ * @param {Object} options - Options.
268
+ * @param {string} options.text - The source text.
269
+ * @param {Object} options.ast - The `Program` node built by `jsonvLanguage#parse`.
270
+ */
271
+ constructor({ text, ast }) {
272
+ super({ text, ast, lineEndingPattern: LINE_ENDING_PATTERN });
273
+ /** @type {Object[]} Comments in source order. */
274
+ this.comments = ast.comments;
275
+ /** @type {Object[]} Tokens in source order, excluding comments. */
276
+ this.tokens = ast.tokens;
277
+ /** @type {boolean} Whether the text starts with a byte order mark. */
278
+ this.hasBOM = text.charCodeAt(0) === 0xfeff;
279
+ }
280
+
281
+ /**
282
+ * Returns the source text split into lines (a fresh copy of `lines`).
283
+ * @public
284
+ * @returns {string[]} The lines.
285
+ */
286
+ getLines() {
287
+ return [...this.lines];
288
+ }
289
+
290
+ /**
291
+ * Walks the AST once, recording enter/exit steps and each node's parent.
292
+ * @returns {VisitNodeStep[]} The traversal steps.
293
+ */
294
+ #ensureTraversal() {
295
+ if (!this.#steps) {
296
+ const steps = [];
297
+ const visit = (node, parent) => {
298
+ if (parent) this.#parents.set(node, parent);
299
+ steps.push(new VisitNodeStep({ target: node, phase: 1, args: [node, parent] }));
300
+ for (const key of getChildKeys(node)) {
301
+ const child = node[key];
302
+ for (const item of Array.isArray(child) ? child : [child]) visit(item, node);
303
+ }
304
+ steps.push(new VisitNodeStep({ target: node, phase: 2, args: [node, parent] }));
305
+ };
306
+ visit(this.ast, undefined);
307
+ this.#steps = steps;
308
+ }
309
+ return this.#steps;
310
+ }
311
+
312
+ /**
313
+ * Returns the enter/exit visit steps for every node, depth first in source order.
314
+ * @public
315
+ * @returns {Iterator<VisitNodeStep>} The traversal steps.
316
+ */
317
+ traverse() {
318
+ return this.#ensureTraversal().values();
319
+ }
320
+
321
+ /**
322
+ * Returns the parent of a node, or `undefined` for the `Program` root.
323
+ * @public
324
+ * @param {Object} node - An AST node.
325
+ * @returns {Object|undefined} The parent node.
326
+ */
327
+ getParent(node) {
328
+ this.#ensureTraversal();
329
+ return this.#parents.get(node);
330
+ }
331
+
332
+ /**
333
+ * Returns the comments that hold inline configuration (`eslint`, `eslint-enable`,
334
+ * `eslint-disable`, `eslint-disable-line`, `eslint-disable-next-line`).
335
+ * @public
336
+ * @returns {Object[]} The inline configuration comments.
337
+ */
338
+ getInlineConfigNodes() {
339
+ if (!this.#inlineConfigComments) {
340
+ this.#inlineConfigComments = this.comments.filter((comment) => INLINE_CONFIG.test(comment.value));
341
+ }
342
+ return this.#inlineConfigComments;
343
+ }
344
+
345
+ /**
346
+ * Returns the `eslint-disable*` / `eslint-enable` directives, plus problems for malformed
347
+ * ones (an `eslint-disable-line` spanning several lines).
348
+ * @public
349
+ * @returns {{problems: Object[], directives: Directive[]}} Directives and problems.
350
+ */
351
+ getDisableDirectives() {
352
+ const problems = [];
353
+ const directives = [];
354
+
355
+ for (const comment of this.getInlineConfigNodes()) {
356
+ const { label, value, justification } = commentParser.parseDirective(comment.value);
357
+
358
+ if (label === "eslint-disable-line" && comment.loc.start.line !== comment.loc.end.line) {
359
+ problems.push({ ruleId: null, message: `${label} comment should not span multiple lines.`, loc: comment.loc });
360
+ continue;
361
+ }
362
+
363
+ if (label !== "eslint") {
364
+ directives.push(new Directive({ type: label.slice("eslint-".length), node: comment, value, justification }));
365
+ }
366
+ }
367
+
368
+ return { problems, directives };
369
+ }
370
+
371
+ /**
372
+ * Returns the rule configurations from `/* eslint rule: ... *\/` comments, plus problems for
373
+ * ones that fail to parse.
374
+ * @public
375
+ * @returns {{problems: Object[], configs: Array<{config: {rules: Object}, loc: Object}>}} Configs and problems.
376
+ */
377
+ applyInlineConfig() {
378
+ const problems = [];
379
+ const configs = [];
380
+
381
+ for (const comment of this.getInlineConfigNodes()) {
382
+ const { label, value } = commentParser.parseDirective(comment.value);
383
+ if (label !== "eslint") continue;
384
+
385
+ const parseResult = commentParser.parseJSONLikeConfig(value);
386
+ if (parseResult.ok) {
387
+ configs.push({ config: { rules: parseResult.config }, loc: comment.loc });
388
+ } else {
389
+ problems.push({ ruleId: null, message: parseResult.error.message, loc: comment.loc });
390
+ }
391
+ }
392
+
393
+ return { problems, configs };
394
+ }
395
+ }
396
+
80
397
  /**
81
398
  * Language definition for jsonv files
82
399
  *
@@ -88,13 +405,21 @@ const jsonvLanguage = {
88
405
  fileType: "text",
89
406
  /** Line numbering starts at 1 */
90
407
  lineStart: 1,
91
- /** Column numbering starts at 1 */
408
+ /** Column numbering starts at 1 (node `loc` columns are 1-based) */
92
409
  columnStart: 1,
93
410
  /** Property name for node types */
94
411
  nodeTypeKey: "type",
412
+ /** Child keys of each AST node type, used for selector matching */
413
+ visitorKeys: VISITOR_KEYS,
95
414
 
96
415
  /**
97
- * Parse jsonv source code - ESLint 9 Language API
416
+ * Parse jsonv source code - ESLint Language API.
417
+ *
418
+ * 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.
422
+ *
98
423
  * @param {Object} file - File object with body property containing source text
99
424
  * @param {Object} context - Context with languageOptions
100
425
  * @returns {Object} Parse result {ok, ast, jsonvValue} or {ok: false, errors}
@@ -102,46 +427,37 @@ const jsonvLanguage = {
102
427
  parse(file, context) {
103
428
  const text = file.body;
104
429
  const options = context.languageOptions || {};
430
+ const parseOptions = {
431
+ year: options.year || 2025,
432
+ strictBigInt: options.strictBigInt !== undefined ? options.strictBigInt : false,
433
+ mode: "jsonv"
434
+ };
105
435
 
106
436
  try {
107
- // Parse with all features enabled (ES2025) and strict mode off by default
108
- const result = parseWithOptions(text, {
109
- year: options.year || 2025,
110
- strictBigInt: options.strictBigInt !== undefined ? options.strictBigInt : false,
111
- mode: "jsonv",
112
- preserveComments: true,
113
- tolerant: false
114
- });
115
-
116
- // Return parse result with minimal ESTree-compatible AST
437
+ const jsonvValue = parseWithOptions(text, { ...parseOptions, tolerant: false });
438
+ const ast = buildProgram(parseToAst(text, parseOptions));
439
+
117
440
  return {
118
441
  ok: true,
119
- ast: {
120
- type: "Program",
121
- body: [],
122
- sourceType: "module",
123
- comments: [],
124
- tokens: []
125
- },
126
- // Store the parsed jsonv value for potential use by rules
127
- jsonvValue: result
442
+ ast,
443
+ // The evaluated document (references resolved) for callers of parse()
444
+ jsonvValue
128
445
  };
129
446
  } catch (error) {
130
- // @cldmv/jsonv (>=1.0.10) throws a `JsonvSyntaxError` — or its `LexerError` subclass, which
131
- // extends it — for every parser-level and lexer-level failure, carrying the real
132
- // `line`/`column`/`loc` of the failure. Its `column` is 0-based (matching the column
133
- // embedded in its own messages), while this language's `columnStart: 1` means ESLint
134
- // expects 1-based columns, so the reported column is `error.column + 1`.
447
+ // @cldmv/jsonv (>=1.1.0) throws a `JsonvSyntaxError` (or its `LexerError` subclass) for
448
+ // every parser-level and lexer-level failure, and a `JsonvReferenceError` for an
449
+ // unresolved or circular internal reference; both carry the real `line`/`column`/`loc`.
450
+ // Their `column` is 0-based (matching the column embedded in jsonv's messages), while
451
+ // this language's `columnStart: 1` means ESLint expects 1-based columns, hence `+ 1`.
135
452
  //
136
- // Unresolved-reference errors (jsonv#32, in progress) are still a plain `Error` with no
137
- // position — fall back to line 1, column 1 for anything that isn't a `JsonvSyntaxError`.
138
- const hasPosition = error instanceof JsonvSyntaxError;
453
+ // Anything else (e.g. a RangeError from exhausting the call stack on pathologically deep
454
+ // nesting) has no position — fall back to line 1, column 1.
455
+ const hasPosition = error instanceof JsonvSyntaxError || error instanceof JsonvReferenceError;
139
456
 
140
457
  const errorInfo = hasPosition
141
458
  ? { message: error.message, line: error.line, column: error.column + 1, endLine: error.loc.end.line, endColumn: error.loc.end.column + 1 }
142
459
  : { message: error.message, line: 1, column: 1 };
143
460
 
144
- // Return error result
145
461
  return {
146
462
  ok: false,
147
463
  errors: [errorInfo]
@@ -150,67 +466,16 @@ const jsonvLanguage = {
150
466
  },
151
467
 
152
468
  /**
153
- * Create source code object
154
- * Required by ESLint 9 language API
469
+ * Create the SourceCode object ESLint hands to rules
155
470
  *
156
471
  * @public
157
472
  * @param {Object} file - The file object
158
- * @param {Object} parseResult - The result from parse()
159
- * @param {Object} context - Context object
160
- * @returns {Object} SourceCode object
473
+ * @param {Object} parseResult - The successful result from parse()
474
+ * @returns {JsonvSourceCode} SourceCode object
161
475
  */
162
- createSourceCode(file, parseResult, context) {
476
+ createSourceCode(file, parseResult) {
163
477
  const text = typeof file.body === "string" ? file.body : String(file.body || "");
164
- const ast = parseResult.ast;
165
-
166
- return {
167
- text,
168
- ast,
169
- lines: text.split(/\r\n|[\r\n\u2028\u2029]/g),
170
- hasBOM: text.charCodeAt(0) === 0xfeff,
171
-
172
- // Required methods
173
- getLoc(node) {
174
- return node.loc || { start: { line: 1, column: 0 }, end: { line: 1, column: 0 } };
175
- },
176
-
177
- getRange(node) {
178
- return node.range || [0, 0];
179
- },
180
-
181
- traverse() {
182
- // ESLint expects: { kind: 1 (VISIT), target: node, phase: 1|2, args: [] }
183
- // kind: 1 = STEP_KIND_VISIT
184
- // phase: 1 = enter, 2 = exit
185
- return [
186
- {
187
- kind: 1,
188
- target: ast,
189
- phase: 1,
190
- args: [ast]
191
- },
192
- {
193
- kind: 1,
194
- target: ast,
195
- phase: 2,
196
- args: [ast]
197
- }
198
- ].values();
199
- },
200
-
201
- // Optional helper methods
202
- getText(node, beforeCount, afterCount) {
203
- if (node && node.range) {
204
- const [start, end] = node.range;
205
- return text.slice(Math.max(0, start - (beforeCount || 0)), Math.min(text.length, end + (afterCount || 0)));
206
- }
207
- return text;
208
- },
209
-
210
- getLines() {
211
- return text.split(/\r\n|[\r\n\u2028\u2029]/g);
212
- }
213
- };
478
+ return new JsonvSourceCode({ text, ast: parseResult.ast });
214
479
  },
215
480
 
216
481
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cldmv/eslint-plugin-jsonv",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "ESLint plugin for @cldmv/jsonv files",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -29,14 +29,14 @@
29
29
  "README.md"
30
30
  ],
31
31
  "peerDependencies": {
32
- "@cldmv/jsonv": "^1.0.10"
32
+ "@cldmv/jsonv": "^1.1.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@cldmv/jsonv": "^1.0.10",
35
+ "@cldmv/jsonv": "^1.1.0",
36
36
  "@cldmv/vitest-runner": "^1.4.3",
37
37
  "@vitest/coverage-v8": "^5.0.2",
38
38
  "eslint": "^10.11.0",
39
- "typescript": "^5.9.3",
39
+ "typescript": "^7.0.2",
40
40
  "vitest": "^5.0.2"
41
41
  },
42
42
  "keywords": [
@@ -81,5 +81,8 @@
81
81
  "homepage": "https://github.com/CLDMV/jsonv-eslint-plugin-jsonv/#readme",
82
82
  "engines": {
83
83
  "node": ">=18.0.0"
84
+ },
85
+ "dependencies": {
86
+ "@eslint/plugin-kit": "^0.7.3"
84
87
  }
85
88
  }