@amritk/lint 0.0.0 → 0.2.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 (136) hide show
  1. package/README.md +34 -1
  2. package/dist/core/document.d.ts +19 -0
  3. package/dist/core/document.js +11 -0
  4. package/dist/core/formats.d.ts +9 -0
  5. package/dist/core/formats.js +14 -0
  6. package/dist/core/glob.d.ts +4 -0
  7. package/dist/core/glob.js +48 -0
  8. package/dist/core/index.d.ts +11 -0
  9. package/dist/core/index.js +11 -0
  10. package/dist/core/jsonpath.d.ts +59 -0
  11. package/dist/core/jsonpath.js +415 -0
  12. package/dist/core/lint.d.ts +57 -0
  13. package/dist/core/lint.js +82 -0
  14. package/dist/core/plugin.d.ts +59 -0
  15. package/dist/core/plugin.js +25 -0
  16. package/dist/core/pointers.d.ts +35 -0
  17. package/dist/core/pointers.js +169 -0
  18. package/dist/core/ruleset.d.ts +52 -0
  19. package/dist/core/ruleset.js +170 -0
  20. package/dist/core/runner.d.ts +21 -0
  21. package/dist/core/runner.js +222 -0
  22. package/dist/core/types.d.ts +170 -0
  23. package/dist/core/types.js +1 -0
  24. package/dist/core/validate-ruleset.d.ts +14 -0
  25. package/dist/core/validate-ruleset.js +105 -0
  26. package/dist/fix/apply.d.ts +21 -0
  27. package/dist/fix/apply.js +51 -0
  28. package/dist/fix/index.d.ts +3 -0
  29. package/dist/fix/index.js +2 -0
  30. package/dist/fix/plugin.d.ts +18 -0
  31. package/dist/fix/plugin.js +21 -0
  32. package/dist/fix/types.d.ts +39 -0
  33. package/dist/fix/types.js +0 -0
  34. package/dist/functions/alphabetical.d.ts +8 -0
  35. package/dist/functions/alphabetical.js +28 -0
  36. package/dist/functions/casing.d.ts +14 -0
  37. package/dist/functions/casing.js +30 -0
  38. package/dist/functions/defined.d.ts +3 -0
  39. package/dist/functions/defined.js +6 -0
  40. package/dist/functions/enumeration.d.ts +5 -0
  41. package/dist/functions/enumeration.js +10 -0
  42. package/dist/functions/falsy.d.ts +3 -0
  43. package/dist/functions/falsy.js +6 -0
  44. package/dist/functions/index.d.ts +16 -0
  45. package/dist/functions/index.js +42 -0
  46. package/dist/functions/length.d.ts +6 -0
  47. package/dist/functions/length.js +27 -0
  48. package/dist/functions/pattern.d.ts +6 -0
  49. package/dist/functions/pattern.js +20 -0
  50. package/dist/functions/schema.d.ts +8 -0
  51. package/dist/functions/schema.js +36 -0
  52. package/dist/functions/truthy.d.ts +3 -0
  53. package/dist/functions/truthy.js +6 -0
  54. package/dist/functions/typed-enum.d.ts +3 -0
  55. package/dist/functions/typed-enum.js +34 -0
  56. package/dist/functions/undefined.d.ts +6 -0
  57. package/dist/functions/undefined.js +9 -0
  58. package/dist/functions/unreferenced-reusable-object.d.ts +8 -0
  59. package/dist/functions/unreferenced-reusable-object.js +37 -0
  60. package/dist/functions/xor.d.ts +7 -0
  61. package/dist/functions/xor.js +11 -0
  62. package/dist/index.d.ts +89 -0
  63. package/dist/index.js +168 -0
  64. package/dist/parsers/edit-model.d.ts +69 -0
  65. package/dist/parsers/edit-model.js +326 -0
  66. package/dist/parsers/index.d.ts +18 -0
  67. package/dist/parsers/index.js +21 -0
  68. package/dist/parsers/json.d.ts +3 -0
  69. package/dist/parsers/json.js +38 -0
  70. package/dist/parsers/lines.d.ts +13 -0
  71. package/dist/parsers/lines.js +28 -0
  72. package/dist/parsers/types.d.ts +50 -0
  73. package/dist/parsers/types.js +8 -0
  74. package/dist/parsers/yaml.d.ts +6 -0
  75. package/dist/parsers/yaml.js +65 -0
  76. package/dist/rules/openapi/fixers.d.ts +7 -0
  77. package/dist/rules/openapi/fixers.js +189 -0
  78. package/dist/rules/openapi/formats.d.ts +13 -0
  79. package/dist/rules/openapi/formats.js +23 -0
  80. package/dist/rules/openapi/functions/example-validation.d.ts +5 -0
  81. package/dist/rules/openapi/functions/example-validation.js +97 -0
  82. package/dist/rules/openapi/functions/helpers.d.ts +3 -0
  83. package/dist/rules/openapi/functions/helpers.js +5 -0
  84. package/dist/rules/openapi/functions/index.d.ts +24 -0
  85. package/dist/rules/openapi/functions/index.js +67 -0
  86. package/dist/rules/openapi/functions/oas-additional-operations.d.ts +8 -0
  87. package/dist/rules/openapi/functions/oas-additional-operations.js +25 -0
  88. package/dist/rules/openapi/functions/oas-discriminator.d.ts +3 -0
  89. package/dist/rules/openapi/functions/oas-discriminator.js +23 -0
  90. package/dist/rules/openapi/functions/oas-example-value.d.ts +3 -0
  91. package/dist/rules/openapi/functions/oas-example-value.js +31 -0
  92. package/dist/rules/openapi/functions/oas-mutually-exclusive.d.ts +10 -0
  93. package/dist/rules/openapi/functions/oas-mutually-exclusive.js +20 -0
  94. package/dist/rules/openapi/functions/oas-op-form-data-consume-check.d.ts +3 -0
  95. package/dist/rules/openapi/functions/oas-op-form-data-consume-check.js +21 -0
  96. package/dist/rules/openapi/functions/oas-op-id-unique.d.ts +3 -0
  97. package/dist/rules/openapi/functions/oas-op-id-unique.js +27 -0
  98. package/dist/rules/openapi/functions/oas-op-params.d.ts +3 -0
  99. package/dist/rules/openapi/functions/oas-op-params.js +21 -0
  100. package/dist/rules/openapi/functions/oas-op-security-defined.d.ts +5 -0
  101. package/dist/rules/openapi/functions/oas-op-security-defined.js +44 -0
  102. package/dist/rules/openapi/functions/oas-op-success-response.d.ts +3 -0
  103. package/dist/rules/openapi/functions/oas-op-success-response.js +11 -0
  104. package/dist/rules/openapi/functions/oas-path-param.d.ts +3 -0
  105. package/dist/rules/openapi/functions/oas-path-param.js +45 -0
  106. package/dist/rules/openapi/functions/oas-schema-example-deprecated.d.ts +10 -0
  107. package/dist/rules/openapi/functions/oas-schema-example-deprecated.js +42 -0
  108. package/dist/rules/openapi/functions/oas-schema.d.ts +15 -0
  109. package/dist/rules/openapi/functions/oas-schema.js +15 -0
  110. package/dist/rules/openapi/functions/oas-server-name-unique.d.ts +3 -0
  111. package/dist/rules/openapi/functions/oas-server-name-unique.js +20 -0
  112. package/dist/rules/openapi/functions/oas-server-variables.d.ts +3 -0
  113. package/dist/rules/openapi/functions/oas-server-variables.js +23 -0
  114. package/dist/rules/openapi/functions/oas-tag-defined.d.ts +3 -0
  115. package/dist/rules/openapi/functions/oas-tag-defined.js +21 -0
  116. package/dist/rules/openapi/functions/oas-tag-kind.d.ts +3 -0
  117. package/dist/rules/openapi/functions/oas-tag-kind.js +17 -0
  118. package/dist/rules/openapi/functions/oas-tag-parent-defined.d.ts +7 -0
  119. package/dist/rules/openapi/functions/oas-tag-parent-defined.js +45 -0
  120. package/dist/rules/openapi/functions/oas-tags-unique.d.ts +3 -0
  121. package/dist/rules/openapi/functions/oas-tags-unique.js +17 -0
  122. package/dist/rules/openapi/functions/oas-unused-component.d.ts +7 -0
  123. package/dist/rules/openapi/functions/oas-unused-component.js +52 -0
  124. package/dist/rules/openapi/functions/ref-siblings.d.ts +3 -0
  125. package/dist/rules/openapi/functions/ref-siblings.js +13 -0
  126. package/dist/rules/openapi/index.d.ts +25 -0
  127. package/dist/rules/openapi/index.js +127 -0
  128. package/dist/rules/openapi/oas.d.ts +3 -0
  129. package/dist/rules/openapi/oas.js +492 -0
  130. package/dist/rules/openapi/schemas/index.d.ts +8 -0
  131. package/dist/rules/openapi/schemas/index.js +36 -0
  132. package/dist/rules/openapi/schemas/oas20.json +1592 -0
  133. package/dist/rules/openapi/schemas/oas30.json +1651 -0
  134. package/dist/rules/openapi/schemas/oas31.json +1412 -0
  135. package/dist/rules/openapi/schemas/oas32.json +1684 -0
  136. package/package.json +8 -5
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Builds a {@link LineMap} for `source`. The line-start offsets are precomputed
3
+ * once so each `positionAt` lookup is a binary search rather than a re-scan.
4
+ */
5
+ export const createLineMap = (source) => {
6
+ const length = source.length;
7
+ // Offset at which each line starts. lineStarts[0] === 0.
8
+ const lineStarts = [0];
9
+ for (let i = 0; i < source.length; i++) {
10
+ if (source.charCodeAt(i) === 10 /* \n */)
11
+ lineStarts.push(i + 1);
12
+ }
13
+ const positionAt = (offset) => {
14
+ const clamped = Math.max(0, Math.min(offset, length));
15
+ // Binary search for the last line start <= clamped.
16
+ let low = 0;
17
+ let high = lineStarts.length - 1;
18
+ while (low < high) {
19
+ const mid = (low + high + 1) >> 1;
20
+ if ((lineStarts[mid] ?? 0) <= clamped)
21
+ low = mid;
22
+ else
23
+ high = mid - 1;
24
+ }
25
+ return { line: low, character: clamped - (lineStarts[low] ?? 0) };
26
+ };
27
+ return { positionAt };
28
+ };
@@ -0,0 +1,50 @@
1
+ /** A path into a parsed document: object keys and array indices from the root. */
2
+ export type JsonPath = (string | number)[];
3
+ /** A zero-based line/character position, matching LSP and Linter conventions. */
4
+ export type IPosition = {
5
+ /** Zero-based line number. */
6
+ line: number;
7
+ /** Zero-based character offset within the line. */
8
+ character: number;
9
+ };
10
+ /** An inclusive-start, exclusive-end span between two positions. */
11
+ export type IRange = {
12
+ start: IPosition;
13
+ end: IPosition;
14
+ };
15
+ /** A resolved source location — currently just the range a node occupies. */
16
+ export type ILocation = {
17
+ range: IRange;
18
+ };
19
+ /** Severity levels, ordered most-to-least severe to match LSP's numeric scale. */
20
+ export declare enum DiagnosticSeverity {
21
+ Error = 0,
22
+ Warning = 1,
23
+ Information = 2,
24
+ Hint = 3
25
+ }
26
+ /** A problem reported by the parser itself (e.g. a syntax or duplicate-key error). */
27
+ export type IDiagnostic = {
28
+ code?: string | number;
29
+ message: string;
30
+ path?: JsonPath;
31
+ range: IRange;
32
+ severity: DiagnosticSeverity;
33
+ };
34
+ /** The result of parsing a document: its data, parser diagnostics, and a position lookup. */
35
+ export type IParseResult<T = unknown> = {
36
+ data: T;
37
+ diagnostics: IDiagnostic[];
38
+ /**
39
+ * Returns the source location for a JSON path. When `closest` is true and the
40
+ * exact path is not found, walks up to the nearest ancestor that is.
41
+ */
42
+ getLocationForJsonPath(path: JsonPath, closest?: boolean): ILocation | undefined;
43
+ };
44
+ /** Tuning for how strictly the parser treats YAML/JSON edge cases. */
45
+ export type IParserOptions = {
46
+ /** Severity for duplicate object keys. Default: error. `false`/`"off"` disables. */
47
+ duplicateKeys?: DiagnosticSeverity | 'off' | false;
48
+ /** Severity for YAML values incompatible with JSON (e.g. bigints). Default: error. */
49
+ incompatibleValues?: DiagnosticSeverity | 'off' | false;
50
+ };
@@ -0,0 +1,8 @@
1
+ /** Severity levels, ordered most-to-least severe to match LSP's numeric scale. */
2
+ export var DiagnosticSeverity;
3
+ (function (DiagnosticSeverity) {
4
+ DiagnosticSeverity[DiagnosticSeverity["Error"] = 0] = "Error";
5
+ DiagnosticSeverity[DiagnosticSeverity["Warning"] = 1] = "Warning";
6
+ DiagnosticSeverity[DiagnosticSeverity["Information"] = 2] = "Information";
7
+ DiagnosticSeverity[DiagnosticSeverity["Hint"] = 3] = "Hint";
8
+ })(DiagnosticSeverity || (DiagnosticSeverity = {}));
@@ -0,0 +1,6 @@
1
+ import { type IParseResult, type IParserOptions } from './types.js';
2
+ /**
3
+ * Parses YAML (a JSON superset, so this handles both) into data plus a source
4
+ * map, surfacing duplicate-key and incompatible-value diagnostics per `options`.
5
+ */
6
+ export declare const parseYaml: <T = unknown>(source: string, options?: IParserOptions) => IParseResult<T>;
@@ -0,0 +1,65 @@
1
+ import { isMap, isPair, isScalar, isSeq, parseDocument } from '@amritk/yaml';
2
+ import { createLineMap } from './lines.js';
3
+ import { DiagnosticSeverity, } from './types.js';
4
+ const pathKey = (path) => path.join('\0');
5
+ /**
6
+ * Parses YAML (a JSON superset, so this handles both) into data plus a source
7
+ * map, surfacing duplicate-key and incompatible-value diagnostics per `options`.
8
+ */
9
+ export const parseYaml = (source, options = {}) => {
10
+ const lineMap = createLineMap(source);
11
+ const dedupe = options.duplicateKeys === 'off' || options.duplicateKeys === false;
12
+ const doc = parseDocument(source, { uniqueKeys: !dedupe });
13
+ const index = new Map();
14
+ const rangeOf = (node) => ({
15
+ start: lineMap.positionAt(node.start),
16
+ end: lineMap.positionAt(node.end),
17
+ });
18
+ const walk = (node, path) => {
19
+ if (node == null)
20
+ return;
21
+ index.set(pathKey(path), rangeOf(node));
22
+ if (isMap(node)) {
23
+ for (const item of node.items) {
24
+ if (!isPair(item))
25
+ continue;
26
+ const key = item.key;
27
+ const keyName = isScalar(key) ? key.value : String(key);
28
+ walk(item.value, [...path, keyName]);
29
+ }
30
+ }
31
+ else if (isSeq(node)) {
32
+ node.items.forEach((item, i) => {
33
+ walk(item, [...path, i]);
34
+ });
35
+ }
36
+ };
37
+ walk(doc.contents, []);
38
+ const data = doc.toJS();
39
+ const diagnostics = [];
40
+ const pushError = (severity, message, start, end) => {
41
+ diagnostics.push({
42
+ message,
43
+ severity,
44
+ range: { start: lineMap.positionAt(start), end: lineMap.positionAt(end) },
45
+ });
46
+ };
47
+ for (const err of doc.errors) {
48
+ pushError(DiagnosticSeverity.Error, err.message, err.start, err.end);
49
+ }
50
+ for (const warn of doc.warnings) {
51
+ pushError(DiagnosticSeverity.Warning, warn.message, warn.start, warn.end);
52
+ }
53
+ const getLocationForJsonPath = (path, closest = false) => {
54
+ const p = path.slice();
55
+ while (true) {
56
+ const range = index.get(pathKey(p));
57
+ if (range)
58
+ return { range };
59
+ if (!closest || p.length === 0)
60
+ return undefined;
61
+ p.pop();
62
+ }
63
+ };
64
+ return { data, diagnostics, getLocationForJsonPath };
65
+ };
@@ -0,0 +1,7 @@
1
+ import type { FixerRegistry } from '../../fix/index.js';
2
+ /**
3
+ * Auto-fixers for the mechanically-repairable OpenAPI rules, keyed by rule
4
+ * code. Pass these to `@amritk/lint`'s `fixDocument` (as its `fixers`), or wrap
5
+ * them with `createFixPlugin` for a lower-level plugin.
6
+ */
7
+ export declare const oasFixers: FixerRegistry;
@@ -0,0 +1,189 @@
1
+ /** Reads the value at `path` in the parsed document, or `undefined` if absent. */
2
+ const getAtPath = (data, path) => {
3
+ let current = data;
4
+ for (const segment of path) {
5
+ if (current == null || typeof current !== 'object')
6
+ return undefined;
7
+ current = current[segment];
8
+ }
9
+ return current;
10
+ };
11
+ const isObject = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
12
+ const stripTrailingSlash = (value) => value.replace(/\/+$/, '');
13
+ /**
14
+ * `oas2-host-trailing-slash` / `oas3-server-trailing-slash`: drop the trailing
15
+ * slash from a string value (host or server URL).
16
+ */
17
+ const trailingSlashValue = {
18
+ safe: true,
19
+ fix: ({ diagnostic, data }) => {
20
+ const value = getAtPath(data, diagnostic.path);
21
+ if (typeof value !== 'string')
22
+ return undefined;
23
+ const stripped = stripTrailingSlash(value);
24
+ if (stripped === value || stripped === '')
25
+ return undefined;
26
+ return { op: 'setValue', path: diagnostic.path, value: stripped };
27
+ },
28
+ };
29
+ /** `path-keys-no-trailing-slash`: rename a `paths` key to drop its trailing slash. */
30
+ const pathKeyTrailingSlash = {
31
+ safe: true,
32
+ fix: ({ diagnostic }) => {
33
+ const key = diagnostic.path[diagnostic.path.length - 1];
34
+ if (typeof key !== 'string')
35
+ return undefined;
36
+ const stripped = stripTrailingSlash(key);
37
+ if (stripped === key || stripped === '')
38
+ return undefined;
39
+ return { op: 'renameProperty', path: diagnostic.path, newKey: stripped };
40
+ },
41
+ };
42
+ /** `no-$ref-siblings`: delete the sibling key that sits next to a `$ref`. */
43
+ const refSibling = {
44
+ safe: true,
45
+ fix: ({ diagnostic }) => {
46
+ if (diagnostic.path.length === 0)
47
+ return undefined;
48
+ return { op: 'removeProperty', path: diagnostic.path };
49
+ },
50
+ };
51
+ /** `duplicated-entry-in-enum`: remove the later copies of each repeated enum value. */
52
+ const duplicatedEnum = {
53
+ safe: true,
54
+ fix: ({ diagnostic, data }) => {
55
+ const array = getAtPath(data, diagnostic.path);
56
+ if (!Array.isArray(array))
57
+ return undefined;
58
+ const seen = new Set();
59
+ const duplicates = [];
60
+ array.forEach((item, index) => {
61
+ const key = JSON.stringify(item);
62
+ if (seen.has(key))
63
+ duplicates.push(index);
64
+ else
65
+ seen.add(key);
66
+ });
67
+ if (duplicates.length === 0)
68
+ return undefined;
69
+ return { op: 'removeItems', path: diagnostic.path, indices: duplicates };
70
+ },
71
+ };
72
+ /** `openapi-tags-alphabetical`: reorder the top-level `tags` array by `name`. */
73
+ const tagsAlphabetical = {
74
+ safe: true,
75
+ fix: ({ diagnostic, data }) => {
76
+ // Findings point at the out-of-order item; the array is its parent.
77
+ const arrayPath = diagnostic.path.slice(0, -1);
78
+ const array = getAtPath(data, arrayPath);
79
+ if (!Array.isArray(array))
80
+ return undefined;
81
+ const nameOf = (item) => item != null && typeof item === 'object' ? String(item['name']) : String(item);
82
+ const order = array.map((_, index) => index).sort((a, b) => nameOf(array[a]).localeCompare(nameOf(array[b])));
83
+ if (order.every((value, index) => value === index))
84
+ return undefined;
85
+ return { op: 'reorderArray', path: arrayPath, order };
86
+ },
87
+ };
88
+ /** `path-not-include-query`: drop the `?query` portion from a `paths` key. */
89
+ const pathKeyQueryString = {
90
+ safe: true,
91
+ fix: ({ diagnostic, data }) => {
92
+ const key = diagnostic.path[diagnostic.path.length - 1];
93
+ if (typeof key !== 'string')
94
+ return undefined;
95
+ const stripped = key.replace(/\?.*$/, '');
96
+ if (stripped === key || stripped === '')
97
+ return undefined;
98
+ // Renaming onto an existing path would collide and silently drop a path, so skip.
99
+ const paths = getAtPath(data, diagnostic.path.slice(0, -1));
100
+ if (isObject(paths) && stripped in paths)
101
+ return undefined;
102
+ return { op: 'renameProperty', path: diagnostic.path, newKey: stripped };
103
+ },
104
+ };
105
+ /** `openapi-tags-uniqueness`: remove the later copies of each repeated tag name. */
106
+ const tagsUnique = {
107
+ safe: true,
108
+ fix: ({ diagnostic, data }) => {
109
+ // Findings point at `tags[index].name`, so the array sits two segments up.
110
+ const arrayPath = diagnostic.path.slice(0, -2);
111
+ const array = getAtPath(data, arrayPath);
112
+ if (!Array.isArray(array))
113
+ return undefined;
114
+ const seen = new Set();
115
+ const duplicates = [];
116
+ array.forEach((tag, index) => {
117
+ const name = isObject(tag) ? tag['name'] : undefined;
118
+ if (typeof name !== 'string')
119
+ return;
120
+ if (seen.has(name))
121
+ duplicates.push(index);
122
+ else
123
+ seen.add(name);
124
+ });
125
+ if (duplicates.length === 0)
126
+ return undefined;
127
+ return { op: 'removeItems', path: arrayPath, indices: duplicates };
128
+ },
129
+ };
130
+ /**
131
+ * `oas3-unused-component` / `oas2-unused-definition`: delete the unreferenced
132
+ * component. Marked unsafe because a component can be referenced from another
133
+ * document or resolved dynamically, so removing it may not be semantics-preserving
134
+ * — it only applies under `--fix-unsafe`.
135
+ */
136
+ const unusedComponent = {
137
+ safe: false,
138
+ fix: ({ diagnostic }) => {
139
+ if (diagnostic.path.length === 0)
140
+ return undefined;
141
+ return { op: 'removeProperty', path: diagnostic.path };
142
+ },
143
+ };
144
+ /**
145
+ * `oas3_1-no-nullable`: migrate the 3.0-era `nullable` keyword to its OpenAPI
146
+ * 3.1 (JSON Schema 2020-12) equivalent. `nullable: false` is the schema default,
147
+ * so it is simply dropped; `nullable: true` folds a `"null"` member into the
148
+ * sibling `type` before the keyword is removed. Marked unsafe because it rewrites
149
+ * the schema's `type`, so it only applies under `--fix-unsafe`.
150
+ */
151
+ const noNullable = {
152
+ safe: false,
153
+ fix: ({ diagnostic, data }) => {
154
+ const value = getAtPath(data, diagnostic.path);
155
+ const remove = { op: 'removeProperty', path: diagnostic.path };
156
+ // `nullable: false` (or any non-true value) is the default — drop the keyword.
157
+ if (value !== true)
158
+ return remove;
159
+ const typePath = [...diagnostic.path.slice(0, -1), 'type'];
160
+ const type = getAtPath(data, typePath);
161
+ if (typeof type === 'string' && type !== 'null') {
162
+ return [{ op: 'setValue', path: typePath, value: [type, 'null'] }, remove];
163
+ }
164
+ if (Array.isArray(type) && !type.includes('null')) {
165
+ return [{ op: 'insertItem', path: typePath, value: 'null' }, remove];
166
+ }
167
+ // No type to widen (the schema already permits any value, null included) or
168
+ // `null` is already allowed — removing the keyword is enough.
169
+ return remove;
170
+ },
171
+ };
172
+ /**
173
+ * Auto-fixers for the mechanically-repairable OpenAPI rules, keyed by rule
174
+ * code. Pass these to `@amritk/lint`'s `fixDocument` (as its `fixers`), or wrap
175
+ * them with `createFixPlugin` for a lower-level plugin.
176
+ */
177
+ export const oasFixers = {
178
+ 'oas2-host-trailing-slash': trailingSlashValue,
179
+ 'oas3-server-trailing-slash': trailingSlashValue,
180
+ 'path-keys-no-trailing-slash': pathKeyTrailingSlash,
181
+ 'path-not-include-query': pathKeyQueryString,
182
+ 'no-$ref-siblings': refSibling,
183
+ 'duplicated-entry-in-enum': duplicatedEnum,
184
+ 'openapi-tags-alphabetical': tagsAlphabetical,
185
+ 'openapi-tags-uniqueness': tagsUnique,
186
+ 'oas3-unused-component': unusedComponent,
187
+ 'oas2-unused-definition': unusedComponent,
188
+ 'oas3_1-no-nullable': noNullable,
189
+ };
@@ -0,0 +1,13 @@
1
+ import type { Format } from '../../core/index.js';
2
+ /** Matches OpenAPI/Swagger 2.0 (`swagger: "2.0"`). */
3
+ export declare const oas2: Format;
4
+ /** Matches any OpenAPI 3.x (`openapi: 3.*`). */
5
+ export declare const oas3: Format;
6
+ /** Matches OpenAPI 3.0.x specifically. */
7
+ export declare const oas3_0: Format;
8
+ /** Matches OpenAPI 3.1.x specifically. */
9
+ export declare const oas3_1: Format;
10
+ /** Matches OpenAPI 3.2.x specifically. */
11
+ export declare const oas3_2: Format;
12
+ /** OpenAPI format detectors keyed by Loupe-compatible names. */
13
+ export declare const oasFormats: Record<string, Format>;
@@ -0,0 +1,23 @@
1
+ const isObject = (value) => typeof value === 'object' && value !== null;
2
+ const openapiVersion = (document) => isObject(document) && typeof document['openapi'] === 'string' ? document['openapi'] : undefined;
3
+ /** Matches OpenAPI/Swagger 2.0 (`swagger: "2.0"`). */
4
+ export const oas2 = (document) => isObject(document) && document['swagger'] === '2.0';
5
+ /** Matches any OpenAPI 3.x (`openapi: 3.*`). */
6
+ export const oas3 = (document) => openapiVersion(document)?.startsWith('3.') ?? false;
7
+ /** Matches OpenAPI 3.0.x specifically. */
8
+ export const oas3_0 = (document) => openapiVersion(document)?.startsWith('3.0') ?? false;
9
+ /** Matches OpenAPI 3.1.x specifically. */
10
+ export const oas3_1 = (document) => openapiVersion(document)?.startsWith('3.1') ?? false;
11
+ /** Matches OpenAPI 3.2.x specifically. */
12
+ export const oas3_2 = (document) => openapiVersion(document)?.startsWith('3.2') ?? false;
13
+ /** OpenAPI format detectors keyed by Loupe-compatible names. */
14
+ export const oasFormats = {
15
+ oas2,
16
+ oas3,
17
+ 'oas3.0': oas3_0,
18
+ oas3_0,
19
+ 'oas3.1': oas3_1,
20
+ oas3_1,
21
+ 'oas3.2': oas3_2,
22
+ oas3_2,
23
+ };
@@ -0,0 +1,5 @@
1
+ import type { RulesetFunction } from '../../../core/index.js';
2
+ /** Validates a schema object's inline `example` against the schema itself. */
3
+ export declare const oasSchemaExample: RulesetFunction;
4
+ /** Validates media type / parameter `example` and `examples` against the schema. */
5
+ export declare const oasMediaExample: RulesetFunction;
@@ -0,0 +1,97 @@
1
+ import { validate } from '@amritk/runtime-validators';
2
+ import { isObject } from './helpers.js';
3
+ // The example schemas come from the *linted document*, so they are only known at
4
+ // runtime. `@amritk/runtime-validators` interprets a schema directly — no
5
+ // `ajv.compile` (whose codegen dominated lint time on large specs, recompiling
6
+ // `$ref`-duplicated schemas tens of thousands of times) and no `new Function`,
7
+ // so it is also CSP/edge-runtime safe. Returns undefined for a non-object or a
8
+ // schema the validator can't build (mirrors the old skip-on-compile-failure
9
+ // behavior).
10
+ const buildValidator = (schema) => {
11
+ if (!isObject(schema))
12
+ return undefined;
13
+ try {
14
+ return validate(schema);
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ };
20
+ const buildValidatorOrNull = (schema) => buildValidator(schema) ?? null;
21
+ // Keyed by the Schema Object node; `oasSchemaExample` validates a schema's own
22
+ // `example` against the schema minus its `example`/`examples` keywords.
23
+ const schemaExampleResults = new WeakMap();
24
+ // Keyed by the Media Type Object node; `oasMediaExample` validates the media's
25
+ // `example`/`examples` against the media's `schema`. The node (not the schema) is
26
+ // the cache key because two media objects can share a `schema` but carry different
27
+ // examples.
28
+ const mediaExampleResults = new WeakMap();
29
+ const withPath = (findings, path) => {
30
+ if (findings.length === 0)
31
+ return [];
32
+ return findings.map((finding) => ({ message: finding.message, path: [...path, ...finding.suffix] }));
33
+ };
34
+ /** Validates a schema object's inline `example` against the schema itself. */
35
+ export const oasSchemaExample = (schema, _options, context) => {
36
+ if (!isObject(schema) || schema['example'] === undefined)
37
+ return [];
38
+ let findings = schemaExampleResults.get(schema);
39
+ if (findings === undefined) {
40
+ findings = [];
41
+ const { example, examples, ...rest } = schema;
42
+ void example;
43
+ void examples;
44
+ const check = buildValidatorOrNull(rest);
45
+ if (check) {
46
+ const result = check(schema['example']);
47
+ if (result !== true) {
48
+ for (const error of result.errors)
49
+ findings.push({ message: `"example" ${error.message}`.trim(), suffix: ['example'] });
50
+ }
51
+ }
52
+ schemaExampleResults.set(schema, findings);
53
+ }
54
+ return withPath(findings, context.path);
55
+ };
56
+ /** Validates media type / parameter `example` and `examples` against the schema. */
57
+ export const oasMediaExample = (media, _options, context) => {
58
+ if (!isObject(media) || !isObject(media['schema']))
59
+ return [];
60
+ // Skip building a validator when there is no example to check (most media
61
+ // objects in a large spec have a schema but no example).
62
+ const hasExample = media['example'] !== undefined || isObject(media['examples']);
63
+ if (!hasExample)
64
+ return [];
65
+ let findings = mediaExampleResults.get(media);
66
+ if (findings === undefined) {
67
+ findings = [];
68
+ const check = buildValidatorOrNull(media['schema']);
69
+ if (check) {
70
+ if (media['example'] !== undefined) {
71
+ const result = check(media['example']);
72
+ if (result !== true) {
73
+ for (const error of result.errors)
74
+ findings.push({ message: `"example" ${error.message}`.trim(), suffix: ['example'] });
75
+ }
76
+ }
77
+ const examples = isObject(media['examples']) ? media['examples'] : undefined;
78
+ if (examples) {
79
+ for (const [name, example] of Object.entries(examples)) {
80
+ if (isObject(example) && example['value'] !== undefined) {
81
+ const result = check(example['value']);
82
+ if (result !== true) {
83
+ for (const error of result.errors) {
84
+ findings.push({
85
+ message: `Example "${name}" ${error.message}`.trim(),
86
+ suffix: ['examples', name, 'value'],
87
+ });
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ mediaExampleResults.set(media, findings);
95
+ }
96
+ return withPath(findings, context.path);
97
+ };
@@ -0,0 +1,3 @@
1
+ /** True for a non-null, non-array object (an OpenAPI "object" value). */
2
+ export declare const isObject: (value: unknown) => value is Record<string, unknown>;
3
+ export declare const HTTP_METHODS: Set<string>;
@@ -0,0 +1,5 @@
1
+ /** True for a non-null, non-array object (an OpenAPI "object" value). */
2
+ export const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ // The eight standard HTTP methods that have a dedicated fixed field on the Path
4
+ // Item Object. Shared by the rules that iterate a path item's operations.
5
+ export const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
@@ -0,0 +1,24 @@
1
+ import type { FunctionRegistry } from '../../../core/index.js';
2
+ export { oasMediaExample, oasSchemaExample } from './example-validation.js';
3
+ export { oasAdditionalOperations } from './oas-additional-operations.js';
4
+ export { oasDiscriminator } from './oas-discriminator.js';
5
+ export { oasExampleValue } from './oas-example-value.js';
6
+ export { oasMutuallyExclusive } from './oas-mutually-exclusive.js';
7
+ export { oasOpFormDataConsumeCheck } from './oas-op-form-data-consume-check.js';
8
+ export { oasOpIdUnique } from './oas-op-id-unique.js';
9
+ export { oasOpParams } from './oas-op-params.js';
10
+ export { oasOpSecurityDefined } from './oas-op-security-defined.js';
11
+ export { oasOpSuccessResponse } from './oas-op-success-response.js';
12
+ export { oasPathParam } from './oas-path-param.js';
13
+ export { type IOasSchemaOptions, oasSchema } from './oas-schema.js';
14
+ export { oasSchemaExampleDeprecated } from './oas-schema-example-deprecated.js';
15
+ export { oasServerNameUnique } from './oas-server-name-unique.js';
16
+ export { oasServerVariables } from './oas-server-variables.js';
17
+ export { oasTagDefined } from './oas-tag-defined.js';
18
+ export { oasTagKind } from './oas-tag-kind.js';
19
+ export { oasTagParentDefined } from './oas-tag-parent-defined.js';
20
+ export { oasTagsUnique } from './oas-tags-unique.js';
21
+ export { oasUnusedComponent } from './oas-unused-component.js';
22
+ export { refSiblings } from './ref-siblings.js';
23
+ /** The OpenAPI-specific custom functions, keyed by name for ruleset `then` references. */
24
+ export declare const oasFunctions: FunctionRegistry;
@@ -0,0 +1,67 @@
1
+ import { oasMediaExample, oasSchemaExample } from './example-validation.js';
2
+ import { oasAdditionalOperations } from './oas-additional-operations.js';
3
+ import { oasDiscriminator } from './oas-discriminator.js';
4
+ import { oasExampleValue } from './oas-example-value.js';
5
+ import { oasMutuallyExclusive } from './oas-mutually-exclusive.js';
6
+ import { oasOpFormDataConsumeCheck } from './oas-op-form-data-consume-check.js';
7
+ import { oasOpIdUnique } from './oas-op-id-unique.js';
8
+ import { oasOpParams } from './oas-op-params.js';
9
+ import { oasOpSecurityDefined } from './oas-op-security-defined.js';
10
+ import { oasOpSuccessResponse } from './oas-op-success-response.js';
11
+ import { oasPathParam } from './oas-path-param.js';
12
+ import { oasSchema } from './oas-schema.js';
13
+ import { oasSchemaExampleDeprecated } from './oas-schema-example-deprecated.js';
14
+ import { oasServerNameUnique } from './oas-server-name-unique.js';
15
+ import { oasServerVariables } from './oas-server-variables.js';
16
+ import { oasTagDefined } from './oas-tag-defined.js';
17
+ import { oasTagKind } from './oas-tag-kind.js';
18
+ import { oasTagParentDefined } from './oas-tag-parent-defined.js';
19
+ import { oasTagsUnique } from './oas-tags-unique.js';
20
+ import { oasUnusedComponent } from './oas-unused-component.js';
21
+ import { refSiblings } from './ref-siblings.js';
22
+ export { oasMediaExample, oasSchemaExample } from './example-validation.js';
23
+ export { oasAdditionalOperations } from './oas-additional-operations.js';
24
+ export { oasDiscriminator } from './oas-discriminator.js';
25
+ export { oasExampleValue } from './oas-example-value.js';
26
+ export { oasMutuallyExclusive } from './oas-mutually-exclusive.js';
27
+ export { oasOpFormDataConsumeCheck } from './oas-op-form-data-consume-check.js';
28
+ export { oasOpIdUnique } from './oas-op-id-unique.js';
29
+ export { oasOpParams } from './oas-op-params.js';
30
+ export { oasOpSecurityDefined } from './oas-op-security-defined.js';
31
+ export { oasOpSuccessResponse } from './oas-op-success-response.js';
32
+ export { oasPathParam } from './oas-path-param.js';
33
+ export { oasSchema } from './oas-schema.js';
34
+ export { oasSchemaExampleDeprecated } from './oas-schema-example-deprecated.js';
35
+ export { oasServerNameUnique } from './oas-server-name-unique.js';
36
+ export { oasServerVariables } from './oas-server-variables.js';
37
+ export { oasTagDefined } from './oas-tag-defined.js';
38
+ export { oasTagKind } from './oas-tag-kind.js';
39
+ export { oasTagParentDefined } from './oas-tag-parent-defined.js';
40
+ export { oasTagsUnique } from './oas-tags-unique.js';
41
+ export { oasUnusedComponent } from './oas-unused-component.js';
42
+ export { refSiblings } from './ref-siblings.js';
43
+ /** The OpenAPI-specific custom functions, keyed by name for ruleset `then` references. */
44
+ export const oasFunctions = {
45
+ refSiblings,
46
+ oasOpSuccessResponse,
47
+ oasTagDefined,
48
+ oasOpIdUnique,
49
+ oasPathParam,
50
+ oasOpParams,
51
+ oasTagsUnique,
52
+ oasOpSecurityDefined: oasOpSecurityDefined,
53
+ oasOpFormDataConsumeCheck,
54
+ oasDiscriminator,
55
+ oasServerVariables,
56
+ oasSchemaExample,
57
+ oasMediaExample,
58
+ oasUnusedComponent,
59
+ oasMutuallyExclusive: oasMutuallyExclusive,
60
+ oasSchema: oasSchema,
61
+ oasAdditionalOperations,
62
+ oasServerNameUnique,
63
+ oasTagParentDefined,
64
+ oasSchemaExampleDeprecated,
65
+ oasTagKind,
66
+ oasExampleValue,
67
+ };
@@ -0,0 +1,8 @@
1
+ import type { RulesetFunction } from '../../../core/index.js';
2
+ /**
3
+ * Flags standard HTTP methods inside an OpenAPI 3.2 `additionalOperations` map.
4
+ * The spec reserves that map for methods without a dedicated fixed field, so a
5
+ * standard method key (sent uppercase, e.g. `POST`) belongs in the lowercase
6
+ * fixed field (`post`) instead.
7
+ */
8
+ export declare const oasAdditionalOperations: RulesetFunction;
@@ -0,0 +1,25 @@
1
+ import { HTTP_METHODS, isObject } from './helpers.js';
2
+ // HTTP methods that have a dedicated fixed field on the Path Item Object. In
3
+ // OpenAPI 3.2 `query` joined the original eight, and these MUST NOT be redefined
4
+ // inside the new `additionalOperations` map (which is for non-standard methods).
5
+ const FIXED_PATH_ITEM_METHODS = new Set([...HTTP_METHODS, 'query']);
6
+ /**
7
+ * Flags standard HTTP methods inside an OpenAPI 3.2 `additionalOperations` map.
8
+ * The spec reserves that map for methods without a dedicated fixed field, so a
9
+ * standard method key (sent uppercase, e.g. `POST`) belongs in the lowercase
10
+ * fixed field (`post`) instead.
11
+ */
12
+ export const oasAdditionalOperations = (input, _options, context) => {
13
+ if (!isObject(input))
14
+ return [];
15
+ const results = [];
16
+ for (const method of Object.keys(input)) {
17
+ if (FIXED_PATH_ITEM_METHODS.has(method.toLowerCase())) {
18
+ results.push({
19
+ message: `"additionalOperations" must not redefine the standard method "${method}"; use the "${method.toLowerCase()}" field instead`,
20
+ path: [...context.path, method],
21
+ });
22
+ }
23
+ }
24
+ return results;
25
+ };
@@ -0,0 +1,3 @@
1
+ import type { RulesetFunction } from '../../../core/index.js';
2
+ /** Validates a v2 discriminator references a required property. */
3
+ export declare const oasDiscriminator: RulesetFunction;