@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,121 +1,107 @@
1
- import { compileQuery } from './jsonpath.js';
2
- const SEVERITIES = new Set(['error', 'warn', 'info', 'hint', 'off']);
3
- const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
1
+ import { compileQuery } from "./jsonpath.js";
2
+ const SEVERITIES = /* @__PURE__ */ new Set(["error", "warn", "info", "hint", "off"]);
3
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4
4
  const isValidSeverity = (value) => {
5
- if (typeof value === 'number')
6
- return Number.isInteger(value) && value >= 0 && value <= 3;
7
- return typeof value === 'string' && SEVERITIES.has(value);
5
+ if (typeof value === "number")
6
+ return Number.isInteger(value) && value >= 0 && value <= 3;
7
+ return typeof value === "string" && SEVERITIES.has(value);
8
8
  };
9
9
  const validateThen = (then, path, problems) => {
10
- const entries = Array.isArray(then) ? then : [then];
11
- entries.forEach((entry, index) => {
12
- const at = Array.isArray(then) ? [...path, index] : path;
13
- if (!isObject(entry)) {
14
- problems.push({ message: '`then` must be an object (or array of objects)', path: at });
15
- return;
16
- }
17
- const fn = entry['function'];
18
- if (typeof fn !== 'string' && typeof fn !== 'function') {
19
- problems.push({ message: '`then.function` must be a function name or reference', path: [...at, 'function'] });
20
- }
21
- if (entry['field'] !== undefined && typeof entry['field'] !== 'string') {
22
- problems.push({ message: '`then.field` must be a string', path: [...at, 'field'] });
23
- }
24
- });
25
- };
26
- const validateRule = (name, entry, path, problems) => {
27
- // Shorthand: boolean toggle or a severity string.
28
- if (typeof entry === 'boolean')
29
- return;
30
- if (typeof entry === 'string') {
31
- if (!isValidSeverity(entry)) {
32
- problems.push({ message: `Rule "${name}" has invalid severity "${entry}"`, path });
33
- }
34
- return;
35
- }
10
+ const entries = Array.isArray(then) ? then : [then];
11
+ entries.forEach((entry, index) => {
12
+ const at = Array.isArray(then) ? [...path, index] : path;
36
13
  if (!isObject(entry)) {
37
- problems.push({ message: `Rule "${name}" must be an object, boolean, or severity string`, path });
38
- return;
14
+ problems.push({ message: "`then` must be an object (or array of objects)", path: at });
15
+ return;
39
16
  }
40
- if (entry['given'] === undefined) {
41
- problems.push({ message: `Rule "${name}" is missing \`given\``, path: [...path, 'given'] });
17
+ const fn = entry["function"];
18
+ if (typeof fn !== "string" && typeof fn !== "function") {
19
+ problems.push({ message: "`then.function` must be a function name or reference", path: [...at, "function"] });
42
20
  }
43
- else if (typeof entry['given'] !== 'string' && !Array.isArray(entry['given'])) {
44
- problems.push({ message: `Rule "${name}" \`given\` must be a string or array`, path: [...path, 'given'] });
45
- }
46
- else {
47
- // Flag a malformed JSONPath expression so it does not silently match nothing
48
- // at run time. Alias references (`#Alias`) are only valid once expanded, so
49
- // they are skipped here.
50
- const givens = Array.isArray(entry['given']) ? entry['given'] : [entry['given']];
51
- givens.forEach((given, index) => {
52
- if (typeof given !== 'string' || given.startsWith('#'))
53
- return;
54
- const error = compileQuery(given).error;
55
- if (error !== undefined) {
56
- const at = Array.isArray(entry['given']) ? [...path, 'given', index] : [...path, 'given'];
57
- problems.push({ message: `Rule "${name}" has an invalid \`given\` "${given}": ${error}`, path: at });
58
- }
59
- });
60
- }
61
- if (entry['then'] === undefined) {
62
- problems.push({ message: `Rule "${name}" is missing \`then\``, path: [...path, 'then'] });
63
- }
64
- else {
65
- validateThen(entry['then'], [...path, 'then'], problems);
66
- }
67
- if (entry['severity'] !== undefined && !isValidSeverity(entry['severity'])) {
68
- problems.push({ message: `Rule "${name}" has invalid severity`, path: [...path, 'severity'] });
69
- }
70
- if (entry['formats'] !== undefined && !Array.isArray(entry['formats'])) {
71
- problems.push({ message: `Rule "${name}" \`formats\` must be an array`, path: [...path, 'formats'] });
21
+ if (entry["field"] !== void 0 && typeof entry["field"] !== "string") {
22
+ problems.push({ message: "`then.field` must be a string", path: [...at, "field"] });
72
23
  }
24
+ });
73
25
  };
74
- /**
75
- * Validates the *shape* of a ruleset definition, returning a list of problems
76
- * (empty when valid). This is a lightweight structural check — it does not load
77
- * `extends` targets or verify that referenced functions exist — so a malformed
78
- * ruleset surfaces actionable diagnostics instead of failing obscurely at runtime.
79
- */
80
- export const validateRuleset = (definition) => {
81
- const problems = [];
82
- if (!isObject(definition)) {
83
- return [{ message: 'Ruleset must be an object', path: [] }];
84
- }
85
- if (definition['rules'] !== undefined) {
86
- if (!isObject(definition['rules'])) {
87
- problems.push({ message: '`rules` must be an object', path: ['rules'] });
88
- }
89
- else {
90
- for (const [name, entry] of Object.entries(definition['rules'])) {
91
- validateRule(name, entry, ['rules', name], problems);
92
- }
93
- }
94
- }
95
- const ext = definition['extends'];
96
- if (ext !== undefined && typeof ext !== 'string' && !Array.isArray(ext) && !isObject(ext)) {
97
- problems.push({ message: '`extends` must be a string, array, or object', path: ['extends'] });
98
- }
99
- if (definition['overrides'] !== undefined && !Array.isArray(definition['overrides'])) {
100
- problems.push({ message: '`overrides` must be an array', path: ['overrides'] });
101
- }
102
- else if (Array.isArray(definition['overrides'])) {
103
- definition['overrides'].forEach((override, index) => {
104
- if (!isObject(override) || !Array.isArray(override['files'])) {
105
- problems.push({ message: 'Each override must have a `files` array', path: ['overrides', index] });
106
- }
107
- });
108
- }
109
- if (definition['functions'] !== undefined && !Array.isArray(definition['functions'])) {
110
- problems.push({ message: '`functions` must be an array of names', path: ['functions'] });
111
- }
112
- if (definition['formats'] !== undefined && !Array.isArray(definition['formats'])) {
113
- problems.push({ message: '`formats` must be an array', path: ['formats'] });
114
- }
115
- // A ruleset with neither rules nor extends does nothing — flag it.
116
- if (definition.rules === undefined &&
117
- definition.extends === undefined) {
118
- problems.push({ message: 'Ruleset has no `rules` and no `extends` (it will produce no findings)', path: [] });
119
- }
120
- return problems;
26
+ const validateRule = (name, entry, path, problems) => {
27
+ if (typeof entry === "boolean")
28
+ return;
29
+ if (typeof entry === "string") {
30
+ if (!isValidSeverity(entry)) {
31
+ problems.push({ message: `Rule "${name}" has invalid severity "${entry}"`, path });
32
+ }
33
+ return;
34
+ }
35
+ if (!isObject(entry)) {
36
+ problems.push({ message: `Rule "${name}" must be an object, boolean, or severity string`, path });
37
+ return;
38
+ }
39
+ if (entry["given"] === void 0) {
40
+ problems.push({ message: `Rule "${name}" is missing \`given\``, path: [...path, "given"] });
41
+ } else if (typeof entry["given"] !== "string" && !Array.isArray(entry["given"])) {
42
+ problems.push({ message: `Rule "${name}" \`given\` must be a string or array`, path: [...path, "given"] });
43
+ } else {
44
+ const givens = Array.isArray(entry["given"]) ? entry["given"] : [entry["given"]];
45
+ givens.forEach((given, index) => {
46
+ if (typeof given !== "string" || given.startsWith("#"))
47
+ return;
48
+ const error = compileQuery(given).error;
49
+ if (error !== void 0) {
50
+ const at = Array.isArray(entry["given"]) ? [...path, "given", index] : [...path, "given"];
51
+ problems.push({ message: `Rule "${name}" has an invalid \`given\` "${given}": ${error}`, path: at });
52
+ }
53
+ });
54
+ }
55
+ if (entry["then"] === void 0) {
56
+ problems.push({ message: `Rule "${name}" is missing \`then\``, path: [...path, "then"] });
57
+ } else {
58
+ validateThen(entry["then"], [...path, "then"], problems);
59
+ }
60
+ if (entry["severity"] !== void 0 && !isValidSeverity(entry["severity"])) {
61
+ problems.push({ message: `Rule "${name}" has invalid severity`, path: [...path, "severity"] });
62
+ }
63
+ if (entry["formats"] !== void 0 && !Array.isArray(entry["formats"])) {
64
+ problems.push({ message: `Rule "${name}" \`formats\` must be an array`, path: [...path, "formats"] });
65
+ }
66
+ };
67
+ const validateRuleset = (definition) => {
68
+ const problems = [];
69
+ if (!isObject(definition)) {
70
+ return [{ message: "Ruleset must be an object", path: [] }];
71
+ }
72
+ if (definition["rules"] !== void 0) {
73
+ if (!isObject(definition["rules"])) {
74
+ problems.push({ message: "`rules` must be an object", path: ["rules"] });
75
+ } else {
76
+ for (const [name, entry] of Object.entries(definition["rules"])) {
77
+ validateRule(name, entry, ["rules", name], problems);
78
+ }
79
+ }
80
+ }
81
+ const ext = definition["extends"];
82
+ if (ext !== void 0 && typeof ext !== "string" && !Array.isArray(ext) && !isObject(ext)) {
83
+ problems.push({ message: "`extends` must be a string, array, or object", path: ["extends"] });
84
+ }
85
+ if (definition["overrides"] !== void 0 && !Array.isArray(definition["overrides"])) {
86
+ problems.push({ message: "`overrides` must be an array", path: ["overrides"] });
87
+ } else if (Array.isArray(definition["overrides"])) {
88
+ definition["overrides"].forEach((override, index) => {
89
+ if (!isObject(override) || !Array.isArray(override["files"])) {
90
+ problems.push({ message: "Each override must have a `files` array", path: ["overrides", index] });
91
+ }
92
+ });
93
+ }
94
+ if (definition["functions"] !== void 0 && !Array.isArray(definition["functions"])) {
95
+ problems.push({ message: "`functions` must be an array of names", path: ["functions"] });
96
+ }
97
+ if (definition["formats"] !== void 0 && !Array.isArray(definition["formats"])) {
98
+ problems.push({ message: "`formats` must be an array", path: ["formats"] });
99
+ }
100
+ if (definition.rules === void 0 && definition.extends === void 0) {
101
+ problems.push({ message: "Ruleset has no `rules` and no `extends` (it will produce no findings)", path: [] });
102
+ }
103
+ return problems;
104
+ };
105
+ export {
106
+ validateRuleset
121
107
  };
@@ -1,4 +1,4 @@
1
- import type { IDiagnostic } from '../core/index.js';
1
+ import type { IDiagnostic } from '../core/types.js';
2
2
  import { type ParserFormat } from '../parsers/index.js';
3
3
  import type { FixerRegistry, FixResult } from './types.js';
4
4
  /** Options for {@link applyFixes}. */
package/dist/fix/apply.js CHANGED
@@ -1,101 +1,63 @@
1
- import { applyEditOpsWithChanges } from '../parsers/index.js';
2
- /** The ops that structurally reshape an array, invalidating positional indices into it. */
3
- const STRUCTURAL_ARRAY_OPS = new Set(['removeItems', 'reorderArray', 'insertItem']);
1
+ import { applyEditOpsWithChanges } from "../parsers/index.js";
2
+ const STRUCTURAL_ARRAY_OPS = /* @__PURE__ */ new Set(["removeItems", "reorderArray", "insertItem"]);
4
3
  const isStructuralArrayOp = (op) => STRUCTURAL_ARRAY_OPS.has(op.op);
5
- /**
6
- * Whether `path` addresses (or reaches into) an array that an earlier op in this
7
- * batch already reshaped. After a `removeItems`/`reorderArray`/`insertItem`, the
8
- * element indices of that array are stale, so a second op that either targets the
9
- * same array or indexes into it by position would act on the wrong element. Such
10
- * an op is deferred to the next fixpoint pass, which re-derives indices from the
11
- * freshly-parsed document.
12
- */
13
4
  const touchesModifiedArray = (path, modified) => modified.some((array) => {
14
- if (array.length > path.length)
15
- return false;
16
- if (!array.every((segment, i) => segment === path[i]))
17
- return false;
18
- // Another structural op on the same array conflicts; a deeper op conflicts
19
- // only when it indexes the array by position (the stale part).
20
- return array.length === path.length || typeof path[array.length] === 'number';
5
+ if (array.length > path.length)
6
+ return false;
7
+ if (!array.every((segment, i) => segment === path[i]))
8
+ return false;
9
+ return array.length === path.length || typeof path[array.length] === "number";
21
10
  });
22
- /**
23
- * Computes the structural edits for every fixable finding in `diagnostics`, then
24
- * applies them to `input` in one pass. Edits from different findings that come
25
- * out identical (e.g. several "not alphabetical" findings on one array all asking
26
- * for the same reorder) are de-duplicated so the edit is applied once.
27
- *
28
- * `data` is the *unresolved* parsed document: fixers read the real node at a
29
- * finding's path to derive the edit, and edits whose path no longer resolves are
30
- * dropped — so a finding on an inlined `$ref` node simply isn't fixed rather than
31
- * corrupting the source.
32
- *
33
- * When two ops in one batch would both reshape the same array, only the first is
34
- * applied this pass; the second is *deferred* and its finding is left unreported,
35
- * so the surrounding fixpoint loop re-derives it against the already-edited
36
- * document (where the indices are fresh) on the next pass. A finding is reported
37
- * in `applied` only when *every* edit it contributed actually changed the text —
38
- * a partially-applied or deferred fix is retried rather than falsely counted.
39
- */
40
- export const applyFixes = (input, format, data, diagnostics, fixers, options = {}) => {
41
- const safeOnly = options.safeOnly !== false;
42
- // Gather each candidate finding's ops up front so we can reason about conflicts
43
- // across the whole batch before lowering anything to text.
44
- const candidates = [];
45
- for (const diagnostic of diagnostics) {
46
- const fixer = fixers[String(diagnostic.code)];
47
- if (!fixer)
48
- continue;
49
- if (safeOnly && fixer.safe === false)
50
- continue;
51
- const produced = fixer.fix({ diagnostic, data, format });
52
- if (!produced)
53
- continue;
54
- const ops = Array.isArray(produced) ? produced : [produced];
55
- if (ops.length === 0)
56
- continue;
57
- candidates.push({ fix: { code: diagnostic.code, path: diagnostic.path }, ops });
11
+ const applyFixes = (input, format, data, diagnostics, fixers, options = {}) => {
12
+ const safeOnly = options.safeOnly !== false;
13
+ const candidates = [];
14
+ for (const diagnostic of diagnostics) {
15
+ const fixer = fixers[String(diagnostic.code)];
16
+ if (!fixer)
17
+ continue;
18
+ if (safeOnly && fixer.safe === false)
19
+ continue;
20
+ const produced = fixer.fix({ diagnostic, data, format });
21
+ if (!produced)
22
+ continue;
23
+ const ops2 = Array.isArray(produced) ? produced : [produced];
24
+ if (ops2.length === 0)
25
+ continue;
26
+ candidates.push({ fix: { code: diagnostic.code, path: diagnostic.path }, ops: ops2 });
27
+ }
28
+ const ops = [];
29
+ const indexByKey = /* @__PURE__ */ new Map();
30
+ const modifiedArrays = [];
31
+ const planned = [];
32
+ for (const candidate of candidates) {
33
+ const indices = [];
34
+ let deferred = false;
35
+ for (const op of candidate.ops) {
36
+ const key = JSON.stringify(op);
37
+ const existing = indexByKey.get(key);
38
+ if (existing !== void 0) {
39
+ indices.push(existing);
40
+ continue;
41
+ }
42
+ if (touchesModifiedArray(op.path, modifiedArrays)) {
43
+ deferred = true;
44
+ continue;
45
+ }
46
+ const index = ops.length;
47
+ indexByKey.set(key, index);
48
+ ops.push(op);
49
+ if (isStructuralArrayOp(op))
50
+ modifiedArrays.push(op.path);
51
+ indices.push(index);
58
52
  }
59
- const ops = [];
60
- const indexByKey = new Map();
61
- const modifiedArrays = [];
62
- // Per candidate: the batch index of each op that made it into this pass, and
63
- // whether any op had to be deferred (which keeps the finding unreported so it
64
- // is retried once the earlier structural edit has landed).
65
- const planned = [];
66
- for (const candidate of candidates) {
67
- const indices = [];
68
- let deferred = false;
69
- for (const op of candidate.ops) {
70
- const key = JSON.stringify(op);
71
- // De-duplicate identical edits (e.g. several findings on one array all asking
72
- // for the same reorder): the same edit is applied — and counted — once, and
73
- // is never a conflict with itself.
74
- const existing = indexByKey.get(key);
75
- if (existing !== undefined) {
76
- indices.push(existing);
77
- continue;
78
- }
79
- // A distinct op that would reshape or index into an already-reshaped array
80
- // is deferred to the next pass, where indices are re-derived from fresh data.
81
- if (touchesModifiedArray(op.path, modifiedArrays)) {
82
- deferred = true;
83
- continue;
84
- }
85
- const index = ops.length;
86
- indexByKey.set(key, index);
87
- ops.push(op);
88
- if (isStructuralArrayOp(op))
89
- modifiedArrays.push(op.path);
90
- indices.push(index);
91
- }
92
- planned.push({ fix: candidate.fix, indices, deferred });
93
- }
94
- if (ops.length === 0)
95
- return { output: input, applied: [], changed: false };
96
- const { output, changed } = applyEditOpsWithChanges(input, format, ops);
97
- const applied = planned
98
- .filter((plan) => !plan.deferred && plan.indices.length > 0 && plan.indices.every((index) => changed[index]))
99
- .map((plan) => plan.fix);
100
- return { output, applied, changed: output !== input };
53
+ planned.push({ fix: candidate.fix, indices, deferred });
54
+ }
55
+ if (ops.length === 0)
56
+ return { output: input, applied: [], changed: false };
57
+ const { output, changed } = applyEditOpsWithChanges(input, format, ops);
58
+ const applied = planned.filter((plan) => !plan.deferred && plan.indices.length > 0 && plan.indices.every((index) => changed[index])).map((plan) => plan.fix);
59
+ return { output, applied, changed: output !== input };
60
+ };
61
+ export {
62
+ applyFixes
101
63
  };
package/dist/fix/index.js CHANGED
@@ -1,2 +1,7 @@
1
- export { applyFixes } from './apply.js';
2
- export { createFixPlugin, FIX_PLUGIN_NAME } from './plugin.js';
1
+ import { applyFixes } from "./apply.js";
2
+ import { createFixPlugin, FIX_PLUGIN_NAME } from "./plugin.js";
3
+ export {
4
+ FIX_PLUGIN_NAME,
5
+ applyFixes,
6
+ createFixPlugin
7
+ };
@@ -1,21 +1,16 @@
1
- import { applyFixes } from './apply.js';
2
- /** The name the fix plugin registers under (its `data` is surfaced here). */
3
- export const FIX_PLUGIN_NAME = 'fix';
4
- /**
5
- * Builds the auto-fix {@link LintPlugin} from a {@link FixerRegistry}. As a
6
- * post-lint plugin it reads the run's findings and the raw document text, applies
7
- * the fixers' edits, and returns the rewritten text as `output` plus the list of
8
- * repaired findings as `data`. The core engine stays unaware of fixing — remove
9
- * this plugin (and the `../fix` dependency) and Linter lints exactly as
10
- * before.
11
- */
12
- export const createFixPlugin = (fixers, options = {}) => ({
13
- name: FIX_PLUGIN_NAME,
14
- afterLint: (diagnostics, context) => {
15
- const result = applyFixes(context.input, context.format, context.document.data, diagnostics, fixers, options);
16
- if (!result.changed)
17
- return undefined;
18
- const data = { applied: result.applied };
19
- return { output: result.output, data };
20
- },
1
+ import { applyFixes } from "./apply.js";
2
+ const FIX_PLUGIN_NAME = "fix";
3
+ const createFixPlugin = (fixers, options = {}) => ({
4
+ name: FIX_PLUGIN_NAME,
5
+ afterLint: (diagnostics, context) => {
6
+ const result = applyFixes(context.input, context.format, context.document.data, diagnostics, fixers, options);
7
+ if (!result.changed)
8
+ return void 0;
9
+ const data = { applied: result.applied };
10
+ return { output: result.output, data };
11
+ }
21
12
  });
13
+ export {
14
+ FIX_PLUGIN_NAME,
15
+ createFixPlugin
16
+ };
@@ -1,4 +1,4 @@
1
- import type { IDiagnostic, JsonPath } from '../core/index.js';
1
+ import type { IDiagnostic, JsonPath } from '../core/types.js';
2
2
  import type { EditOp, ParserFormat } from '../parsers/index.js';
3
3
  export type { EditOp, ParserFormat };
4
4
  /** What a {@link Fixer} is handed for a single finding it might repair. */
@@ -1,4 +1,4 @@
1
- import type { RulesetFunction } from '../core/index.js';
1
+ import type { RulesetFunction } from '../core/types.js';
2
2
  /** Options for {@link alphabetical}. */
3
3
  export type IAlphabeticalOptions = {
4
4
  /** Compare objects by this property instead of the value itself. */
@@ -1,55 +1,44 @@
1
- const isRecord = (value) => typeof value === 'object' && value !== null;
2
- const isStringOrNumber = (value) => typeof value === 'string' || typeof value === 'number';
3
- /** A string made up only of digits, e.g. an integer-like object key such as "10". */
4
- const isIntegerLike = (value) => typeof value === 'string' && /^(?:0|[1-9]\d*)$/.test(value);
1
+ const isRecord = (value) => typeof value === "object" && value !== null;
2
+ const isStringOrNumber = (value) => typeof value === "string" || typeof value === "number";
3
+ const isIntegerLike = (value) => typeof value === "string" && /^(?:0|[1-9]\d*)$/.test(value);
5
4
  const compare = (a, b) => {
6
- // Deliberate deviation from Spectral, which relies on source order and falls
7
- // back to `localeCompare`: JavaScript enumerates integer-like object keys in
8
- // ascending numeric order ({ "2": …, "10": }), yet `localeCompare` sorts
9
- // "10" before "2" and would flag that natural key order as a violation.
10
- // Comparing integer-like strings numerically avoids that false positive.
11
- if (isIntegerLike(a) && isIntegerLike(b))
12
- return Math.sign(Number(a) - Number(b));
13
- // Match Spectral: when either side is a real number or a numeric string,
14
- // compare numerically so mixed inputs like [2, "10"] read as ordered.
15
- if ((typeof a === 'number' || !Number.isNaN(Number(a))) && (typeof b === 'number' || !Number.isNaN(Number(b)))) {
16
- return Math.min(1, Math.max(-1, Number(a) - Number(b)));
17
- }
18
- if (typeof a !== 'string' || typeof b !== 'string')
19
- return 0;
20
- return a.localeCompare(b);
5
+ if (isIntegerLike(a) && isIntegerLike(b))
6
+ return Math.sign(Number(a) - Number(b));
7
+ if ((typeof a === "number" || !Number.isNaN(Number(a))) && (typeof b === "number" || !Number.isNaN(Number(b)))) {
8
+ return Math.min(1, Math.max(-1, Number(a) - Number(b)));
9
+ }
10
+ if (typeof a !== "string" || typeof b !== "string")
11
+ return 0;
12
+ return a.localeCompare(b);
21
13
  };
22
- /** Flags array items or object keys that are not in ascending (optionally `keyedBy`) order. */
23
- export const alphabetical = (input, options, context) => {
24
- if (typeof input !== 'object' || input === null)
25
- return [];
26
- const isArray = Array.isArray(input);
27
- const rawItems = isArray ? input : Object.keys(input);
28
- const keyedBy = options?.keyedBy;
29
- // Resolve the actual comparands. With `keyedBy` we read a property off each
30
- // item, which is only meaningful when every item is an object; otherwise the
31
- // comparison would silently run against `undefined`. Surface the same explicit
32
- // findings Spectral does instead of producing a misleading order violation.
33
- const items = [];
34
- for (const item of rawItems) {
35
- if (keyedBy) {
36
- if (!isRecord(item))
37
- return [{ message: 'The value must be an object' }];
38
- items.push(item[keyedBy]);
39
- }
40
- else {
41
- items.push(item);
42
- }
43
- }
44
- if (!items.every(isStringOrNumber)) {
45
- return [{ message: 'The value must be one of the allowed types: number, string' }];
14
+ const alphabetical = (input, options, context) => {
15
+ if (typeof input !== "object" || input === null)
16
+ return [];
17
+ const isArray = Array.isArray(input);
18
+ const rawItems = isArray ? input : Object.keys(input);
19
+ const keyedBy = options?.keyedBy;
20
+ const items = [];
21
+ for (const item of rawItems) {
22
+ if (keyedBy) {
23
+ if (!isRecord(item))
24
+ return [{ message: "The value must be an object" }];
25
+ items.push(item[keyedBy]);
26
+ } else {
27
+ items.push(item);
46
28
  }
47
- const results = [];
48
- for (let i = 0; i < items.length - 1; i++) {
49
- if (compare(items[i], items[i + 1]) > 0) {
50
- const path = isArray ? [...context.path, i + 1] : [...context.path, rawItems[i + 1]];
51
- results.push({ message: 'The items must be in alphabetical order', path });
52
- }
29
+ }
30
+ if (!items.every(isStringOrNumber)) {
31
+ return [{ message: "The value must be one of the allowed types: number, string" }];
32
+ }
33
+ const results = [];
34
+ for (let i = 0; i < items.length - 1; i++) {
35
+ if (compare(items[i], items[i + 1]) > 0) {
36
+ const path = isArray ? [...context.path, i + 1] : [...context.path, rawItems[i + 1]];
37
+ results.push({ message: "The items must be in alphabetical order", path });
53
38
  }
54
- return results;
39
+ }
40
+ return results;
41
+ };
42
+ export {
43
+ alphabetical
55
44
  };
@@ -1,4 +1,4 @@
1
- import type { RulesetFunction } from '../core/index.js';
1
+ import type { RulesetFunction } from '../core/types.js';
2
2
  /** The supported casing styles a value can be checked against. */
3
3
  export type CasingType = 'flat' | 'camel' | 'pascal' | 'kebab' | 'cobol' | 'snake' | 'macro';
4
4
  /** Options for {@link casing}. */