@amritk/lint 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/AI.md +55 -0
  2. package/dist/core/document.js +12 -10
  3. package/dist/core/formats.js +10 -13
  4. package/dist/core/glob.js +96 -118
  5. package/dist/core/index.d.ts +0 -1
  6. package/dist/core/index.js +30 -11
  7. package/dist/core/jsonpath.js +487 -561
  8. package/dist/core/lint.js +78 -85
  9. package/dist/core/plugin.js +21 -29
  10. package/dist/core/pointers.js +107 -151
  11. package/dist/core/ruleset.js +0 -0
  12. package/dist/core/runner.js +214 -272
  13. package/dist/core/types.js +4 -1
  14. package/dist/core/validate-ruleset.js +98 -112
  15. package/dist/fix/apply.d.ts +1 -1
  16. package/dist/fix/apply.js +58 -96
  17. package/dist/fix/index.js +7 -2
  18. package/dist/fix/plugin.js +15 -20
  19. package/dist/fix/types.d.ts +1 -1
  20. package/dist/functions/alphabetical.d.ts +1 -1
  21. package/dist/functions/alphabetical.js +39 -50
  22. package/dist/functions/casing.d.ts +1 -1
  23. package/dist/functions/casing.js +39 -45
  24. package/dist/functions/defined.d.ts +1 -1
  25. package/dist/functions/defined.js +7 -5
  26. package/dist/functions/enumeration.d.ts +1 -1
  27. package/dist/functions/enumeration.js +15 -25
  28. package/dist/functions/falsy.d.ts +1 -1
  29. package/dist/functions/falsy.js +7 -5
  30. package/dist/functions/index.d.ts +1 -1
  31. package/dist/functions/index.js +60 -44
  32. package/dist/functions/length.d.ts +1 -1
  33. package/dist/functions/length.js +22 -32
  34. package/dist/functions/or.d.ts +1 -1
  35. package/dist/functions/or.js +18 -24
  36. package/dist/functions/pattern.d.ts +1 -1
  37. package/dist/functions/pattern.js +39 -49
  38. package/dist/functions/schema.d.ts +1 -1
  39. package/dist/functions/schema.js +93 -119
  40. package/dist/functions/truthy.d.ts +1 -1
  41. package/dist/functions/truthy.js +7 -5
  42. package/dist/functions/typed-enum.d.ts +1 -1
  43. package/dist/functions/typed-enum.js +33 -36
  44. package/dist/functions/undefined.d.ts +1 -1
  45. package/dist/functions/undefined.js +7 -8
  46. package/dist/functions/unreferenced-reusable-object.d.ts +1 -1
  47. package/dist/functions/unreferenced-reusable-object.js +35 -47
  48. package/dist/functions/xor.d.ts +1 -1
  49. package/dist/functions/xor.js +13 -16
  50. package/dist/index.d.ts +17 -4
  51. package/dist/index.js +149 -164
  52. package/dist/parsers/edit-model.js +316 -444
  53. package/dist/parsers/index.d.ts +1 -1
  54. package/dist/parsers/index.js +23 -19
  55. package/dist/parsers/json.js +39 -37
  56. package/dist/parsers/lines.js +23 -26
  57. package/dist/parsers/types.js +9 -7
  58. package/dist/parsers/yaml.js +137 -204
  59. package/dist/rules/openapi/fixers.js +166 -221
  60. package/dist/rules/openapi/formats.js +26 -27
  61. package/dist/rules/openapi/functions/example-validation.d.ts +1 -1
  62. package/dist/rules/openapi/functions/example-validation.js +98 -138
  63. package/dist/rules/openapi/functions/helpers.js +8 -10
  64. package/dist/rules/openapi/functions/index.d.ts +1 -1
  65. package/dist/rules/openapi/functions/index.js +98 -72
  66. package/dist/rules/openapi/functions/oas-additional-operations.d.ts +1 -1
  67. package/dist/rules/openapi/functions/oas-additional-operations.js +16 -22
  68. package/dist/rules/openapi/functions/oas-discriminator.d.ts +1 -1
  69. package/dist/rules/openapi/functions/oas-discriminator.js +24 -22
  70. package/dist/rules/openapi/functions/oas-example-external-value.d.ts +1 -1
  71. package/dist/rules/openapi/functions/oas-example-external-value.js +13 -21
  72. package/dist/rules/openapi/functions/oas-example-value.d.ts +1 -1
  73. package/dist/rules/openapi/functions/oas-example-value.js +24 -27
  74. package/dist/rules/openapi/functions/oas-mutually-exclusive.d.ts +1 -1
  75. package/dist/rules/openapi/functions/oas-mutually-exclusive.js +15 -19
  76. package/dist/rules/openapi/functions/oas-no-nullable.d.ts +1 -1
  77. package/dist/rules/openapi/functions/oas-no-nullable.js +13 -21
  78. package/dist/rules/openapi/functions/oas-op-form-data-consume-check.d.ts +1 -1
  79. package/dist/rules/openapi/functions/oas-op-form-data-consume-check.js +21 -19
  80. package/dist/rules/openapi/functions/oas-op-id-unique.d.ts +1 -1
  81. package/dist/rules/openapi/functions/oas-op-id-unique.js +27 -27
  82. package/dist/rules/openapi/functions/oas-op-params.d.ts +1 -1
  83. package/dist/rules/openapi/functions/oas-op-params.js +36 -42
  84. package/dist/rules/openapi/functions/oas-op-security-defined.d.ts +1 -1
  85. package/dist/rules/openapi/functions/oas-op-security-defined.js +40 -39
  86. package/dist/rules/openapi/functions/oas-op-success-response.d.ts +1 -1
  87. package/dist/rules/openapi/functions/oas-op-success-response.js +11 -13
  88. package/dist/rules/openapi/functions/oas-path-param.d.ts +1 -1
  89. package/dist/rules/openapi/functions/oas-path-param.js +75 -96
  90. package/dist/rules/openapi/functions/oas-schema-example-deprecated.d.ts +1 -1
  91. package/dist/rules/openapi/functions/oas-schema-example-deprecated.js +33 -40
  92. package/dist/rules/openapi/functions/oas-schema.d.ts +1 -1
  93. package/dist/rules/openapi/functions/oas-schema.js +9 -14
  94. package/dist/rules/openapi/functions/oas-server-name-unique.d.ts +1 -1
  95. package/dist/rules/openapi/functions/oas-server-name-unique.js +21 -19
  96. package/dist/rules/openapi/functions/oas-server-variables.d.ts +1 -1
  97. package/dist/rules/openapi/functions/oas-server-variables.js +45 -49
  98. package/dist/rules/openapi/functions/oas-tag-defined.d.ts +1 -1
  99. package/dist/rules/openapi/functions/oas-tag-defined.js +20 -20
  100. package/dist/rules/openapi/functions/oas-tag-kind.d.ts +1 -1
  101. package/dist/rules/openapi/functions/oas-tag-kind.js +16 -16
  102. package/dist/rules/openapi/functions/oas-tag-parent-defined.d.ts +1 -1
  103. package/dist/rules/openapi/functions/oas-tag-parent-defined.js +40 -43
  104. package/dist/rules/openapi/functions/oas-tags-unique.d.ts +1 -1
  105. package/dist/rules/openapi/functions/oas-tags-unique.js +18 -16
  106. package/dist/rules/openapi/functions/oas-unused-component.d.ts +1 -1
  107. package/dist/rules/openapi/functions/oas-unused-component.js +48 -58
  108. package/dist/rules/openapi/functions/ref-siblings.d.ts +1 -1
  109. package/dist/rules/openapi/functions/ref-siblings.js +13 -11
  110. package/dist/rules/openapi/index.d.ts +2 -1
  111. package/dist/rules/openapi/index.js +99 -117
  112. package/dist/rules/openapi/oas.d.ts +1 -1
  113. package/dist/rules/openapi/oas.js +524 -536
  114. package/dist/rules/openapi/schemas/index.js +17 -33
  115. package/dist/rules/openapi/schemas/oas20.json +1 -1592
  116. package/dist/rules/openapi/schemas/oas30.json +1 -1651
  117. package/dist/rules/openapi/schemas/oas31.json +1 -1412
  118. package/dist/rules/openapi/schemas/oas32.json +1 -1684
  119. package/package.json +11 -6
