@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 +49 -0
- package/dist/index.d.mts +32 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +353 -88
- package/package.json +7 -4
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
|
-
|
|
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
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../index.mjs"],"names":[],"mappings":";
|
|
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
|
-
|
|
32
|
-
import {
|
|
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
|
|
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
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
// expects 1-based columns,
|
|
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
|
-
//
|
|
137
|
-
// position — fall back to line 1, column 1
|
|
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
|
|
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
|
-
* @
|
|
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
|
|
476
|
+
createSourceCode(file, parseResult) {
|
|
163
477
|
const text = typeof file.body === "string" ? file.body : String(file.body || "");
|
|
164
|
-
|
|
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.
|
|
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
|
|
32
|
+
"@cldmv/jsonv": "^1.1.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@cldmv/jsonv": "^1.0
|
|
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": "^
|
|
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
|
}
|