@amritk/lint 0.1.0 → 0.3.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 (108) hide show
  1. package/README.md +52 -1
  2. package/dist/core/glob.d.ts +1 -1
  3. package/dist/core/glob.js +89 -5
  4. package/dist/core/index.d.ts +1 -1
  5. package/dist/core/index.js +1 -1
  6. package/dist/core/jsonpath.d.ts +17 -1
  7. package/dist/core/jsonpath.js +218 -23
  8. package/dist/core/lint.d.ts +15 -8
  9. package/dist/core/lint.js +12 -3
  10. package/dist/core/plugin.d.ts +6 -0
  11. package/dist/core/plugin.js +6 -0
  12. package/dist/core/pointers.js +15 -15
  13. package/dist/core/ruleset.js +0 -0
  14. package/dist/core/runner.d.ts +6 -1
  15. package/dist/core/runner.js +127 -43
  16. package/dist/core/types.d.ts +17 -2
  17. package/dist/core/validate-ruleset.js +16 -0
  18. package/dist/fix/apply.d.ts +8 -2
  19. package/dist/fix/apply.js +68 -18
  20. package/dist/functions/alphabetical.js +40 -13
  21. package/dist/functions/casing.js +27 -5
  22. package/dist/functions/enumeration.d.ts +5 -3
  23. package/dist/functions/enumeration.js +18 -1
  24. package/dist/functions/index.d.ts +1 -0
  25. package/dist/functions/index.js +3 -0
  26. package/dist/functions/length.d.ts +13 -3
  27. package/dist/functions/length.js +11 -3
  28. package/dist/functions/or.d.ts +11 -0
  29. package/dist/functions/or.js +25 -0
  30. package/dist/functions/pattern.d.ts +5 -3
  31. package/dist/functions/pattern.js +42 -9
  32. package/dist/functions/schema.d.ts +13 -0
  33. package/dist/functions/schema.js +95 -2
  34. package/dist/functions/typed-enum.js +7 -1
  35. package/dist/functions/unreferenced-reusable-object.d.ts +7 -1
  36. package/dist/functions/unreferenced-reusable-object.js +18 -3
  37. package/dist/functions/xor.js +8 -1
  38. package/dist/index.js +9 -1
  39. package/dist/parsers/edit-model.d.ts +15 -0
  40. package/dist/parsers/edit-model.js +210 -41
  41. package/dist/parsers/types.d.ts +14 -2
  42. package/dist/parsers/yaml.d.ts +10 -0
  43. package/dist/parsers/yaml.js +174 -26
  44. package/dist/rules/openapi/fixers.d.ts +7 -0
  45. package/dist/rules/openapi/fixers.js +248 -0
  46. package/dist/rules/openapi/formats.d.ts +13 -0
  47. package/dist/rules/openapi/formats.js +30 -0
  48. package/dist/rules/openapi/functions/example-validation.d.ts +18 -0
  49. package/dist/rules/openapi/functions/example-validation.js +160 -0
  50. package/dist/rules/openapi/functions/helpers.d.ts +4 -0
  51. package/dist/rules/openapi/functions/helpers.js +10 -0
  52. package/dist/rules/openapi/functions/index.d.ts +26 -0
  53. package/dist/rules/openapi/functions/index.js +73 -0
  54. package/dist/rules/openapi/functions/oas-additional-operations.d.ts +8 -0
  55. package/dist/rules/openapi/functions/oas-additional-operations.js +25 -0
  56. package/dist/rules/openapi/functions/oas-discriminator.d.ts +3 -0
  57. package/dist/rules/openapi/functions/oas-discriminator.js +23 -0
  58. package/dist/rules/openapi/functions/oas-example-external-value.d.ts +11 -0
  59. package/dist/rules/openapi/functions/oas-example-external-value.js +23 -0
  60. package/dist/rules/openapi/functions/oas-example-value.d.ts +3 -0
  61. package/dist/rules/openapi/functions/oas-example-value.js +31 -0
  62. package/dist/rules/openapi/functions/oas-mutually-exclusive.d.ts +10 -0
  63. package/dist/rules/openapi/functions/oas-mutually-exclusive.js +20 -0
  64. package/dist/rules/openapi/functions/oas-no-nullable.d.ts +13 -0
  65. package/dist/rules/openapi/functions/oas-no-nullable.js +22 -0
  66. package/dist/rules/openapi/functions/oas-op-form-data-consume-check.d.ts +3 -0
  67. package/dist/rules/openapi/functions/oas-op-form-data-consume-check.js +21 -0
  68. package/dist/rules/openapi/functions/oas-op-id-unique.d.ts +3 -0
  69. package/dist/rules/openapi/functions/oas-op-id-unique.js +29 -0
  70. package/dist/rules/openapi/functions/oas-op-params.d.ts +9 -0
  71. package/dist/rules/openapi/functions/oas-op-params.js +46 -0
  72. package/dist/rules/openapi/functions/oas-op-security-defined.d.ts +5 -0
  73. package/dist/rules/openapi/functions/oas-op-security-defined.js +44 -0
  74. package/dist/rules/openapi/functions/oas-op-success-response.d.ts +3 -0
  75. package/dist/rules/openapi/functions/oas-op-success-response.js +16 -0
  76. package/dist/rules/openapi/functions/oas-path-param.d.ts +12 -0
  77. package/dist/rules/openapi/functions/oas-path-param.js +106 -0
  78. package/dist/rules/openapi/functions/oas-schema-example-deprecated.d.ts +10 -0
  79. package/dist/rules/openapi/functions/oas-schema-example-deprecated.js +42 -0
  80. package/dist/rules/openapi/functions/oas-schema.d.ts +15 -0
  81. package/dist/rules/openapi/functions/oas-schema.js +15 -0
  82. package/dist/rules/openapi/functions/oas-server-name-unique.d.ts +3 -0
  83. package/dist/rules/openapi/functions/oas-server-name-unique.js +20 -0
  84. package/dist/rules/openapi/functions/oas-server-variables.d.ts +8 -0
  85. package/dist/rules/openapi/functions/oas-server-variables.js +52 -0
  86. package/dist/rules/openapi/functions/oas-tag-defined.d.ts +3 -0
  87. package/dist/rules/openapi/functions/oas-tag-defined.js +21 -0
  88. package/dist/rules/openapi/functions/oas-tag-kind.d.ts +3 -0
  89. package/dist/rules/openapi/functions/oas-tag-kind.js +17 -0
  90. package/dist/rules/openapi/functions/oas-tag-parent-defined.d.ts +7 -0
  91. package/dist/rules/openapi/functions/oas-tag-parent-defined.js +45 -0
  92. package/dist/rules/openapi/functions/oas-tags-unique.d.ts +3 -0
  93. package/dist/rules/openapi/functions/oas-tags-unique.js +17 -0
  94. package/dist/rules/openapi/functions/oas-unused-component.d.ts +7 -0
  95. package/dist/rules/openapi/functions/oas-unused-component.js +65 -0
  96. package/dist/rules/openapi/functions/ref-siblings.d.ts +3 -0
  97. package/dist/rules/openapi/functions/ref-siblings.js +13 -0
  98. package/dist/rules/openapi/index.d.ts +25 -0
  99. package/dist/rules/openapi/index.js +127 -0
  100. package/dist/rules/openapi/oas.d.ts +3 -0
  101. package/dist/rules/openapi/oas.js +549 -0
  102. package/dist/rules/openapi/schemas/index.d.ts +8 -0
  103. package/dist/rules/openapi/schemas/index.js +36 -0
  104. package/dist/rules/openapi/schemas/oas20.json +1592 -0
  105. package/dist/rules/openapi/schemas/oas30.json +1651 -0
  106. package/dist/rules/openapi/schemas/oas31.json +1412 -0
  107. package/dist/rules/openapi/schemas/oas32.json +1684 -0
  108. package/package.json +17 -5
