@amritk/lint 0.4.7 → 0.5.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 (112) hide show
  1. package/AI.md +73 -4
  2. package/README.md +65 -14
  3. package/dist/core/filter-expression.js +17 -4
  4. package/dist/core/filter.js +82 -35
  5. package/dist/core/glob.js +25 -31
  6. package/dist/core/jsonpath.d.ts +9 -1
  7. package/dist/core/jsonpath.js +74 -16
  8. package/dist/core/lint.d.ts +8 -1
  9. package/dist/core/lint.js +4 -18
  10. package/dist/core/order.d.ts +30 -0
  11. package/dist/core/order.js +36 -0
  12. package/dist/core/own-key.d.ts +18 -0
  13. package/dist/core/own-key.js +12 -0
  14. package/dist/core/pointers.d.ts +3 -2
  15. package/dist/core/pointers.js +27 -18
  16. package/dist/core/ruleset.js +74 -27
  17. package/dist/core/runner.js +16 -23
  18. package/dist/core/severity.d.ts +11 -0
  19. package/dist/core/severity.js +13 -0
  20. package/dist/fix/apply.js +9 -2
  21. package/dist/fix/plugin.js +2 -1
  22. package/dist/functions/alphabetical.d.ts +14 -0
  23. package/dist/functions/alphabetical.js +4 -3
  24. package/dist/functions/or.js +1 -1
  25. package/dist/functions/ref-index.d.ts +15 -0
  26. package/dist/functions/ref-index.js +33 -0
  27. package/dist/functions/schema.d.ts +15 -0
  28. package/dist/functions/schema.js +2 -0
  29. package/dist/functions/typed-enum.js +2 -1
  30. package/dist/functions/unreferenced-reusable-object.js +3 -20
  31. package/dist/functions/xor.js +1 -1
  32. package/dist/index.d.ts +16 -16
  33. package/dist/index.js +25 -94
  34. package/dist/rules/asyncapi/asyncapi.d.ts +3 -0
  35. package/dist/rules/asyncapi/asyncapi.js +563 -0
  36. package/dist/rules/asyncapi/formats.d.ts +23 -0
  37. package/dist/rules/asyncapi/formats.js +49 -0
  38. package/dist/rules/asyncapi/functions/aas-server-variables.d.ts +8 -0
  39. package/dist/rules/asyncapi/functions/aas-server-variables.js +5 -0
  40. package/dist/rules/asyncapi/functions/aas-tags-unique.d.ts +2 -0
  41. package/dist/rules/asyncapi/functions/aas-tags-unique.js +5 -0
  42. package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.d.ts +7 -0
  43. package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.js +46 -0
  44. package/dist/rules/asyncapi/functions/asyncapi-channel-servers.d.ts +27 -0
  45. package/dist/rules/asyncapi/functions/asyncapi-channel-servers.js +61 -0
  46. package/dist/rules/asyncapi/functions/asyncapi-document-schema.d.ts +17 -0
  47. package/dist/rules/asyncapi/functions/asyncapi-document-schema.js +12 -0
  48. package/dist/rules/asyncapi/functions/asyncapi-headers-object.d.ts +23 -0
  49. package/dist/rules/asyncapi/functions/asyncapi-headers-object.js +25 -0
  50. package/dist/rules/asyncapi/functions/asyncapi-message-examples.d.ts +8 -0
  51. package/dist/rules/asyncapi/functions/asyncapi-message-examples.js +42 -0
  52. package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.d.ts +3 -0
  53. package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.js +30 -0
  54. package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.d.ts +3 -0
  55. package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.js +30 -0
  56. package/dist/rules/asyncapi/functions/asyncapi-payload.d.ts +14 -0
  57. package/dist/rules/asyncapi/functions/asyncapi-payload.js +32 -0
  58. package/dist/rules/asyncapi/functions/asyncapi-schema-validation.d.ts +11 -0
  59. package/dist/rules/asyncapi/functions/asyncapi-schema-validation.js +17 -0
  60. package/dist/rules/asyncapi/functions/asyncapi-security.d.ts +22 -0
  61. package/dist/rules/asyncapi/functions/asyncapi-security.js +78 -0
  62. package/dist/rules/asyncapi/functions/helpers.d.ts +51 -0
  63. package/dist/rules/asyncapi/functions/helpers.js +91 -0
  64. package/dist/rules/asyncapi/functions/index.d.ts +15 -0
  65. package/dist/rules/asyncapi/functions/index.js +53 -0
  66. package/dist/rules/asyncapi/functions/pointer.d.ts +11 -0
  67. package/dist/rules/asyncapi/functions/pointer.js +11 -0
  68. package/dist/rules/asyncapi/functions/schema-format.d.ts +4 -0
  69. package/dist/rules/asyncapi/functions/schema-format.js +7 -0
  70. package/dist/rules/asyncapi/index.d.ts +33 -0
  71. package/dist/rules/asyncapi/index.js +60 -0
  72. package/dist/rules/asyncapi/schemas/aas20.d.ts +2 -0
  73. package/dist/rules/asyncapi/schemas/aas20.js +4 -0
  74. package/dist/rules/asyncapi/schemas/aas21.d.ts +2 -0
  75. package/dist/rules/asyncapi/schemas/aas21.js +4 -0
  76. package/dist/rules/asyncapi/schemas/aas22.d.ts +2 -0
  77. package/dist/rules/asyncapi/schemas/aas22.js +4 -0
  78. package/dist/rules/asyncapi/schemas/aas23.d.ts +2 -0
  79. package/dist/rules/asyncapi/schemas/aas23.js +4 -0
  80. package/dist/rules/asyncapi/schemas/aas24.d.ts +2 -0
  81. package/dist/rules/asyncapi/schemas/aas24.js +4 -0
  82. package/dist/rules/asyncapi/schemas/aas25.d.ts +2 -0
  83. package/dist/rules/asyncapi/schemas/aas25.js +4 -0
  84. package/dist/rules/asyncapi/schemas/aas26.d.ts +2 -0
  85. package/dist/rules/asyncapi/schemas/aas26.js +4 -0
  86. package/dist/rules/asyncapi/schemas/aas30.d.ts +2 -0
  87. package/dist/rules/asyncapi/schemas/aas30.js +4 -0
  88. package/dist/rules/asyncapi/schemas/index.d.ts +20 -0
  89. package/dist/rules/asyncapi/schemas/index.js +45 -0
  90. package/dist/rules/openapi/fixers.d.ts +6 -2
  91. package/dist/rules/openapi/fixers.js +9 -6
  92. package/dist/rules/openapi/functions/oas-discriminator.js +1 -1
  93. package/dist/rules/openapi/functions/oas-mutually-exclusive.js +1 -1
  94. package/dist/rules/openapi/functions/oas-op-security-defined.js +1 -1
  95. package/dist/rules/openapi/functions/oas-path-param.js +5 -4
  96. package/dist/rules/openapi/functions/oas-server-variables.d.ts +6 -6
  97. package/dist/rules/openapi/functions/oas-server-variables.js +2 -45
  98. package/dist/rules/openapi/functions/oas-tags-unique.d.ts +2 -3
  99. package/dist/rules/openapi/functions/oas-tags-unique.js +2 -16
  100. package/dist/rules/openapi/functions/oas-unused-component.js +3 -25
  101. package/dist/rules/openapi/index.d.ts +9 -2
  102. package/dist/rules/openapi/index.js +15 -70
  103. package/dist/rules/openapi/schemas/index.js +5 -1
  104. package/dist/rules/shared/helpers.d.ts +2 -0
  105. package/dist/rules/shared/helpers.js +4 -0
  106. package/dist/rules/shared/server-variables.d.ts +27 -0
  107. package/dist/rules/shared/server-variables.js +55 -0
  108. package/dist/rules/shared/tags-unique.d.ts +7 -0
  109. package/dist/rules/shared/tags-unique.js +19 -0
  110. package/dist/ruleset-files.d.ts +47 -0
  111. package/dist/ruleset-files.js +92 -0
  112. package/package.json +9 -5