@@ -1,610 +1,536 @@
1
- const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2
- // jsonpath-plus emits numeric array indices as strings and Linter historically
3
- // normalized *any* all-digit segment (including object keys like "200") to a
4
- // number. Replicate that exactly so source-map lookups are unchanged.
1
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5
2
  const normalizeSegment = (segment) => {
6
- if (typeof segment === 'number')
7
- return segment;
8
- if (segment.length > 0 && /^\d+$/.test(segment))
9
- return Number(segment);
3
+ if (typeof segment === "number")
10
4
  return segment;
5
+ if (segment.length > 0 && /^\d+$/.test(segment))
6
+ return Number(segment);
7
+ return segment;
11
8
  };
12
9
  const normalizePath = (path) => path.map(normalizeSegment);
13
- /**
14
- * Renders a concrete path as the jsonpath-plus string form (`$['a'][0]`). This
15
- * is what `@path` exposes inside a filter, so ruleset filters authored for
16
- * Spectral (which run on jsonpath-plus) see the same value.
17
- */
18
10
  const pathToJsonPathString = (path) => {
19
- let out = '$';
20
- for (const segment of path) {
21
- if (typeof segment === 'number')
22
- out += `[${segment}]`;
23
- else
24
- out += `['${segment.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}']`;
25
- }
26
- return out;
11
+ let out = "$";
12
+ for (const segment of path) {
13
+ if (typeof segment === "number")
14
+ out += `[${segment}]`;
15
+ else
16
+ out += `['${segment.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
17
+ }
18
+ return out;
27
19
  };
28
- // ---------------------------------------------------------------------------
29
- // Compilation
30
- // ---------------------------------------------------------------------------
31
- const compileCache = new Map();
32
- const filterCache = new Map();
33
- /**
34
- * Rewrites jsonpath-plus' `@`-context tokens onto real identifiers, skipping any
35
- * `@` that sits inside a quoted string literal (so an expression like
36
- * `@.name.indexOf("@")` keeps the literal `@` intact). Longer tokens are matched
37
- * first so `@parentProperty` is not eaten by `@parent`.
38
- */
20
+ const compileCache = /* @__PURE__ */ new Map();
21
+ const filterCache = /* @__PURE__ */ new Map();
39
22
  const substituteContext = (source) => {
40
- let out = '';
41
- let quote = '';
42
- for (let i = 0; i < source.length; i++) {
43
- const ch = source[i];
44
- if (quote) {
45
- out += ch;
46
- if (ch === '\\' && i + 1 < source.length) {
47
- out += source[i + 1];
48
- i++;
49
- }
50
- else if (ch === quote) {
51
- quote = '';
52
- }
53
- continue;
54
- }
55
- if (ch === '"' || ch === "'") {
56
- quote = ch;
57
- out += ch;
58
- continue;
59
- }
60
- if (ch === '@') {
61
- const rest = source.slice(i);
62
- if (rest.startsWith('@parentProperty')) {
63
- out += '_pp';
64
- i += '@parentProperty'.length - 1;
65
- }
66
- else if (rest.startsWith('@parent')) {
67
- out += '_parent';
68
- i += '@parent'.length - 1;
69
- }
70
- else if (rest.startsWith('@property')) {
71
- out += '_prop';
72
- i += '@property'.length - 1;
73
- }
74
- else if (rest.startsWith('@path')) {
75
- out += '_path';
76
- i += '@path'.length - 1;
77
- }
78
- else if (rest.startsWith('@root')) {
79
- out += '_root';
80
- i += '@root'.length - 1;
81
- }
82
- else {
83
- out += '_v';
84
- }
85
- continue;
86
- }
87
- out += ch;
23
+ let out = "";
24
+ let quote = "";
25
+ for (let i = 0; i < source.length; i++) {
26
+ const ch = source[i];
27
+ if (quote) {
28
+ out += ch;
29
+ if (ch === "\\" && i + 1 < source.length) {
30
+ out += source[i + 1];
31
+ i++;
32
+ } else if (ch === quote) {
33
+ quote = "";
34
+ }
35
+ continue;
88
36
  }
89
- return out;
90
- };
91
- const compileFilter = (source) => {
92
- const cached = filterCache.get(source);
93
- if (cached)
94
- return cached;
95
- const body = substituteContext(source);
96
- let fn;
97
- try {
98
- // `_pp` (`@parentProperty`) is supplied directly by the caller rather than
99
- // derived from a materialized path, so most filters never force a path
100
- // allocation. `_path` (`@path`) is only materialized for filters that use it.
101
- const compiled = new Function('_v', '_prop', '_parent', '_root', '_path', '_pp', `try { return !!(${body}); } catch (_e) { return false; }`);
102
- fn = compiled;
37
+ if (ch === '"' || ch === "'") {
38
+ quote = ch;
39
+ out += ch;
40
+ continue;
103
41
  }
104
- catch {
105
- fn = () => false;
42
+ if (ch === "@") {
43
+ const rest = source.slice(i);
44
+ if (rest.startsWith("@parentProperty")) {
45
+ out += "_pp";
46
+ i += "@parentProperty".length - 1;
47
+ } else if (rest.startsWith("@parent")) {
48
+ out += "_parent";
49
+ i += "@parent".length - 1;
50
+ } else if (rest.startsWith("@property")) {
51
+ out += "_prop";
52
+ i += "@property".length - 1;
53
+ } else if (rest.startsWith("@path")) {
54
+ out += "_path";
55
+ i += "@path".length - 1;
56
+ } else if (rest.startsWith("@root")) {
57
+ out += "_root";
58
+ i += "@root".length - 1;
59
+ } else {
60
+ out += "_v";
61
+ }
62
+ continue;
106
63
  }
107
- filterCache.set(source, fn);
108
- return fn;
64
+ out += ch;
65
+ }
66
+ return out;
67
+ };
68
+ const compileFilter = (source) => {
69
+ const cached = filterCache.get(source);
70
+ if (cached)
71
+ return cached;
72
+ const body = substituteContext(source);
73
+ let fn;
74
+ try {
75
+ const compiled = new Function("_v", "_prop", "_parent", "_root", "_path", "_pp", `try { return !!(${body}); } catch (_e) { return false; }`);
76
+ fn = compiled;
77
+ } catch {
78
+ fn = () => false;
79
+ }
80
+ filterCache.set(source, fn);
81
+ return fn;
109
82
  };
110
- /** Splits bracket content on top-level commas, respecting quotes (and their escapes). */
111
83
  const splitUnion = (content) => {
112
- const parts = [];
113
- let depth = 0;
114
- let quote = '';
115
- let current = '';
116
- for (let i = 0; i < content.length; i++) {
117
- const ch = content[i];
118
- if (quote) {
119
- if (ch === '\\' && i + 1 < content.length) {
120
- current += ch + content[i + 1];
121
- i++;
122
- continue;
123
- }
124
- if (ch === quote)
125
- quote = '';
126
- current += ch;
127
- continue;
128
- }
129
- if (ch === '"' || ch === "'") {
130
- quote = ch;
131
- current += ch;
132
- continue;
133
- }
134
- if (ch === '[' || ch === '(')
135
- depth++;
136
- else if (ch === ']' || ch === ')')
137
- depth--;
138
- if (ch === ',' && depth === 0) {
139
- parts.push(current);
140
- current = '';
141
- continue;
142
- }
143
- current += ch;
84
+ const parts = [];
85
+ let depth = 0;
86
+ let quote = "";
87
+ let current = "";
88
+ for (let i = 0; i < content.length; i++) {
89
+ const ch = content[i];
90
+ if (quote) {
91
+ if (ch === "\\" && i + 1 < content.length) {
92
+ current += ch + content[i + 1];
93
+ i++;
94
+ continue;
95
+ }
96
+ if (ch === quote)
97
+ quote = "";
98
+ current += ch;
99
+ continue;
144
100
  }
145
- parts.push(current);
146
- return parts;
101
+ if (ch === '"' || ch === "'") {
102
+ quote = ch;
103
+ current += ch;
104
+ continue;
105
+ }
106
+ if (ch === "[" || ch === "(")
107
+ depth++;
108
+ else if (ch === "]" || ch === ")")
109
+ depth--;
110
+ if (ch === "," && depth === 0) {
111
+ parts.push(current);
112
+ current = "";
113
+ continue;
114
+ }
115
+ current += ch;
116
+ }
117
+ parts.push(current);
118
+ return parts;
147
119
  };
148
- /** Unquotes a `'...'` / `"..."` token, honoring backslash escapes; returns null when not a quoted literal. */
149
120
  const unquote = (token) => {
150
- const t = token.trim();
151
- const quote = t[0];
152
- if (t.length < 2 || (quote !== '"' && quote !== "'"))
153
- return null;
154
- let out = '';
155
- let i = 1;
156
- let closed = false;
157
- while (i < t.length) {
158
- const ch = t[i];
159
- if (ch === '\\' && i + 1 < t.length) {
160
- out += t[i + 1];
161
- i += 2;
162
- continue;
163
- }
164
- if (ch === quote) {
165
- closed = true;
166
- i++;
167
- break;
168
- }
169
- out += ch;
170
- i++;
121
+ const t = token.trim();
122
+ const quote = t[0];
123
+ if (t.length < 2 || quote !== '"' && quote !== "'")
124
+ return null;
125
+ let out = "";
126
+ let i = 1;
127
+ let closed = false;
128
+ while (i < t.length) {
129
+ const ch = t[i];
130
+ if (ch === "\\" && i + 1 < t.length) {
131
+ out += t[i + 1];
132
+ i += 2;
133
+ continue;
171
134
  }
172
- // A valid literal closes exactly at the end of the token.
173
- if (!closed || i !== t.length)
174
- return null;
175
- return out;
135
+ if (ch === quote) {
136
+ closed = true;
137
+ i++;
138
+ break;
139
+ }
140
+ out += ch;
141
+ i++;
142
+ }
143
+ if (!closed || i !== t.length)
144
+ return null;
145
+ return out;
176
146
  };
177
- // A slice is `start:end` or `start:end:step`, each part optional and possibly
178
- // negative, e.g. `0:2`, `-1:`, `::2`, `:`.
179
147
  const SLICE_RE = /^-?\d*:-?\d*(:-?\d+)?$/;
180
148
  const buildSlice = (trimmed) => {
181
- const [s, e, st] = trimmed.split(':');
182
- const num = (value) => value === undefined || value === '' ? undefined : Number(value);
183
- const selector = { kind: 'slice' };
184
- const start = num(s);
185
- const end = num(e);
186
- const step = num(st);
187
- if (start !== undefined)
188
- selector.start = start;
189
- if (end !== undefined)
190
- selector.end = end;
191
- if (step !== undefined)
192
- selector.step = step;
193
- return selector;
149
+ const [s, e, st] = trimmed.split(":");
150
+ const num = (value) => value === void 0 || value === "" ? void 0 : Number(value);
151
+ const selector = { kind: "slice" };
152
+ const start = num(s);
153
+ const end = num(e);
154
+ const step = num(st);
155
+ if (start !== void 0)
156
+ selector.start = start;
157
+ if (end !== void 0)
158
+ selector.end = end;
159
+ if (step !== void 0)
160
+ selector.step = step;
161
+ return selector;
194
162
  };
195
163
  const bracketSelector = (content, onError) => {
196
- const trimmed = content.trim();
197
- if (trimmed === '*')
198
- return { kind: 'wildcard' };
199
- if (trimmed.startsWith('?')) {
200
- // `?(expr)` extract the inner expression between the first `(` and last `)`.
201
- const open = trimmed.indexOf('(');
202
- const close = trimmed.lastIndexOf(')');
203
- const expr = open !== -1 && close > open ? trimmed.slice(open + 1, close) : trimmed.slice(1);
204
- return { kind: 'filter', test: compileFilter(expr), source: expr, usesPath: expr.includes('@path') };
205
- }
206
- if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
207
- // Script subscript `[(expr)]`. We support the common `(@.length - N)` form
208
- // (a from-the-end index); anything else is rejected loudly so a typo does not
209
- // silently match nothing.
210
- const inner = trimmed.slice(1, -1).trim();
211
- const lengthOnly = /^@\.length$/.test(inner);
212
- const withOffset = /^@\.length\s*-\s*(\d+)$/.exec(inner);
213
- if (lengthOnly)
214
- return { kind: 'scriptIndex', offset: 0 };
215
- if (withOffset)
216
- return { kind: 'scriptIndex', offset: -Number(withOffset[1]) };
217
- onError(`Unsupported script subscript "[${content}]"`);
218
- return { kind: 'none' };
219
- }
220
- // A slice is only a slice when it is a single (comma-free) `:`-delimited token.
221
- if (!content.includes(',') && SLICE_RE.test(trimmed))
222
- return buildSlice(trimmed);
223
- const parts = splitUnion(content);
224
- const names = [];
225
- for (const part of parts) {
226
- const token = part.trim();
227
- const literal = unquote(token);
228
- if (literal !== null) {
229
- names.push(literal);
230
- }
231
- else if (/^-?\d+$/.test(token)) {
232
- names.push(Number(token));
233
- }
234
- else {
235
- names.push(token);
236
- }
237
- }
238
- if (names.length === 1) {
239
- const only = names[0];
240
- return typeof only === 'number' ? { kind: 'index', index: only } : { kind: 'child', name: only };
164
+ const trimmed = content.trim();
165
+ if (trimmed === "*")
166
+ return { kind: "wildcard" };
167
+ if (trimmed.startsWith("?")) {
168
+ const open = trimmed.indexOf("(");
169
+ const close = trimmed.lastIndexOf(")");
170
+ const expr = open !== -1 && close > open ? trimmed.slice(open + 1, close) : trimmed.slice(1);
171
+ return { kind: "filter", test: compileFilter(expr), source: expr, usesPath: expr.includes("@path") };
172
+ }
173
+ if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
174
+ const inner = trimmed.slice(1, -1).trim();
175
+ const lengthOnly = /^@\.length$/.test(inner);
176
+ const withOffset = /^@\.length\s*-\s*(\d+)$/.exec(inner);
177
+ if (lengthOnly)
178
+ return { kind: "scriptIndex", offset: 0 };
179
+ if (withOffset)
180
+ return { kind: "scriptIndex", offset: -Number(withOffset[1]) };
181
+ onError(`Unsupported script subscript "[${content}]"`);
182
+ return { kind: "none" };
183
+ }
184
+ if (!content.includes(",") && SLICE_RE.test(trimmed))
185
+ return buildSlice(trimmed);
186
+ const parts = splitUnion(content);
187
+ const names = [];
188
+ for (const part of parts) {
189
+ const token = part.trim();
190
+ const literal = unquote(token);
191
+ if (literal !== null) {
192
+ names.push(literal);
193
+ } else if (/^-?\d+$/.test(token)) {
194
+ names.push(Number(token));
195
+ } else {
196
+ names.push(token);
241
197
  }
242
- return { kind: 'union', names };
198
+ }
199
+ if (names.length === 1) {
200
+ const only = names[0];
201
+ return typeof only === "number" ? { kind: "index", index: only } : { kind: "child", name: only };
202
+ }
203
+ return { kind: "union", names };
243
204
  };
244
- /** Finds the index of the `]` that closes the `[` at `start`, respecting quotes (and escapes) and nesting. */
245
205
  const findBracketEnd = (expression, start) => {
246
- let depth = 0;
247
- let quote = '';
248
- for (let i = start; i < expression.length; i++) {
249
- const ch = expression[i];
250
- if (quote) {
251
- if (ch === '\\') {
252
- // Skip the escaped character so an escaped quote does not end the string.
253
- i++;
254
- continue;
255
- }
256
- if (ch === quote)
257
- quote = '';
258
- continue;
259
- }
260
- if (ch === '"' || ch === "'")
261
- quote = ch;
262
- else if (ch === '[' || ch === '(')
263
- depth++;
264
- else if (ch === ')')
265
- depth--;
266
- else if (ch === ']') {
267
- depth--;
268
- if (depth === 0)
269
- return i;
270
- }
206
+ let depth = 0;
207
+ let quote = "";
208
+ for (let i = start; i < expression.length; i++) {
209
+ const ch = expression[i];
210
+ if (quote) {
211
+ if (ch === "\\") {
212
+ i++;
213
+ continue;
214
+ }
215
+ if (ch === quote)
216
+ quote = "";
217
+ continue;
218
+ }
219
+ if (ch === '"' || ch === "'")
220
+ quote = ch;
221
+ else if (ch === "[" || ch === "(")
222
+ depth++;
223
+ else if (ch === ")")
224
+ depth--;
225
+ else if (ch === "]") {
226
+ depth--;
227
+ if (depth === 0)
228
+ return i;
271
229
  }
272
- return -1;
230
+ }
231
+ return -1;
273
232
  };
274
233
  const readName = (expression, start) => {
275
- let i = start;
276
- while (i < expression.length && !'.[]^~'.includes(expression[i]))
277
- i++;
278
- return { name: expression.slice(start, i), end: i };
234
+ let i = start;
235
+ while (i < expression.length && !".[]^~".includes(expression[i]))
236
+ i++;
237
+ return { name: expression.slice(start, i), end: i };
279
238
  };
280
- /** Compiles a JSONPath `expression` into a {@link CompiledPath}, cached by string so repeats are free. */
281
- export const compileQuery = (expression) => {
282
- const cached = compileCache.get(expression);
283
- if (cached)
284
- return cached;
285
- const steps = [];
286
- const errors = [];
287
- const onError = (message) => {
288
- errors.push(message);
289
- };
290
- let hasDescent = false;
291
- let i = 0;
292
- // Every well-formed JSONPath is rooted at `$`. Without it the expression would
293
- // otherwise compile to zero steps and match the document root — a silent bug.
294
- if (expression[0] === '$')
295
- i = 1;
296
- else
297
- onError('JSONPath must start with "$"');
298
- let recursive = false;
299
- while (i < expression.length) {
300
- const ch = expression[i];
301
- if (ch === '.') {
302
- if (expression[i + 1] === '.') {
303
- recursive = true;
304
- hasDescent = true;
305
- i += 2;
306
- // A bare `..` followed by `.`/end is unusual; loop handles the selector.
307
- continue;
308
- }
309
- i++;
310
- if (expression[i] === '*') {
311
- steps.push({ recursive, selector: { kind: 'wildcard' } });
312
- recursive = false;
313
- i++;
314
- continue;
315
- }
316
- const { name, end } = readName(expression, i);
317
- steps.push({ recursive, selector: { kind: 'child', name } });
318
- recursive = false;
319
- i = end;
320
- continue;
321
- }
322
- if (ch === '[') {
323
- const end = findBracketEnd(expression, i);
324
- if (end === -1) {
325
- onError(`Unterminated "[" in "${expression}"`);
326
- break;
327
- }
328
- const content = expression.slice(i + 1, end);
329
- steps.push({ recursive, selector: bracketSelector(content, onError) });
330
- recursive = false;
331
- i = end + 1;
332
- continue;
333
- }
334
- if (ch === '^') {
335
- // Honor a pending `..` so `$..^` selects every node's parent (fixing a
336
- // flag leak where the descent was dropped and the selector matched nothing).
337
- steps.push({ recursive, selector: { kind: 'parent' } });
338
- recursive = false;
339
- i++;
340
- continue;
341
- }
342
- if (ch === '~') {
343
- steps.push({ recursive, selector: { kind: 'keys' } });
344
- recursive = false;
345
- i++;
346
- continue;
347
- }
348
- if (ch === '*') {
349
- steps.push({ recursive, selector: { kind: 'wildcard' } });
350
- recursive = false;
351
- i++;
352
- continue;
353
- }
354
- // Bare name following `..` (e.g. `$..foo`) or other unexpected token.
355
- if (recursive) {
356
- const { name, end } = readName(expression, i);
357
- if (end > i) {
358
- steps.push({ recursive: true, selector: { kind: 'child', name } });
359
- recursive = false;
360
- i = end;
361
- continue;
362
- }
363
- }
239
+ const compileQuery = (expression) => {
240
+ const cached = compileCache.get(expression);
241
+ if (cached)
242
+ return cached;
243
+ const steps = [];
244
+ const errors = [];
245
+ const onError = (message) => {
246
+ errors.push(message);
247
+ };
248
+ let hasDescent = false;
249
+ let i = 0;
250
+ if (expression[0] === "$")
251
+ i = 1;
252
+ else
253
+ onError('JSONPath must start with "$"');
254
+ let recursive = false;
255
+ while (i < expression.length) {
256
+ const ch = expression[i];
257
+ if (ch === ".") {
258
+ if (expression[i + 1] === ".") {
259
+ recursive = true;
260
+ hasDescent = true;
261
+ i += 2;
262
+ continue;
263
+ }
264
+ i++;
265
+ if (expression[i] === "*") {
266
+ steps.push({ recursive, selector: { kind: "wildcard" } });
267
+ recursive = false;
364
268
  i++;
269
+ continue;
270
+ }
271
+ const { name, end } = readName(expression, i);
272
+ steps.push({ recursive, selector: { kind: "child", name } });
273
+ recursive = false;
274
+ i = end;
275
+ continue;
365
276
  }
366
- const compiled = {
367
- expression,
368
- steps,
369
- hasDescent,
370
- ...(errors.length > 0 ? { error: errors.join('; ') } : {}),
371
- };
372
- compileCache.set(expression, compiled);
373
- return compiled;
277
+ if (ch === "[") {
278
+ const end = findBracketEnd(expression, i);
279
+ if (end === -1) {
280
+ onError(`Unterminated "[" in "${expression}"`);
281
+ break;
282
+ }
283
+ const content = expression.slice(i + 1, end);
284
+ steps.push({ recursive, selector: bracketSelector(content, onError) });
285
+ recursive = false;
286
+ i = end + 1;
287
+ continue;
288
+ }
289
+ if (ch === "^") {
290
+ steps.push({ recursive, selector: { kind: "parent" } });
291
+ recursive = false;
292
+ i++;
293
+ continue;
294
+ }
295
+ if (ch === "~") {
296
+ steps.push({ recursive, selector: { kind: "keys" } });
297
+ recursive = false;
298
+ i++;
299
+ continue;
300
+ }
301
+ if (ch === "*") {
302
+ steps.push({ recursive, selector: { kind: "wildcard" } });
303
+ recursive = false;
304
+ i++;
305
+ continue;
306
+ }
307
+ if (recursive) {
308
+ const { name, end } = readName(expression, i);
309
+ if (end > i) {
310
+ steps.push({ recursive: true, selector: { kind: "child", name } });
311
+ recursive = false;
312
+ i = end;
313
+ continue;
314
+ }
315
+ }
316
+ i++;
317
+ }
318
+ const compiled = {
319
+ expression,
320
+ steps,
321
+ hasDescent,
322
+ ...errors.length > 0 ? { error: errors.join("; ") } : {}
323
+ };
324
+ compileCache.set(expression, compiled);
325
+ return compiled;
374
326
  };
375
- /** Materializes the concrete (un-normalized) path from root to `node`. */
376
327
  const pathOf = (node) => {
377
- let depth = 0;
378
- for (let n = node; n !== undefined && n.parent !== undefined; n = n.parent)
379
- depth++;
380
- const out = new Array(depth);
381
- let i = depth - 1;
382
- for (let n = node; n !== undefined && n.parent !== undefined; n = n.parent) {
383
- out[i--] = n.key;
384
- }
385
- return out;
328
+ let depth = 0;
329
+ for (let n = node; n !== void 0 && n.parent !== void 0; n = n.parent)
330
+ depth++;
331
+ const out = new Array(depth);
332
+ let i = depth - 1;
333
+ for (let n = node; n !== void 0 && n.parent !== void 0; n = n.parent) {
334
+ out[i--] = n.key;
335
+ }
336
+ return out;
386
337
  };
387
338
  const applySelector = (node, selector, root, out) => {
388
- const value = node.value;
389
- switch (selector.kind) {
390
- case 'child': {
391
- if (isObject(value)) {
392
- if (Object.hasOwn(value, selector.name))
393
- out.push({ value: value[selector.name], parent: node, key: selector.name });
394
- }
395
- else if (Array.isArray(value) && /^\d+$/.test(selector.name)) {
396
- const idx = Number(selector.name);
397
- if (idx < value.length)
398
- out.push({ value: value[idx], parent: node, key: idx });
399
- }
400
- return;
401
- }
402
- case 'index': {
403
- if (Array.isArray(value)) {
404
- const idx = selector.index < 0 ? value.length + selector.index : selector.index;
405
- if (idx >= 0 && idx < value.length)
406
- out.push({ value: value[idx], parent: node, key: idx });
407
- }
408
- else if (isObject(value) && Object.hasOwn(value, selector.index)) {
409
- out.push({ value: value[selector.index], parent: node, key: selector.index });
410
- }
411
- return;
412
- }
413
- case 'wildcard': {
414
- if (Array.isArray(value)) {
415
- for (let idx = 0; idx < value.length; idx++)
416
- out.push({ value: value[idx], parent: node, key: idx });
417
- }
418
- else if (isObject(value)) {
419
- for (const key of Object.keys(value))
420
- out.push({ value: value[key], parent: node, key });
421
- }
422
- return;
423
- }
424
- case 'union': {
425
- for (const name of selector.names) {
426
- if (Array.isArray(value)) {
427
- if (typeof name === 'number') {
428
- const idx = name < 0 ? value.length + name : name;
429
- if (idx >= 0 && idx < value.length)
430
- out.push({ value: value[idx], parent: node, key: idx });
431
- }
432
- }
433
- else if (isObject(value) && Object.hasOwn(value, name)) {
434
- out.push({ value: value[name], parent: node, key: name });
435
- }
436
- }
437
- return;
438
- }
439
- case 'slice': {
440
- if (!Array.isArray(value))
441
- return;
442
- const len = value.length;
443
- const step = selector.step ?? 1;
444
- if (step === 0)
445
- return;
446
- const clamp = (raw, fallback) => {
447
- if (raw === undefined)
448
- return fallback;
449
- return raw < 0 ? raw + len : raw;
450
- };
451
- if (step > 0) {
452
- const start = Math.max(0, clamp(selector.start, 0));
453
- const end = Math.min(len, clamp(selector.end, len));
454
- for (let idx = start; idx < end; idx += step)
455
- out.push({ value: value[idx], parent: node, key: idx });
456
- }
457
- else {
458
- const start = Math.min(len - 1, clamp(selector.start, len - 1));
459
- const end = Math.max(-1, clamp(selector.end, -1));
460
- for (let idx = start; idx > end; idx += step)
461
- out.push({ value: value[idx], parent: node, key: idx });
462
- }
463
- return;
464
- }
465
- case 'scriptIndex': {
466
- // `[(@.length - N)]` indexes from the end; N === 0 (`@.length`) is out of range.
467
- if (!Array.isArray(value))
468
- return;
469
- const idx = value.length + selector.offset;
339
+ const value = node.value;
340
+ switch (selector.kind) {
341
+ case "child": {
342
+ if (isObject(value)) {
343
+ if (Object.hasOwn(value, selector.name))
344
+ out.push({ value: value[selector.name], parent: node, key: selector.name });
345
+ } else if (Array.isArray(value) && /^\d+$/.test(selector.name)) {
346
+ const idx = Number(selector.name);
347
+ if (idx < value.length)
348
+ out.push({ value: value[idx], parent: node, key: idx });
349
+ }
350
+ return;
351
+ }
352
+ case "index": {
353
+ if (Array.isArray(value)) {
354
+ const idx = selector.index < 0 ? value.length + selector.index : selector.index;
355
+ if (idx >= 0 && idx < value.length)
356
+ out.push({ value: value[idx], parent: node, key: idx });
357
+ } else if (isObject(value) && Object.hasOwn(value, selector.index)) {
358
+ out.push({ value: value[selector.index], parent: node, key: selector.index });
359
+ }
360
+ return;
361
+ }
362
+ case "wildcard": {
363
+ if (Array.isArray(value)) {
364
+ for (let idx = 0; idx < value.length; idx++)
365
+ out.push({ value: value[idx], parent: node, key: idx });
366
+ } else if (isObject(value)) {
367
+ for (const key of Object.keys(value))
368
+ out.push({ value: value[key], parent: node, key });
369
+ }
370
+ return;
371
+ }
372
+ case "union": {
373
+ for (const name of selector.names) {
374
+ if (Array.isArray(value)) {
375
+ if (typeof name === "number") {
376
+ const idx = name < 0 ? value.length + name : name;
470
377
  if (idx >= 0 && idx < value.length)
471
- out.push({ value: value[idx], parent: node, key: idx });
472
- return;
473
- }
474
- case 'filter': {
475
- // `@parentProperty` is `node.key`; `@path` is materialized only when used.
476
- const pp = node.key;
477
- if (Array.isArray(value)) {
478
- for (let idx = 0; idx < value.length; idx++) {
479
- const child = { value: value[idx], parent: node, key: idx };
480
- const path = selector.usesPath ? pathToJsonPathString(pathOf(child)) : '';
481
- if (selector.test(value[idx], idx, value, root, path, pp))
482
- out.push(child);
483
- }
484
- }
485
- else if (isObject(value)) {
486
- for (const key of Object.keys(value)) {
487
- const child = { value: value[key], parent: node, key };
488
- const path = selector.usesPath ? pathToJsonPathString(pathOf(child)) : '';
489
- if (selector.test(value[key], key, value, root, path, pp))
490
- out.push(child);
491
- }
492
- }
493
- return;
494
- }
495
- case 'parent': {
496
- if (node.parent !== undefined)
497
- out.push(node.parent);
498
- return;
499
- }
500
- case 'keys': {
501
- if (node.parent === undefined)
502
- return;
503
- // The selected value is the node's own key, but it occupies the same path.
504
- out.push({ value: node.key, parent: node.parent, key: node.key });
505
- return;
378
+ out.push({ value: value[idx], parent: node, key: idx });
379
+ }
380
+ } else if (isObject(value) && Object.hasOwn(value, name)) {
381
+ out.push({ value: value[name], parent: node, key: name });
506
382
  }
507
- case 'none':
508
- return;
383
+ }
384
+ return;
509
385
  }
510
- };
511
- /** Visits `node` and every descendant (preorder), invoking `visit` on each. */
512
- const walkDescendants = (node, visit) => {
513
- visit(node);
514
- const value = node.value;
515
- if (Array.isArray(value)) {
516
- for (let idx = 0; idx < value.length; idx++)
517
- walkDescendants({ value: value[idx], parent: node, key: idx }, visit);
386
+ case "slice": {
387
+ if (!Array.isArray(value))
388
+ return;
389
+ const len = value.length;
390
+ const step = selector.step ?? 1;
391
+ if (step === 0)
392
+ return;
393
+ const clamp = (raw, fallback) => {
394
+ if (raw === void 0)
395
+ return fallback;
396
+ return raw < 0 ? raw + len : raw;
397
+ };
398
+ if (step > 0) {
399
+ const start = Math.max(0, clamp(selector.start, 0));
400
+ const end = Math.min(len, clamp(selector.end, len));
401
+ for (let idx = start; idx < end; idx += step)
402
+ out.push({ value: value[idx], parent: node, key: idx });
403
+ } else {
404
+ const start = Math.min(len - 1, clamp(selector.start, len - 1));
405
+ const end = Math.max(-1, clamp(selector.end, -1));
406
+ for (let idx = start; idx > end; idx += step)
407
+ out.push({ value: value[idx], parent: node, key: idx });
408
+ }
409
+ return;
518
410
  }
519
- else if (isObject(value)) {
520
- for (const key of Object.keys(value))
521
- walkDescendants({ value: value[key], parent: node, key }, visit);
411
+ case "scriptIndex": {
412
+ if (!Array.isArray(value))
413
+ return;
414
+ const idx = value.length + selector.offset;
415
+ if (idx >= 0 && idx < value.length)
416
+ out.push({ value: value[idx], parent: node, key: idx });
417
+ return;
418
+ }
419
+ case "filter": {
420
+ const pp = node.key;
421
+ if (Array.isArray(value)) {
422
+ for (let idx = 0; idx < value.length; idx++) {
423
+ const child = { value: value[idx], parent: node, key: idx };
424
+ const path = selector.usesPath ? pathToJsonPathString(pathOf(child)) : "";
425
+ if (selector.test(value[idx], idx, value, root, path, pp))
426
+ out.push(child);
427
+ }
428
+ } else if (isObject(value)) {
429
+ for (const key of Object.keys(value)) {
430
+ const child = { value: value[key], parent: node, key };
431
+ const path = selector.usesPath ? pathToJsonPathString(pathOf(child)) : "";
432
+ if (selector.test(value[key], key, value, root, path, pp))
433
+ out.push(child);
434
+ }
435
+ }
436
+ return;
522
437
  }
438
+ case "parent": {
439
+ if (node.parent !== void 0)
440
+ out.push(node.parent);
441
+ return;
442
+ }
443
+ case "keys": {
444
+ if (node.parent === void 0)
445
+ return;
446
+ out.push({ value: node.key, parent: node.parent, key: node.key });
447
+ return;
448
+ }
449
+ case "none":
450
+ return;
451
+ }
452
+ };
453
+ const walkDescendants = (node, visit) => {
454
+ visit(node);
455
+ const value = node.value;
456
+ if (Array.isArray(value)) {
457
+ for (let idx = 0; idx < value.length; idx++)
458
+ walkDescendants({ value: value[idx], parent: node, key: idx }, visit);
459
+ } else if (isObject(value)) {
460
+ for (const key of Object.keys(value))
461
+ walkDescendants({ value: value[key], parent: node, key }, visit);
462
+ }
523
463
  };
524
- /** Applies a list of steps to an existing set of nodes. */
525
464
  const applySteps = (root, initial, steps) => {
526
- let current = initial;
527
- for (const step of steps) {
528
- const next = [];
529
- if (step.recursive) {
530
- for (const node of current)
531
- walkDescendants(node, (n) => applySelector(n, step.selector, root, next));
532
- }
533
- else {
534
- for (const node of current)
535
- applySelector(node, step.selector, root, next);
536
- }
537
- current = next;
465
+ let current = initial;
466
+ for (const step of steps) {
467
+ const next = [];
468
+ if (step.recursive) {
469
+ for (const node of current)
470
+ walkDescendants(node, (n) => applySelector(n, step.selector, root, next));
471
+ } else {
472
+ for (const node of current)
473
+ applySelector(node, step.selector, root, next);
538
474
  }
539
- return current;
475
+ current = next;
476
+ }
477
+ return current;
540
478
  };
541
- const runSteps = (root, steps) => applySteps(root, [{ value: root, parent: undefined, key: undefined }], steps);
479
+ const runSteps = (root, steps) => applySteps(root, [{ value: root, parent: void 0, key: void 0 }], steps);
542
480
  const toMatches = (nodes) => {
543
- const out = new Array(nodes.length);
544
- for (let i = 0; i < nodes.length; i++) {
545
- const node = nodes[i];
546
- out[i] = { value: node.value, path: normalizePath(pathOf(node)) };
547
- }
548
- return out;
481
+ const out = new Array(nodes.length);
482
+ for (let i = 0; i < nodes.length; i++) {
483
+ const node = nodes[i];
484
+ out[i] = { value: node.value, path: normalizePath(pathOf(node)) };
485
+ }
486
+ return out;
549
487
  };
550
- /** Evaluates a pre-compiled path against `data`. */
551
- export const queryCompiled = (data, compiled) => {
552
- // A malformed path matches nothing rather than falling back to the root.
553
- if (compiled.error !== undefined)
554
- return [];
555
- if (data === null || data === undefined)
556
- return [];
557
- return toMatches(runSteps(data, compiled.steps));
488
+ const queryCompiled = (data, compiled) => {
489
+ if (compiled.error !== void 0)
490
+ return [];
491
+ if (data === null || data === void 0)
492
+ return [];
493
+ return toMatches(runSteps(data, compiled.steps));
558
494
  };
559
- /**
560
- * Evaluates many pre-compiled paths against `data`, sharing a *single* recursive
561
- * descent of the tree across every `$..`-rooted path. The ruleset has ~16
562
- * descent `given`s; walking the (post-deref, ~60k-node) tree once and testing
563
- * each path's first selector at every node — instead of one full traversal per
564
- * path — is the dominant rule-run speedup on large specs. Non-recursive paths
565
- * are evaluated directly (cheap, no descent).
566
- *
567
- * Returns one match array per input path, index-aligned with `compiled`.
568
- */
569
- export const queryMany = (data, compiled) => {
570
- const out = new Array(compiled.length);
571
- if (data === null || data === undefined) {
572
- for (let i = 0; i < compiled.length; i++)
573
- out[i] = [];
574
- return out;
575
- }
576
- const recursive = [];
577
- for (let i = 0; i < compiled.length; i++) {
578
- const c = compiled[i];
579
- // Skip malformed paths entirely; they contribute no matches.
580
- if (c.error !== undefined) {
581
- out[i] = [];
582
- continue;
583
- }
584
- const first = c.steps[0];
585
- if (first?.recursive)
586
- recursive.push(i);
587
- else
588
- out[i] = queryCompiled(data, c);
495
+ const queryMany = (data, compiled) => {
496
+ const out = new Array(compiled.length);
497
+ if (data === null || data === void 0) {
498
+ for (let i = 0; i < compiled.length; i++)
499
+ out[i] = [];
500
+ return out;
501
+ }
502
+ const recursive = [];
503
+ for (let i = 0; i < compiled.length; i++) {
504
+ const c = compiled[i];
505
+ if (c.error !== void 0) {
506
+ out[i] = [];
507
+ continue;
589
508
  }
590
- if (recursive.length > 0) {
591
- // Walk the whole tree exactly once, applying every descent path's first
592
- // selector at each visited node in the same pass (fused descent), so the
593
- // ~15k-node resolved tree is traversed a single time rather than once per
594
- // `$..` given. Each path's surviving seeds then run its remaining steps.
595
- const firsts = recursive.map((i) => compiled[i].steps[0]);
596
- const seeds = recursive.map(() => []);
597
- const root = { value: data, parent: undefined, key: undefined };
598
- walkDescendants(root, (node) => {
599
- for (let r = 0; r < recursive.length; r++)
600
- applySelector(node, firsts[r].selector, data, seeds[r]);
601
- });
602
- for (let r = 0; r < recursive.length; r++) {
603
- const c = compiled[recursive[r]];
604
- out[recursive[r]] = toMatches(applySteps(data, seeds[r], c.steps.slice(1)));
605
- }
509
+ const first = c.steps[0];
510
+ if (first?.recursive)
511
+ recursive.push(i);
512
+ else
513
+ out[i] = queryCompiled(data, c);
514
+ }
515
+ if (recursive.length > 0) {
516
+ const firsts = recursive.map((i) => compiled[i].steps[0]);
517
+ const seeds = recursive.map(() => []);
518
+ const root = { value: data, parent: void 0, key: void 0 };
519
+ walkDescendants(root, (node) => {
520
+ for (let r = 0; r < recursive.length; r++)
521
+ applySelector(node, firsts[r].selector, data, seeds[r]);
522
+ });
523
+ for (let r = 0; r < recursive.length; r++) {
524
+ const c = compiled[recursive[r]];
525
+ out[recursive[r]] = toMatches(applySteps(data, seeds[r], c.steps.slice(1)));
606
526
  }
607
- return out;
527
+ }
528
+ return out;
529
+ };
530
+ const query = (data, expression) => queryCompiled(data, compileQuery(expression));
531
+ export {
532
+ compileQuery,
533
+ query,
534
+ queryCompiled,
535
+ queryMany
608
536
  };
609
- /** Runs a JSONPath expression and returns each match with its concrete path. */
610
- export const query = (data, expression) => queryCompiled(data, compileQuery(expression));