@@ -0,0 +1,127 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, isAbsolute, resolve as resolvePath } from 'node:path';
4
+ import { createRuleset as createCoreRuleset, } from '../../core/index.js';
5
+ import { builtinFunctions } from '../../functions/index.js';
6
+ import { parseWithPointers } from '../../parsers/index.js';
7
+ import { oasFormats } from './formats.js';
8
+ import { oasFunctions } from './functions/index.js';
9
+ import { oas } from './oas.js';
10
+ export { oasFixers } from './fixers.js';
11
+ export { oas2, oas3, oas3_0, oas3_1, oas3_2, oasFormats } from './formats.js';
12
+ export { oasFunctions } from './functions/index.js';
13
+ export { oas } from './oas.js';
14
+ export { loadOasSchema } from './schemas/index.js';
15
+ /** The built-in `@amritk/lint` functions plus the OpenAPI-specific ones, keyed by name. */
16
+ export const allFunctions = { ...builtinFunctions, ...oasFunctions };
17
+ const require = createRequire(import.meta.url);
18
+ /** The names that resolve to the built-in OpenAPI ruleset (incl. the legacy Spectral alias). */
19
+ const OAS_RULESET_NAMES = new Set(['oas', 'loupe:oas', 'spectral:oas']);
20
+ /** Loads a ruleset definition from a file path by extension (YAML/JSON parsed, JS/CJS/MJS required). */
21
+ const loadRulesetFile = (file) => {
22
+ if (/\.(ya?ml|json)$/i.test(file)) {
23
+ return parseWithPointers(readFileSync(file, 'utf8')).data;
24
+ }
25
+ const module = require(file);
26
+ return (module.default ?? module);
27
+ };
28
+ /**
29
+ * Resolves an `extends` reference to a ruleset definition. Extends the generic
30
+ * file/package resolution with the OpenAPI preset names:
31
+ * - `oas` / `loupe:oas` / `spectral:oas` → the built-in {@link oas} ruleset,
32
+ * - local file paths (relative to `basePath`, or absolute): `.yaml` / `.yml` / `.json` / `.js`,
33
+ * - npm package specifiers (resolved from `basePath`), including subpaths.
34
+ */
35
+ export const resolveOpenApiRuleset = (name, basePath = process.cwd()) => {
36
+ if (OAS_RULESET_NAMES.has(name))
37
+ return { definition: oas, basePath };
38
+ if (name.startsWith('.') || isAbsolute(name)) {
39
+ const file = resolvePath(basePath, name);
40
+ return { definition: loadRulesetFile(file), basePath: dirname(file) };
41
+ }
42
+ let file;
43
+ try {
44
+ file = require.resolve(name, { paths: [basePath] });
45
+ }
46
+ catch {
47
+ throw new Error(`Cannot resolve extended ruleset "${name}" from ${basePath}`);
48
+ }
49
+ return { definition: loadRulesetFile(file), basePath: dirname(file) };
50
+ };
51
+ /** Loads a single custom function module (`<dir>/<name>.{js,cjs,mjs}` or a bare path). */
52
+ const loadFunctionByName = (basePath, dir, name) => {
53
+ const baseFile = resolvePath(basePath, dir, name);
54
+ for (const candidate of [baseFile, `${baseFile}.js`, `${baseFile}.cjs`, `${baseFile}.mjs`]) {
55
+ try {
56
+ const resolvedFile = require.resolve(candidate);
57
+ const module = require(resolvedFile);
58
+ const fn = module.default ?? module;
59
+ if (typeof fn !== 'function')
60
+ throw new Error(`"${name}" did not export a function`);
61
+ return fn;
62
+ }
63
+ catch (error) {
64
+ if (error.code !== 'MODULE_NOT_FOUND')
65
+ throw error;
66
+ }
67
+ }
68
+ throw new Error(`Cannot resolve custom function "${name}" from ${resolvePath(basePath, dir)}`);
69
+ };
70
+ /**
71
+ * Walks a ruleset definition (and its string `extends`) collecting custom
72
+ * functions declared via `functions` / `functionsDir`, each loaded relative to
73
+ * the directory of the ruleset that declared it. The built-in `oas` names carry
74
+ * no custom functions, so they are skipped.
75
+ */
76
+ const collectCustomFunctions = (definition, basePath, into, seen) => {
77
+ if (seen.has(definition))
78
+ return;
79
+ seen.add(definition);
80
+ if (definition.extends) {
81
+ const entries = Array.isArray(definition.extends) ? definition.extends : [definition.extends];
82
+ for (const entry of entries) {
83
+ const target = Array.isArray(entry) ? entry[0] : entry;
84
+ if (typeof target === 'string') {
85
+ if (OAS_RULESET_NAMES.has(target))
86
+ continue;
87
+ const resolved = resolveOpenApiRuleset(target, basePath);
88
+ collectCustomFunctions(resolved.definition, resolved.basePath, into, seen);
89
+ }
90
+ else {
91
+ collectCustomFunctions(target, basePath, into, seen);
92
+ }
93
+ }
94
+ }
95
+ if (Array.isArray(definition.functions)) {
96
+ const dir = definition.functionsDir ?? 'functions';
97
+ for (const name of definition.functions)
98
+ into[name] = loadFunctionByName(basePath, dir, name);
99
+ }
100
+ };
101
+ /**
102
+ * Builds a runnable {@link Ruleset} for OpenAPI, layering the built-in and
103
+ * OpenAPI functions (plus any custom ones the definition declares), the OpenAPI
104
+ * `formats`, and `extends` resolution that understands the `oas` / `loupe:oas` /
105
+ * `spectral:oas` names. With no definition it defaults to `extends: [oas]`
106
+ * (recommended rules only). Feed the result to `@amritk/lint`'s core
107
+ * `lintWithResult` (with a `$ref` resolver for `resolved: true` rules).
108
+ */
109
+ export const createOpenApiRuleset = (definition, basePath) => {
110
+ // With no explicit ruleset, behave like `extends: [oas]` so only `recommended`
111
+ // rules run by default. A user-supplied ruleset is used as-is (its own rules
112
+ // run regardless of `recommended`).
113
+ const resolved = definition ?? { extends: [oas] };
114
+ // Custom functions referenced by name (YAML/JSON rulesets) are loaded relative
115
+ // to the declaring ruleset's directory and layered over the built-ins.
116
+ let functions = allFunctions;
117
+ const custom = {};
118
+ collectCustomFunctions(resolved, basePath ?? process.cwd(), custom, new Set());
119
+ if (Object.keys(custom).length > 0)
120
+ functions = { ...allFunctions, ...custom };
121
+ return createCoreRuleset(resolved, {
122
+ functions,
123
+ formats: oasFormats,
124
+ resolve: resolveOpenApiRuleset,
125
+ ...(basePath !== undefined ? { basePath } : {}),
126
+ });
127
+ };
@@ -0,0 +1,3 @@
1
+ import type { RulesetDefinition } from '../../core/index.js';
2
+ /** Loupe's built-in OpenAPI ruleset (`loupe:oas`). */
3
+ export declare const oas: RulesetDefinition;
@@ -0,0 +1,549 @@
1
+ // `query` is a fixed operation field from OpenAPI 3.2 onward. Including it in the
2
+ // operation selectors is harmless on older versions (which never carry a `query`
3
+ // operation) and lets the operation-scoped rules cover 3.2 `query` operations.
4
+ const OPERATIONS = '$.paths[*][get,put,post,delete,options,head,patch,trace,query]';
5
+ const WEBHOOK_OPERATIONS = '$.webhooks[*][get,put,post,delete,options,head,patch,trace,query]';
6
+ const OPERATION_ID_URL_SAFE = "^[A-Za-z0-9-._~:/?#\\[\\]@!\\$&'()*+,;=]*$";
7
+ // Matches every Schema Object that carries an inline `example` or `default` and
8
+ // looks like a schema (it has a JSON Schema keyword), while skipping the
9
+ // `properties`/`patternProperties` *maps* — whose keys can coincidentally be
10
+ // named like schema keywords. A single filter given (rather than a `$..example^`
11
+ // / `$..default^` union) keeps each schema node matched exactly once, so the
12
+ // `oasSchemaExample` finding for a schema is never duplicated.
13
+ const SCHEMA_EXAMPLE_GIVEN = "$..[?(@property !== 'properties' && @property !== 'patternProperties' && @ && (@.example !== void 0 || @.default !== void 0) && (@.type || @.enum || @.format || @.$ref || @.properties || @.items || @.allOf || @.anyOf || @.oneOf))]";
14
+ /** Rules shared across OpenAPI v2 and v3. */
15
+ const sharedRules = {
16
+ 'contact-properties': {
17
+ description: 'Contact object must have name, url, and email.',
18
+ given: '$.info.contact',
19
+ severity: 'warn',
20
+ recommended: false,
21
+ then: [
22
+ { field: 'name', function: 'truthy' },
23
+ { field: 'url', function: 'truthy' },
24
+ { field: 'email', function: 'truthy' },
25
+ ],
26
+ },
27
+ 'duplicated-entry-in-enum': {
28
+ description: 'Enum values must not have duplicate entries.',
29
+ given: '$..enum',
30
+ severity: 'warn',
31
+ then: { function: 'schema', functionOptions: { schema: { type: 'array', uniqueItems: true } } },
32
+ },
33
+ 'info-contact': {
34
+ description: 'Info object must have a contact object.',
35
+ given: '$.info',
36
+ then: { field: 'contact', function: 'truthy' },
37
+ },
38
+ 'info-description': {
39
+ description: 'Info object must have a description.',
40
+ given: '$.info',
41
+ then: { field: 'description', function: 'truthy' },
42
+ },
43
+ 'info-license': {
44
+ description: 'Info object should have a license.',
45
+ given: '$.info',
46
+ severity: 'warn',
47
+ recommended: false,
48
+ then: { field: 'license', function: 'truthy' },
49
+ },
50
+ 'license-url': {
51
+ description: 'License object should have a url.',
52
+ given: '$.info.license',
53
+ severity: 'warn',
54
+ recommended: false,
55
+ then: { field: 'url', function: 'truthy' },
56
+ },
57
+ 'no-$ref-siblings': {
58
+ description: 'Sibling values alongside $ref are ignored.',
59
+ // OpenAPI 3.1 (JSON Schema 2020-12) permits siblings next to `$ref`, so this
60
+ // rule is gated to 2.0 / 3.0 — matching `spectral:oas`.
61
+ formats: ['oas2', 'oas3_0'],
62
+ given: '$..$ref^',
63
+ severity: 'error',
64
+ resolved: false,
65
+ then: { function: 'refSiblings' },
66
+ },
67
+ 'no-eval-in-markdown': {
68
+ description: 'Markdown descriptions must not contain "eval(".',
69
+ // Spectral scans both `description` and `title` markdown fields.
70
+ given: ['$..description', '$..title'],
71
+ then: { function: 'pattern', functionOptions: { notMatch: 'eval\\(' } },
72
+ },
73
+ 'no-script-tags-in-markdown': {
74
+ description: 'Markdown descriptions must not contain <script> tags.',
75
+ given: ['$..description', '$..title'],
76
+ then: { function: 'pattern', functionOptions: { notMatch: '<script' } },
77
+ },
78
+ 'openapi-tags': {
79
+ description: 'Top-level tags should be present and non-empty.',
80
+ given: '$.tags',
81
+ severity: 'warn',
82
+ recommended: false,
83
+ then: { function: 'length', functionOptions: { min: 1 } },
84
+ },
85
+ 'openapi-tags-alphabetical': {
86
+ description: 'Top-level tags should be in alphabetical order.',
87
+ given: '$.tags',
88
+ severity: 'warn',
89
+ recommended: false,
90
+ then: { function: 'alphabetical', functionOptions: { keyedBy: 'name' } },
91
+ },
92
+ 'openapi-tags-uniqueness': {
93
+ description: 'Top-level tag names must be unique.',
94
+ given: '$.tags',
95
+ severity: 'error',
96
+ then: { function: 'oasTagsUnique' },
97
+ },
98
+ 'operation-description': {
99
+ description: 'Operation must have a description.',
100
+ given: OPERATIONS,
101
+ then: { field: 'description', function: 'truthy' },
102
+ },
103
+ 'operation-operationId': {
104
+ description: 'Operation must have an operationId.',
105
+ given: OPERATIONS,
106
+ then: { field: 'operationId', function: 'truthy' },
107
+ },
108
+ 'operation-operationId-unique': {
109
+ description: 'Every operationId must be unique.',
110
+ given: '$.paths',
111
+ severity: 'error',
112
+ then: { function: 'oasOpIdUnique' },
113
+ },
114
+ 'operation-operationId-valid-in-url': {
115
+ description: 'operationId must use URL-safe characters.',
116
+ given: OPERATIONS,
117
+ then: { field: 'operationId', function: 'pattern', functionOptions: { match: OPERATION_ID_URL_SAFE } },
118
+ },
119
+ 'operation-parameters': {
120
+ description: 'Operation parameters must be unique and non-repeating.',
121
+ given: '$.paths[*][*].parameters',
122
+ then: { function: 'oasOpParams' },
123
+ },
124
+ 'operation-singular-tag': {
125
+ description: 'Operation should have a single tag.',
126
+ given: OPERATIONS,
127
+ severity: 'warn',
128
+ recommended: false,
129
+ then: { field: 'tags', function: 'length', functionOptions: { max: 1 } },
130
+ },
131
+ 'operation-success-response': {
132
+ description: 'Operation must have at least one 2xx or 3xx response.',
133
+ given: OPERATIONS,
134
+ then: { field: 'responses', function: 'oasOpSuccessResponse' },
135
+ },
136
+ 'operation-tags': {
137
+ description: 'Operation must have non-empty tags.',
138
+ given: OPERATIONS,
139
+ then: { field: 'tags', function: 'truthy' },
140
+ },
141
+ 'operation-tag-defined': {
142
+ description: 'Operation tags must be defined in the global tags list.',
143
+ given: OPERATIONS,
144
+ then: { function: 'oasTagDefined' },
145
+ },
146
+ 'path-declarations-must-exist': {
147
+ description: 'Path parameter declarations must not be empty ({}).',
148
+ given: '$.paths',
149
+ then: { field: '@key', function: 'pattern', functionOptions: { notMatch: '{}' } },
150
+ },
151
+ 'path-keys-no-trailing-slash': {
152
+ description: 'Path keys should not end with a slash.',
153
+ given: '$.paths',
154
+ then: { field: '@key', function: 'pattern', functionOptions: { notMatch: '.+\\/$' } },
155
+ },
156
+ 'path-not-include-query': {
157
+ description: 'Path keys should not include query strings.',
158
+ given: '$.paths',
159
+ then: { field: '@key', function: 'pattern', functionOptions: { notMatch: '\\?' } },
160
+ },
161
+ 'path-params': {
162
+ description: 'Path parameters must be defined and not duplicated.',
163
+ given: '$.paths',
164
+ severity: 'error',
165
+ then: { function: 'oasPathParam' },
166
+ },
167
+ 'tag-description': {
168
+ description: 'Tags should have a description.',
169
+ given: '$.tags[*]',
170
+ severity: 'warn',
171
+ recommended: false,
172
+ then: { field: 'description', function: 'truthy' },
173
+ },
174
+ 'typed-enum': {
175
+ description: 'Enum values must respect the specified type.',
176
+ given: '$..enum^',
177
+ then: { function: 'typedEnum' },
178
+ },
179
+ 'array-items': {
180
+ // Not part of `spectral:oas` — a Loupe extension. `type: array` without
181
+ // `items` is valid in JSON Schema 2020-12 (OpenAPI 3.1/3.2), so this is gated
182
+ // to the versions where `items` is genuinely required (2.0 and 3.0.x) to
183
+ // avoid false positives on valid 3.1+ tuple/unconstrained-array schemas.
184
+ description: 'Schemas of type array must define items (OpenAPI 2.0 / 3.0 only; Loupe extension).',
185
+ formats: ['oas2', 'oas3_0'],
186
+ given: "$..[?(@ && @.type === 'array')]",
187
+ severity: 'error',
188
+ then: { field: 'items', function: 'defined' },
189
+ },
190
+ };
191
+ /** OpenAPI v2.0-only rules. */
192
+ const oas2Rules = {
193
+ 'oas2-anyOf': {
194
+ description: 'anyOf is not available in OpenAPI v2.0.',
195
+ formats: ['oas2'],
196
+ given: '$..anyOf',
197
+ then: { function: 'falsy' },
198
+ },
199
+ 'oas2-oneOf': {
200
+ description: 'oneOf is not available in OpenAPI v2.0.',
201
+ formats: ['oas2'],
202
+ given: '$..oneOf',
203
+ then: { function: 'falsy' },
204
+ },
205
+ 'oas2-api-host': {
206
+ description: 'OpenAPI host must be present and non-empty.',
207
+ formats: ['oas2'],
208
+ given: '$',
209
+ then: { field: 'host', function: 'truthy' },
210
+ },
211
+ 'oas2-api-schemes': {
212
+ description: 'OpenAPI schemes must be present and non-empty.',
213
+ formats: ['oas2'],
214
+ // Target the root and the `schemes` field so a *missing* `schemes` (not just
215
+ // an empty array) is reported — `given: '$.schemes'` matches nothing when the
216
+ // key is absent, so the rule never fired for a document with no schemes.
217
+ given: '$',
218
+ then: {
219
+ field: 'schemes',
220
+ function: 'schema',
221
+ functionOptions: { schema: { type: 'array', minItems: 1 } },
222
+ },
223
+ },
224
+ 'oas2-discriminator': {
225
+ description: 'Discriminator must reference a required property.',
226
+ formats: ['oas2'],
227
+ given: '$.definitions[*]',
228
+ severity: 'error',
229
+ then: { function: 'oasDiscriminator' },
230
+ },
231
+ 'oas2-host-not-example': {
232
+ description: 'Host should not point to example.com.',
233
+ formats: ['oas2'],
234
+ given: '$',
235
+ severity: 'warn',
236
+ recommended: false,
237
+ then: { field: 'host', function: 'pattern', functionOptions: { notMatch: 'example\\.com' } },
238
+ },
239
+ 'oas2-host-trailing-slash': {
240
+ description: 'Host should not have a trailing slash.',
241
+ formats: ['oas2'],
242
+ given: '$',
243
+ then: { field: 'host', function: 'pattern', functionOptions: { notMatch: '/$' } },
244
+ },
245
+ 'oas2-operation-formData-consume-check': {
246
+ description: 'formData operations must consume form media types.',
247
+ formats: ['oas2'],
248
+ given: OPERATIONS,
249
+ then: { function: 'oasOpFormDataConsumeCheck' },
250
+ },
251
+ 'oas2-operation-security-defined': {
252
+ description: 'Operation security must reference defined securityDefinitions.',
253
+ formats: ['oas2'],
254
+ given: '$',
255
+ then: { function: 'oasOpSecurityDefined', functionOptions: { schemesPath: ['securityDefinitions'] } },
256
+ },
257
+ 'oas2-parameter-description': {
258
+ description: 'Parameters should have a description.',
259
+ formats: ['oas2'],
260
+ given: '$..parameters[*]',
261
+ severity: 'warn',
262
+ recommended: false,
263
+ then: { field: 'description', function: 'truthy' },
264
+ },
265
+ 'oas2-unused-definition': {
266
+ description: 'Definitions should be referenced.',
267
+ formats: ['oas2'],
268
+ given: '$.definitions',
269
+ severity: 'warn',
270
+ resolved: false,
271
+ then: { function: 'unreferencedReusableObject', functionOptions: { reusableObjectsLocation: '#/definitions' } },
272
+ },
273
+ 'oas2-valid-schema-example': {
274
+ description: 'Schema examples must be valid against their schema.',
275
+ formats: ['oas2'],
276
+ given: SCHEMA_EXAMPLE_GIVEN,
277
+ severity: 'error',
278
+ then: { function: 'oasSchemaExample' },
279
+ },
280
+ 'oas2-valid-media-example': {
281
+ description: 'Media type examples must be valid against their schema.',
282
+ formats: ['oas2'],
283
+ // In OpenAPI 2.0 examples live on the Response Object as a MIME-type → value
284
+ // map alongside a sibling `schema`, so target responses that have both.
285
+ given: '$..responses..[?(@ && @.schema && @.examples)]',
286
+ severity: 'error',
287
+ then: { function: 'oasMediaExample', functionOptions: { oasVersion: 2 } },
288
+ },
289
+ 'oas2-schema': {
290
+ description: 'Validate structure of OpenAPI v2 specification.',
291
+ formats: ['oas2'],
292
+ severity: 'error',
293
+ recommended: true,
294
+ resolved: false,
295
+ given: '$',
296
+ then: { function: 'oasSchema', functionOptions: { version: '2.0' } },
297
+ },
298
+ };
299
+ /** OpenAPI v3.x rules. */
300
+ const oas3Rules = {
301
+ 'oas3-api-servers': {
302
+ description: 'OpenAPI 3 documents must have a non-empty servers array.',
303
+ formats: ['oas3'],
304
+ // Target the root and the `servers` field so a *missing* `servers` (not just
305
+ // an empty array) is reported — `given: '$.servers'` matches nothing when the
306
+ // key is absent, so the rule never fired for a document with no servers.
307
+ given: '$',
308
+ then: {
309
+ field: 'servers',
310
+ function: 'schema',
311
+ functionOptions: { schema: { type: 'array', minItems: 1, items: { type: 'object' } } },
312
+ },
313
+ },
314
+ 'oas3-examples-value-or-externalValue': {
315
+ description: 'Example objects must use either value or externalValue, not both.',
316
+ formats: ['oas3'],
317
+ given: [
318
+ '$.components.examples[*]',
319
+ '$..content[*].examples[*]',
320
+ '$..parameters[*].examples[*]',
321
+ '$..headers[*].examples[*]',
322
+ ],
323
+ then: { function: 'oasExampleExternalValue' },
324
+ },
325
+ 'oas3-operation-security-defined': {
326
+ description: 'Operation security must reference defined securitySchemes.',
327
+ formats: ['oas3'],
328
+ given: '$',
329
+ then: { function: 'oasOpSecurityDefined', functionOptions: { schemesPath: ['components', 'securitySchemes'] } },
330
+ },
331
+ 'oas3-parameter-description': {
332
+ description: 'Parameters should have a description.',
333
+ formats: ['oas3'],
334
+ given: '$..parameters[*]',
335
+ severity: 'warn',
336
+ recommended: false,
337
+ then: { field: 'description', function: 'truthy' },
338
+ },
339
+ 'oas3-server-not-example.com': {
340
+ description: 'Server URLs should not point to example.com.',
341
+ formats: ['oas3'],
342
+ given: '$.servers[*].url',
343
+ severity: 'warn',
344
+ recommended: false,
345
+ then: { function: 'pattern', functionOptions: { notMatch: 'example\\.com' } },
346
+ },
347
+ 'oas3-server-trailing-slash': {
348
+ description: 'Server URLs should not have trailing slashes.',
349
+ formats: ['oas3'],
350
+ given: '$.servers[*].url',
351
+ then: { function: 'pattern', functionOptions: { notMatch: './$' } },
352
+ },
353
+ 'oas3-server-variables': {
354
+ description: 'Server variables must be defined and used.',
355
+ formats: ['oas3'],
356
+ // Servers can appear at the root, on a path item, on an operation, and on a
357
+ // Link Object, so cover every location rather than just the root servers.
358
+ given: [
359
+ '$.servers[*]',
360
+ '$.paths[*].servers[*]',
361
+ '$.paths[*][*].servers[*]',
362
+ '$..links[*].server',
363
+ '$.webhooks[*].servers[*]',
364
+ '$.webhooks[*][*].servers[*]',
365
+ ],
366
+ severity: 'error',
367
+ then: { function: 'oasServerVariables' },
368
+ },
369
+ 'oas3-callbacks-in-callbacks': {
370
+ description: 'Callbacks must not be defined within other callbacks.',
371
+ formats: ['oas3'],
372
+ given: '$..callbacks..callbacks',
373
+ then: { function: 'falsy' },
374
+ },
375
+ 'oas3-unused-component': {
376
+ description: 'Reusable components should be referenced.',
377
+ formats: ['oas3'],
378
+ given: '$.components',
379
+ severity: 'warn',
380
+ resolved: false,
381
+ then: { function: 'oasUnusedComponent' },
382
+ },
383
+ 'oas3-valid-schema-example': {
384
+ description: 'Schema examples must be valid against their schema.',
385
+ formats: ['oas3'],
386
+ given: SCHEMA_EXAMPLE_GIVEN,
387
+ severity: 'error',
388
+ then: { function: 'oasSchemaExample' },
389
+ },
390
+ 'oas3-valid-media-example': {
391
+ description: 'Media type examples must be valid against their schema.',
392
+ formats: ['oas3'],
393
+ // Spectral validates examples on media types, parameters, and headers.
394
+ given: ['$..content[*]', '$..parameters[*]', '$..headers[*]'],
395
+ severity: 'error',
396
+ then: { function: 'oasMediaExample', functionOptions: { oasVersion: 3 } },
397
+ },
398
+ 'oas3-schema': {
399
+ description: 'Validate structure of OpenAPI v3.0.x specification.',
400
+ formats: ['oas3_0'],
401
+ severity: 'error',
402
+ recommended: true,
403
+ resolved: false,
404
+ given: '$',
405
+ then: { function: 'oasSchema', functionOptions: { version: '3.0' } },
406
+ },
407
+ };
408
+ /** Rules for features introduced in OpenAPI 3.1 (and still in 3.2). */
409
+ const oas31Rules = {
410
+ 'oas3_1-servers-in-webhook': {
411
+ description: 'Webhooks must not define servers.',
412
+ // Servers can sit on the webhook Path Item itself (`$.webhooks[*]`) as well as
413
+ // on each operation, so target both. Also applies to 3.2.
414
+ formats: ['oas3_1', 'oas3_2'],
415
+ given: [WEBHOOK_OPERATIONS, '$.webhooks[*]'],
416
+ then: { field: 'servers', function: 'falsy' },
417
+ },
418
+ 'oas3_1-callbacks-in-webhook': {
419
+ description: 'Webhooks must not define callbacks.',
420
+ formats: ['oas3_1', 'oas3_2'],
421
+ given: WEBHOOK_OPERATIONS,
422
+ then: { field: 'callbacks', function: 'falsy' },
423
+ },
424
+ 'oas3_1-no-nullable': {
425
+ // `nullable` was removed in OpenAPI 3.1 (JSON Schema 2020-12 uses a `null`
426
+ // type instead) and stays gone in 3.2 — mirroring the oas2-anyOf/oneOf
427
+ // "feature not available in this version" rules. A custom function (targeting
428
+ // the parent of a `nullable` key) is used instead of `$..nullable` + `falsy`
429
+ // so a property literally named `nullable` is not flagged and `nullable: false`
430
+ // still is (so the migration fixer can drop it).
431
+ description: 'nullable is not available in OpenAPI 3.1 or later; use a "null" type instead.',
432
+ formats: ['oas3_1', 'oas3_2'],
433
+ given: '$..nullable^',
434
+ then: { function: 'oasNoNullable' },
435
+ },
436
+ 'oas3_1-license-identifier': {
437
+ // The License Object's `identifier` (SPDX) field was added in 3.1 and is
438
+ // "mutually exclusive of the url field".
439
+ description: 'License object identifier and url are mutually exclusive.',
440
+ formats: ['oas3_1', 'oas3_2'],
441
+ given: '$.info.license',
442
+ severity: 'error',
443
+ then: { function: 'oasMutuallyExclusive', functionOptions: { properties: ['identifier', 'url'] } },
444
+ },
445
+ 'oas3_1-schema': {
446
+ // The official, self-contained OpenAPI 3.1 meta-schema (spec.openapis.org).
447
+ // 3.1 realigned Schema Objects with JSON Schema 2020-12, so the official
448
+ // schema validates the whole document *envelope* while leaving Schema Object
449
+ // internals permissive (a local `$dynamicRef` "#meta" that the runtime
450
+ // validator resolves natively). The 3.0-only `oas3-schema` rule does not
451
+ // apply to 3.1 (different `openapi` version and structure).
452
+ description: 'Validate structure of OpenAPI v3.1 specification.',
453
+ formats: ['oas3_1'],
454
+ severity: 'error',
455
+ recommended: true,
456
+ resolved: false,
457
+ given: '$',
458
+ then: { function: 'oasSchema', functionOptions: { version: '3.1' } },
459
+ },
460
+ 'oas3_1-schema-example-deprecated': {
461
+ // JSON Schema 2020-12 deprecates a Schema Object's singular `example` in
462
+ // favor of the `examples` array. Off by default (recommended: false) since
463
+ // singular examples remain widespread and valid-but-deprecated.
464
+ description: 'Schema "example" is deprecated in OpenAPI 3.1; use "examples" instead.',
465
+ formats: ['oas3_1', 'oas3_2'],
466
+ given: '$..example^',
467
+ severity: 'warn',
468
+ recommended: false,
469
+ then: { function: 'oasSchemaExampleDeprecated' },
470
+ },
471
+ };
472
+ /** Rules for features introduced in OpenAPI 3.2. */
473
+ const oas32Rules = {
474
+ 'oas3_2-schema': {
475
+ // The official, self-contained OpenAPI 3.2 meta-schema (spec.openapis.org).
476
+ // Like `oas3_1-schema`, it validates the document envelope and leaves Schema
477
+ // Object internals to JSON Schema 2020-12 via a local `$dynamicRef`.
478
+ description: 'Validate structure of OpenAPI v3.2 specification.',
479
+ formats: ['oas3_2'],
480
+ severity: 'error',
481
+ recommended: true,
482
+ resolved: false,
483
+ given: '$',
484
+ then: { function: 'oasSchema', functionOptions: { version: '3.2' } },
485
+ },
486
+ 'oas3_2-additional-operations-standard-method': {
487
+ // `additionalOperations` is for HTTP methods without a dedicated fixed
488
+ // field; the spec forbids redefining a standard method there.
489
+ description: 'additionalOperations must not redefine a standard HTTP method that has its own fixed field.',
490
+ formats: ['oas3_2'],
491
+ given: '$..additionalOperations',
492
+ severity: 'error',
493
+ then: { function: 'oasAdditionalOperations' },
494
+ },
495
+ 'oas3_2-server-name-unique': {
496
+ description: 'Server names should be unique across the servers array.',
497
+ formats: ['oas3_2'],
498
+ given: '$.servers',
499
+ severity: 'warn',
500
+ then: { function: 'oasServerNameUnique' },
501
+ },
502
+ 'oas3_2-tag-parent-defined': {
503
+ // Tag hierarchies (3.2): a `parent` must name a defined tag, with no cycles.
504
+ // Mirrors `operation-tag-defined` for the new nesting feature.
505
+ description: 'Tag parent must reference a tag defined in the global tags list, without cycles.',
506
+ formats: ['oas3_2'],
507
+ given: '$.tags',
508
+ severity: 'warn',
509
+ then: { function: 'oasTagParentDefined' },
510
+ },
511
+ 'oas3_2-tag-kind': {
512
+ // `kind` is free-form with a community registry of conventional values, so
513
+ // this is off by default (recommended: false) to avoid flagging valid
514
+ // custom kinds. Only a present-but-unregistered value is flagged.
515
+ description: 'Tag kind should use a registered value (nav, badge, audience).',
516
+ formats: ['oas3_2'],
517
+ given: '$.tags[*]',
518
+ severity: 'warn',
519
+ recommended: false,
520
+ then: { function: 'oasTagKind' },
521
+ },
522
+ 'oas3_2-example-value': {
523
+ // 3.2 added `dataValue`/`serializedValue` to the Example Object with
524
+ // MUST-level exclusivity: dataValue excludes value; serializedValue excludes
525
+ // value and externalValue. (The value/externalValue pair is left to the
526
+ // 3.x-wide oas3-examples-value-or-externalValue rule.)
527
+ description: 'Example object dataValue/serializedValue must not be combined with value or externalValue.',
528
+ formats: ['oas3_2'],
529
+ given: [
530
+ '$.components.examples[*]',
531
+ '$..content[*].examples[*]',
532
+ '$..parameters[*].examples[*]',
533
+ '$..headers[*].examples[*]',
534
+ ],
535
+ severity: 'error',
536
+ then: { function: 'oasExampleValue' },
537
+ },
538
+ };
539
+ /** Loupe's built-in OpenAPI ruleset (`loupe:oas`). */
540
+ export const oas = {
541
+ formats: ['oas2', 'oas3'],
542
+ rules: {
543
+ ...sharedRules,
544
+ ...oas2Rules,
545
+ ...oas3Rules,
546
+ ...oas31Rules,
547
+ ...oas32Rules,
548
+ },
549
+ };