@@ -1,12 +1,27 @@
1
1
  import { createBoundedCache } from "./bounded-cache.js";
2
2
  import { compileFilter } from "./filter.js";
3
3
  const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4
+ const hasMember = (value, name) => Object.prototype.propertyIsEnumerable.call(value, name);
5
+ const isDigits = (value) => {
6
+ const length = value.length;
7
+ if (length === 0)
8
+ return false;
9
+ for (let i = 0; i < length; i++) {
10
+ const code = value.charCodeAt(i);
11
+ if (code < 48 || code > 57)
12
+ return false;
13
+ }
14
+ return true;
15
+ };
16
+ const childSelector = (name) => ({
17
+ kind: "child",
18
+ name,
19
+ index: isDigits(name) ? Number(name) : void 0
20
+ });
4
21
  const normalizeSegment = (segment) => {
5
22
  if (typeof segment === "number")
6
23
  return segment;
7
- if (segment.length > 0 && /^\d+$/.test(segment))
8
- return Number(segment);
9
- return segment;
24
+ return isDigits(segment) ? Number(segment) : segment;
10
25
  };
11
26
  const normalizePath = (path) => path.map(normalizeSegment);
12
27
  const pathToJsonPathString = (path) => {
@@ -143,7 +158,7 @@ const bracketSelector = (content, onError) => {
143
158
  }
144
159
  if (names.length === 1) {
145
160
  const only = names[0];
146
- return typeof only === "number" ? { kind: "index", index: only } : { kind: "child", name: only };
161
+ return typeof only === "number" ? { kind: "index", index: only } : childSelector(only);
147
162
  }
148
163
  return { kind: "union", names };
149
164
  };
@@ -214,7 +229,7 @@ const compileQuery = (expression) => {
214
229
  continue;
215
230
  }
216
231
  const { name, end } = readName(expression, i);
217
- steps.push({ recursive, selector: { kind: "child", name } });
232
+ steps.push({ recursive, selector: childSelector(name) });
218
233
  recursive = false;
219
234
  i = end;
220
235
  continue;
@@ -252,7 +267,7 @@ const compileQuery = (expression) => {
252
267
  if (recursive) {
253
268
  const { name, end } = readName(expression, i);
254
269
  if (end > i) {
255
- steps.push({ recursive: true, selector: { kind: "child", name } });
270
+ steps.push({ recursive: true, selector: childSelector(name) });
256
271
  recursive = false;
257
272
  i = end;
258
273
  continue;
@@ -285,10 +300,10 @@ const applySelector = (node, selector, root, out) => {
285
300
  switch (selector.kind) {
286
301
  case "child": {
287
302
  if (isObject(value)) {
288
- if (Object.hasOwn(value, selector.name))
303
+ if (hasMember(value, selector.name))
289
304
  out.push({ value: value[selector.name], parent: node, key: selector.name });
290
- } else if (Array.isArray(value) && /^\d+$/.test(selector.name)) {
291
- const idx = Number(selector.name);
305
+ } else if (Array.isArray(value) && selector.index !== void 0) {
306
+ const idx = selector.index;
292
307
  if (idx < value.length)
293
308
  out.push({ value: value[idx], parent: node, key: idx });
294
309
  }
@@ -299,7 +314,7 @@ const applySelector = (node, selector, root, out) => {
299
314
  const idx = selector.index < 0 ? value.length + selector.index : selector.index;
300
315
  if (idx >= 0 && idx < value.length)
301
316
  out.push({ value: value[idx], parent: node, key: idx });
302
- } else if (isObject(value) && Object.hasOwn(value, selector.index)) {
317
+ } else if (isObject(value) && hasMember(value, selector.index)) {
303
318
  out.push({ value: value[selector.index], parent: node, key: selector.index });
304
319
  }
305
320
  return;
@@ -322,7 +337,7 @@ const applySelector = (node, selector, root, out) => {
322
337
  if (idx >= 0 && idx < value.length)
323
338
  out.push({ value: value[idx], parent: node, key: idx });
324
339
  }
325
- } else if (isObject(value) && Object.hasOwn(value, name)) {
340
+ } else if (isObject(value) && hasMember(value, name)) {
326
341
  out.push({ value: value[name], parent: node, key: name });
327
342
  }
328
343
  }
@@ -467,11 +482,7 @@ const queryMany = (data, compiled) => {
467
482
  if (recursive.length > 0) {
468
483
  const firsts = recursive.map((i) => compiled[i].steps[0]);
469
484
  const seeds = recursive.map(() => []);
470
- const root = { value: data, parent: void 0, key: void 0 };
471
- walkDescendants(root, (node) => {
472
- for (let r = 0; r < recursive.length; r++)
473
- applySelector(node, firsts[r].selector, data, seeds[r]);
474
- });
485
+ seedDescents(data, firsts, seeds);
475
486
  for (let r = 0; r < recursive.length; r++) {
476
487
  const c = compiled[recursive[r]];
477
488
  out[recursive[r]] = toMatches(applySteps(data, seeds[r], c.steps.slice(1)));
@@ -479,6 +490,53 @@ const queryMany = (data, compiled) => {
479
490
  }
480
491
  return out;
481
492
  };
493
+ const seedDescents = (data, firsts, seeds) => {
494
+ const byName = /* @__PURE__ */ new Map();
495
+ const numericChild = [];
496
+ const others = [];
497
+ for (let r = 0; r < firsts.length; r++) {
498
+ const selector = firsts[r].selector;
499
+ if (selector.kind !== "child") {
500
+ others.push(r);
501
+ continue;
502
+ }
503
+ const existing = byName.get(selector.name);
504
+ if (existing === void 0)
505
+ byName.set(selector.name, [r]);
506
+ else
507
+ existing.push(r);
508
+ if (selector.index !== void 0)
509
+ numericChild.push(r);
510
+ }
511
+ const stack = [{ value: data, parent: void 0, key: void 0 }];
512
+ while (stack.length > 0) {
513
+ const current = stack.pop();
514
+ const value = current.value;
515
+ if (Array.isArray(value)) {
516
+ for (let idx = value.length - 1; idx >= 0; idx--)
517
+ stack.push({ value: value[idx], parent: current, key: idx });
518
+ for (const r of numericChild)
519
+ applySelector(current, firsts[r].selector, data, seeds[r]);
520
+ } else if (isObject(value)) {
521
+ const base = stack.length;
522
+ for (const key of Object.keys(value)) {
523
+ const child = { value: value[key], parent: current, key };
524
+ stack.push(child);
525
+ const interested = byName.get(key);
526
+ if (interested !== void 0)
527
+ for (const r of interested)
528
+ seeds[r].push(child);
529
+ }
530
+ for (let i = base, j = stack.length - 1; i < j; i++, j--) {
531
+ const held = stack[i];
532
+ stack[i] = stack[j];
533
+ stack[j] = held;
534
+ }
535
+ }
536
+ for (const r of others)
537
+ applySelector(current, firsts[r].selector, data, seeds[r]);
538
+ }
539
+ };
482
540
  const query = (data, expression) => queryCompiled(data, compileQuery(expression));
483
541
  export {
484
542
  compileQuery,
@@ -1,7 +1,7 @@
1
1
  import { type Document, type IDocumentOptions } from './document.js';
2
2
  import type { LintPlugin } from './plugin.js';
3
3
  import type { Ruleset } from './ruleset.js';
4
- import { type IDiagnostic, type ISourceSet } from './types.js';
4
+ import type { IDiagnostic, ISourceSet } from './types.js';
5
5
  /**
6
6
  * What a {@link LintResolver} returns: the dereferenced tree, an optional source
7
7
  * set mapping inlined nodes back to their origin, and any diagnostics raised
@@ -20,6 +20,13 @@ export type LintResolverResult = {
20
20
  * example wrapping `@amritk/resolve-refs`). Returning no `sources` means
21
21
  * findings map back to the root document only; `diagnostics` reports refs that
22
22
  * failed to resolve.
23
+ *
24
+ * The `resolved` tree must be **acyclic**. Rules are evaluated by walking it, and
25
+ * the walk is depth-unbounded by design (document depth is attacker-controlled,
26
+ * so it keeps its own stack rather than recursing) — which means a cycle is an
27
+ * infinite loop, not a `RangeError`. Shared subtrees are fine and expected;
28
+ * `@amritk/resolve-refs` keeps the `$ref` node in place when a chain would close
29
+ * a cycle, which is what makes its output safe here.
23
30
  */
24
31
  export type LintResolver = (document: Document, context: {
25
32
  input: string;
package/dist/core/lint.js CHANGED
@@ -1,29 +1,15 @@
1
1
  import { detectFormat } from "../parsers/index.js";
2
2
  import { createDocument } from "./document.js";
3
+ import { bySourceThenPosition, withoutDuplicates } from "./order.js";
3
4
  import { runPlugins } from "./plugin.js";
4
5
  import { createLinter } from "./runner.js";
5
- import { DiagnosticSeverity } from "./types.js";
6
+ import { severityFromName } from "./severity.js";
6
7
  const toParserSeverity = (value) => {
7
8
  if (value === void 0)
8
9
  return void 0;
9
10
  if (typeof value === "number")
10
11
  return value;
11
- if (value === "off")
12
- return "off";
13
- const names = {
14
- error: DiagnosticSeverity.Error,
15
- warn: DiagnosticSeverity.Warning,
16
- info: DiagnosticSeverity.Information,
17
- hint: DiagnosticSeverity.Hint
18
- };
19
- return names[value];
20
- };
21
- const byPosition = (a, b) => {
22
- const sa = a.source ?? "";
23
- const sb = b.source ?? "";
24
- if (sa !== sb)
25
- return sa < sb ? -1 : 1;
26
- return a.range.start.line - b.range.start.line || a.range.start.character - b.range.start.character;
12
+ return severityFromName(value);
27
13
  };
28
14
  const lintWithResult = async (input, options) => {
29
15
  const { ruleset, resolve, skip, plugins, ...documentOptions } = options;
@@ -59,7 +45,7 @@ const lintWithResult = async (input, options) => {
59
45
  result.source = document.source;
60
46
  return result;
61
47
  });
62
- const diagnostics = [...parserResults, ...resolverDiagnostics, ...ruleResults].sort(byPosition);
48
+ const diagnostics = withoutDuplicates([...parserResults, ...resolverDiagnostics, ...ruleResults].sort(bySourceThenPosition));
63
49
  if (!plugins || plugins.length === 0)
64
50
  return { diagnostics, pluginData: {} };
65
51
  const format = documentOptions.format ?? detectFormat(input);
@@ -0,0 +1,30 @@
1
+ import type { IDiagnostic } from './types.js';
2
+ /**
3
+ * Orders findings by source, then line, then character.
4
+ *
5
+ * Sorting by position alone interleaves findings from different files once a run
6
+ * spans multiple sources; grouping by `source` first keeps each file's findings
7
+ * contiguous. The runner and the lint pipeline both sort — the runner over the
8
+ * rule results, the pipeline over those merged with parser and resolver
9
+ * diagnostics — and they have to agree, so they share this comparator rather
10
+ * than each carrying a copy.
11
+ */
12
+ export declare const bySourceThenPosition: (a: IDiagnostic, b: IDiagnostic) => number;
13
+ /**
14
+ * Drops findings that name the same problem at the same authored node: same
15
+ * rule, severity, message, path, range and source.
16
+ *
17
+ * A rule with `resolved: true` walks the dereferenced tree, where one reusable
18
+ * `components` entry appears once per `$ref` reaching it *and* once at its
19
+ * declaration. Each copy is reported against the authored node it came from (see
20
+ * `locate`), so the copies are identical in every field a reader sees — and the
21
+ * second and third tell them nothing the first did not.
22
+ *
23
+ * The `path` is part of the key, and load-bearing. Ranges alone do not identify
24
+ * a node: `getLocationForJsonPath` falls back to the closest existing ancestor,
25
+ * so findings about *absent* sibling fields — `info.contact` missing `name`,
26
+ * `url` and `email` — all carry the enclosing object's range. They are three
27
+ * distinct problems and keep three distinct paths, so they survive. An earlier
28
+ * version of this keyed on the range alone and collapsed them into one.
29
+ */
30
+ export declare const withoutDuplicates: (diagnostics: readonly IDiagnostic[]) => IDiagnostic[];
@@ -0,0 +1,36 @@
1
+ const bySourceThenPosition = (a, b) => {
2
+ const sourceA = a.source ?? "";
3
+ const sourceB = b.source ?? "";
4
+ if (sourceA !== sourceB)
5
+ return sourceA < sourceB ? -1 : 1;
6
+ return a.range.start.line - b.range.start.line || a.range.start.character - b.range.start.character;
7
+ };
8
+ const withoutDuplicates = (diagnostics) => {
9
+ const seen = /* @__PURE__ */ new Set();
10
+ const kept = [];
11
+ for (const diagnostic of diagnostics) {
12
+ const { start, end } = diagnostic.range;
13
+ const key = [
14
+ diagnostic.source ?? "",
15
+ String(diagnostic.code),
16
+ diagnostic.severity,
17
+ start.line,
18
+ start.character,
19
+ end.line,
20
+ end.character,
21
+ // Segments are joined with a character a JSON key cannot contain
22
+ // unescaped, so `['a.b']` and `['a','b']` do not collide.
23
+ diagnostic.path.map(String).join("\0"),
24
+ diagnostic.message
25
+ ].join("");
26
+ if (seen.has(key))
27
+ continue;
28
+ seen.add(key);
29
+ kept.push(diagnostic);
30
+ }
31
+ return kept;
32
+ };
33
+ export {
34
+ bySourceThenPosition,
35
+ withoutDuplicates
36
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Own-property reads and writes for maps keyed by names the ruleset or the
3
+ * linted document supplies.
4
+ *
5
+ * Those names are author-chosen, so `constructor`, `toString` and `__proto__`
6
+ * are all names someone may legitimately use — and a bare index answers them
7
+ * from `Object.prototype` while a bare assignment on `__proto__` sets the map's
8
+ * prototype instead of adding a key. Neither is ever what the caller meant.
9
+ *
10
+ * `@amritk/helpers` carries the same pair, but `@amritk/lint` depends on
11
+ * nothing beyond `@amritk/runtime-validators` and `@amritk/yaml` by design (see
12
+ * `.claude/architecture.md`), so it keeps its own. One copy inside the package,
13
+ * not one per call site.
14
+ */
15
+ /** The value at `key`, or `undefined` when the map does not own that name. */
16
+ export declare const ownKey: <T>(source: Readonly<Record<string, T>>, key: string) => T | undefined;
17
+ /** Assigns `value` under `key` as an own data property, `__proto__` included. */
18
+ export declare const setOwnKey: <T>(target: Record<string, T>, key: string, value: NoInfer<T>) => void;
@@ -0,0 +1,12 @@
1
+ const ownKey = (source, key) => Object.hasOwn(source, key) ? source[key] : void 0;
2
+ const setOwnKey = (target, key, value) => {
3
+ if (key === "__proto__") {
4
+ Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
5
+ } else {
6
+ target[key] = value;
7
+ }
8
+ };
9
+ export {
10
+ ownKey,
11
+ setOwnKey
12
+ };
@@ -6,7 +6,7 @@ export declare const pointerToPath: (pointer: string) => JsonPath | undefined;
6
6
  * equivalent path in the *original* source document, following internal `$ref`s.
7
7
  * This lets findings produced against the resolved tree resolve to the exact
8
8
  * line:column of the original `$ref` target. External refs stop the walk at the
9
- * `$ref` site.
9
+ * `$ref` site, and so does a segment written as a sibling of the `$ref`.
10
10
  */
11
11
  export declare const resolveSourcePath: (root: unknown, path: JsonPath) => JsonPath;
12
12
  /**
@@ -19,7 +19,8 @@ export declare const resolveSourcePath: (root: unknown, path: JsonPath) => JsonP
19
19
  *
20
20
  * The walk re-derives the resolver's traversal over the *unresolved* documents,
21
21
  * so a `$ref` whose target file is not in the registry (e.g. it failed to load)
22
- * stops the walk at the last known location rather than guessing. Prefer
22
+ * stops the walk at the last known location rather than guessing, and a segment
23
+ * written as a sibling of the `$ref` is read at the `$ref` site. Prefer
23
24
  * {@link resolveSourceOriginFromMap} when the resolver supplies an origin map —
24
25
  * it avoids re-deriving this traversal.
25
26
  */
@@ -1,5 +1,6 @@
1
1
  import { dirname, resolve as resolvePath } from "node:path";
2
2
  const isContainer = (value) => typeof value === "object" && value !== null;
3
+ const childOf = (node, segment) => Object.hasOwn(node, segment) ? node[segment] : void 0;
3
4
  const decodeSegment = (segment) => {
4
5
  let decoded = segment;
5
6
  try {
@@ -20,8 +21,13 @@ const isRemote = (location) => /^https?:\/\//i.test(location);
20
21
  const joinLocation = (base, ref) => {
21
22
  if (isRemote(ref))
22
23
  return ref;
23
- if (isRemote(base))
24
- return new URL(ref, base).href;
24
+ if (isRemote(base)) {
25
+ try {
26
+ return new URL(ref, base).href;
27
+ } catch {
28
+ return void 0;
29
+ }
30
+ }
25
31
  return resolvePath(dirname(base), ref);
26
32
  };
27
33
  const splitRef = (ref) => {
@@ -41,30 +47,31 @@ const getAtPath = (root, path) => {
41
47
  for (const segment of path) {
42
48
  if (!isContainer(node))
43
49
  return void 0;
44
- node = node[segment];
50
+ node = childOf(node, segment);
45
51
  }
46
52
  return node;
47
53
  };
54
+ const stepsThroughRef = (node, next) => isContainer(node) && !Array.isArray(node) && typeof node["$ref"] === "string" && (next === void 0 || !Object.hasOwn(node, next));
48
55
  const resolveSourcePath = (root, path) => {
49
56
  let originalPath = [];
50
57
  let node = root;
51
- const followRefs = () => {
58
+ const followRefs = (next) => {
52
59
  let guard = 0;
53
- while (isContainer(node) && !Array.isArray(node) && typeof node["$ref"] === "string" && guard++ < 100) {
54
- const target = pointerToPath(node["$ref"]);
60
+ while (stepsThroughRef(node, next) && guard++ < 100) {
61
+ const target = pointerToPath(node.$ref);
55
62
  if (!target)
56
63
  return;
57
64
  originalPath = [...target];
58
65
  node = getAtPath(root, target);
59
66
  }
60
67
  };
61
- followRefs();
62
- for (const segment of path) {
68
+ followRefs(path[0]);
69
+ for (const [index, segment] of path.entries()) {
63
70
  if (!isContainer(node))
64
71
  break;
65
72
  originalPath.push(segment);
66
- node = node[segment];
67
- followRefs();
73
+ node = childOf(node, segment);
74
+ followRefs(path[index + 1]);
68
75
  }
69
76
  return originalPath;
70
77
  };
@@ -73,12 +80,14 @@ const resolveSourceOrigin = (registry, path) => {
73
80
  let root = registry.get(location)?.data;
74
81
  let originalPath = [];
75
82
  let node = root;
76
- const followRefs = () => {
83
+ const followRefs = (next) => {
77
84
  let guard = 0;
78
- while (isContainer(node) && !Array.isArray(node) && typeof node["$ref"] === "string" && guard++ < 1e3) {
79
- const { filePart, fragment } = splitRef(node["$ref"]);
85
+ while (stepsThroughRef(node, next) && guard++ < 1e3) {
86
+ const { filePart, fragment } = splitRef(node.$ref);
80
87
  if (filePart !== "") {
81
88
  const target = joinLocation(location, filePart);
89
+ if (target === void 0)
90
+ return;
82
91
  const doc = registry.get(target);
83
92
  if (!doc)
84
93
  return;
@@ -89,13 +98,13 @@ const resolveSourceOrigin = (registry, path) => {
89
98
  node = getAtPath(root, originalPath);
90
99
  }
91
100
  };
92
- followRefs();
93
- for (const segment of path) {
101
+ followRefs(path[0]);
102
+ for (const [index, segment] of path.entries()) {
94
103
  if (!isContainer(node))
95
104
  break;
96
105
  originalPath.push(segment);
97
- node = node[segment];
98
- followRefs();
106
+ node = childOf(node, segment);
107
+ followRefs(path[index + 1]);
99
108
  }
100
109
  return { location, path: originalPath };
101
110
  };
@@ -106,7 +115,7 @@ const resolveSourceOriginFromMap = (resolved, origins, rootLocation, path) => {
106
115
  for (const segment of path) {
107
116
  if (!isContainer(node))
108
117
  break;
109
- node = node[segment];
118
+ node = childOf(node, segment);
110
119
  const stamp = isContainer(node) ? origins.get(node) : void 0;
111
120
  if (stamp) {
112
121
  location = stamp.location;
@@ -1,19 +1,14 @@
1
1
  import { DiagnosticSeverity } from "../parsers/index.js";
2
2
  import { matchesGlob } from "./glob.js";
3
3
  import { compileQuery } from "./jsonpath.js";
4
- const SEVERITY_NAMES = {
5
- error: DiagnosticSeverity.Error,
6
- warn: DiagnosticSeverity.Warning,
7
- info: DiagnosticSeverity.Information,
8
- hint: DiagnosticSeverity.Hint,
9
- off: "off"
10
- };
4
+ import { ownKey, setOwnKey } from "./own-key.js";
5
+ import { severityFromName } from "./severity.js";
11
6
  const parseSeverity = (value) => {
12
7
  if (value === void 0)
13
8
  return { severity: DiagnosticSeverity.Warning, enabled: true };
14
9
  if (typeof value === "number")
15
10
  return { severity: value, enabled: true };
16
- const mapped = SEVERITY_NAMES[value];
11
+ const mapped = severityFromName(value);
17
12
  if (mapped === "off")
18
13
  return { severity: DiagnosticSeverity.Warning, enabled: false };
19
14
  if (mapped === void 0)
@@ -21,11 +16,42 @@ const parseSeverity = (value) => {
21
16
  return { severity: mapped, enabled: true };
22
17
  };
23
18
  const toArray = (value) => Array.isArray(value) ? value : [value];
19
+ const isRuleDefinition = (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry);
20
+ const assertRuleEntry = (name, entry) => {
21
+ if (typeof entry === "boolean" || typeof entry === "number" || typeof entry === "string")
22
+ return;
23
+ if (isRuleDefinition(entry))
24
+ return;
25
+ const kind = entry === null ? "null" : Array.isArray(entry) ? "an array" : `\`${typeof entry}\``;
26
+ throw new Error(`Rule "${name}" must be a rule definition, a boolean, or a severity \u2014 got ${kind}`);
27
+ };
28
+ const assertFormats = (name, formats) => {
29
+ if (formats === void 0 || Array.isArray(formats))
30
+ return;
31
+ throw new Error(`Rule "${name}" has an invalid \`formats\`: expected an array of format names`);
32
+ };
33
+ const assertGiven = (name, given) => {
34
+ if (given.length === 0)
35
+ throw new Error(`Rule "${name}" is missing \`given\``);
36
+ for (const expression of given) {
37
+ if (typeof expression !== "string") {
38
+ const kind = expression === null ? "null" : `\`${typeof expression}\``;
39
+ throw new Error(`Rule "${name}" has an invalid \`given\`: expected a JSONPath string, got ${kind}`);
40
+ }
41
+ if (expression.startsWith("#"))
42
+ continue;
43
+ const compiled = compileQuery(expression);
44
+ if (compiled.error !== void 0) {
45
+ throw new Error(`Rule "${name}" has an invalid \`given\` "${expression}": ${compiled.error}`);
46
+ }
47
+ }
48
+ };
24
49
  const normalizeRule = (name, def, modifier, declaring = {}) => {
25
50
  const { severity, enabled } = parseSeverity(def.severity);
26
51
  const recommended = def.recommended ?? true;
27
52
  const modifierEnabled = modifier === "off" ? false : modifier === "recommended" ? recommended : true;
28
53
  const formats = def.formats ?? declaring.formats;
54
+ assertFormats(name, formats);
29
55
  const documentationUrl = def.documentationUrl ?? (declaring.documentationUrl ? `${declaring.documentationUrl}#${name}` : void 0);
30
56
  return {
31
57
  name,
@@ -33,8 +59,11 @@ const normalizeRule = (name, def, modifier, declaring = {}) => {
33
59
  message: def.message,
34
60
  severity,
35
61
  enabled: enabled && modifierEnabled,
36
- given: toArray(def.given),
37
- then: toArray(def.then),
62
+ // `?? []` so a definition missing either field normalizes to "nothing to do"
63
+ // instead of a one-element `[undefined]` that blows up further downstream;
64
+ // `assertGiven` is what reports a missing `given` as the authoring error it is.
65
+ given: toArray(def.given ?? []),
66
+ then: toArray(def.then ?? []),
38
67
  formats: formats ? new Set(formats) : void 0,
39
68
  recommended,
40
69
  resolved: def.resolved ?? true,
@@ -42,6 +71,7 @@ const normalizeRule = (name, def, modifier, declaring = {}) => {
42
71
  };
43
72
  };
44
73
  const applyEntry = (rules, name, entry, throwOnMissing = false) => {
74
+ assertRuleEntry(name, entry);
45
75
  if (typeof entry === "boolean") {
46
76
  const existing = rules.get(name);
47
77
  if (existing)
@@ -50,7 +80,7 @@ const applyEntry = (rules, name, entry, throwOnMissing = false) => {
50
80
  throw new Error(`Cannot extend non-existing rule "${name}"`);
51
81
  return;
52
82
  }
53
- if (typeof entry === "string") {
83
+ if (typeof entry === "string" || typeof entry === "number") {
54
84
  const existing = rules.get(name);
55
85
  if (!existing) {
56
86
  if (throwOnMissing)
@@ -65,19 +95,19 @@ const applyEntry = (rules, name, entry, throwOnMissing = false) => {
65
95
  rules.set(name, normalizeRule(name, entry, "all"));
66
96
  };
67
97
  const registerAlias = (ctx, declaringAliases, name, ruleName) => {
68
- const alias = declaringAliases[name];
98
+ const alias = ownKey(declaringAliases, name);
69
99
  if (!alias)
70
100
  throw new Error(`Rule "${ruleName}" references undefined alias "#${name}"`);
71
- const existing = ctx.aliases[name];
101
+ const existing = ownKey(ctx.aliases, name);
72
102
  if (existing === void 0 || existing === alias) {
73
- ctx.aliases[name] = alias;
103
+ setOwnKey(ctx.aliases, name, alias);
74
104
  return name;
75
105
  }
76
106
  for (let n = 0; ; n++) {
77
107
  const key = `${name}__${n}`;
78
- const at = ctx.aliases[key];
108
+ const at = ownKey(ctx.aliases, key);
79
109
  if (at === void 0 || at === alias) {
80
- ctx.aliases[key] = alias;
110
+ setOwnKey(ctx.aliases, key, alias);
81
111
  return key;
82
112
  }
83
113
  }
@@ -101,6 +131,10 @@ const collectExtends = (extendsValue, defaultModifier, basePath, ctx) => {
101
131
  } else {
102
132
  target = entry;
103
133
  }
134
+ if (typeof target !== "string" && (typeof target !== "object" || target === null)) {
135
+ const kind = target === null ? "null" : `\`${typeof target}\``;
136
+ throw new Error(`An \`extends\` entry must be a ruleset object or a reference string \u2014 got ${kind}`);
137
+ }
104
138
  if (typeof target === "string") {
105
139
  const key = `${basePath}\0${target}`;
106
140
  if (ctx.seen.has(key))
@@ -131,7 +165,7 @@ const mergeInto = (definition, modifier, extendsModifier, basePath, ctx) => {
131
165
  };
132
166
  if (definition.rules) {
133
167
  for (const [name, entry] of Object.entries(definition.rules)) {
134
- if (typeof entry === "object") {
168
+ if (isRuleDefinition(entry)) {
135
169
  const rule = normalizeRule(name, entry, modifier, declaring);
136
170
  rule.given = rewriteGiven(ctx, definition.aliases ?? {}, name, rule.given);
137
171
  ctx.into.set(name, rule);
@@ -150,9 +184,14 @@ const cloneRule = (rule) => ({
150
184
  formats: rule.formats ? new Set(rule.formats) : void 0
151
185
  });
152
186
  const createRuleset = (definition, options = {}) => {
187
+ if (typeof definition !== "object" || definition === null || Array.isArray(definition)) {
188
+ throw new Error("A ruleset must be an object");
189
+ }
153
190
  const functions = options.functions ?? {};
154
191
  const formats = options.formats ?? {};
155
192
  const overrides = definition.overrides ?? [];
193
+ if (!Array.isArray(overrides))
194
+ throw new Error("`overrides` must be an array");
156
195
  const ctx = {
157
196
  into: /* @__PURE__ */ new Map(),
158
197
  resolve: options.resolve,
@@ -167,18 +206,22 @@ const createRuleset = (definition, options = {}) => {
167
206
  mergeInto(definition, "all", "recommended", options.basePath ?? process.cwd(), ctx);
168
207
  const rules = [...ctx.into.values()];
169
208
  const aliases = ctx.aliases;
170
- for (const rule of rules) {
171
- for (const expression of rule.given) {
172
- if (expression.startsWith("#"))
173
- continue;
174
- const compiled = compileQuery(expression);
175
- if (compiled.error !== void 0) {
176
- throw new Error(`Rule "${rule.name}" has an invalid \`given\` "${expression}": ${compiled.error}`);
177
- }
209
+ for (const rule of rules)
210
+ assertGiven(rule.name, rule.given);
211
+ for (const [index, override] of overrides.entries()) {
212
+ if (!Array.isArray(override?.files)) {
213
+ throw new Error(`Override at index ${index} must have a \`files\` array of globs`);
214
+ }
215
+ if (!override.rules)
216
+ continue;
217
+ for (const [name, entry] of Object.entries(override.rules)) {
218
+ assertRuleEntry(name, entry);
219
+ if (isRuleDefinition(entry))
220
+ assertGiven(name, toArray(entry.given ?? []));
178
221
  }
179
222
  }
180
223
  const resolveAlias = (name, documentFormats) => {
181
- const alias = aliases[name];
224
+ const alias = ownKey(aliases, name);
182
225
  if (!alias)
183
226
  return [];
184
227
  if (Array.isArray(alias))
@@ -199,7 +242,11 @@ const createRuleset = (definition, options = {}) => {
199
242
  overrides,
200
243
  parserOptions,
201
244
  enabledRules: rules.filter((rule) => rule.enabled),
202
- getFunction: (name) => functions[name],
245
+ // `Object.hasOwn`, not a bare index: `then.function` is ruleset input, so
246
+ // `"toString"` otherwise resolved to `Function.prototype.toString` and ran
247
+ // as a rule function instead of being reported as unknown — its string
248
+ // return value then read as an iterable of per-character diagnostics.
249
+ getFunction: (name) => ownKey(functions, name),
203
250
  rulesForSource: (source) => {
204
251
  if (!source || !hasOverrides)
205
252
  return rules;