@amritk/lint 0.2.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 (68) hide show
  1. package/README.md +18 -0
  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.js +63 -4
  45. package/dist/rules/openapi/formats.js +11 -4
  46. package/dist/rules/openapi/functions/example-validation.d.ts +16 -3
  47. package/dist/rules/openapi/functions/example-validation.js +102 -39
  48. package/dist/rules/openapi/functions/helpers.d.ts +1 -0
  49. package/dist/rules/openapi/functions/helpers.js +5 -0
  50. package/dist/rules/openapi/functions/index.d.ts +3 -1
  51. package/dist/rules/openapi/functions/index.js +7 -1
  52. package/dist/rules/openapi/functions/oas-additional-operations.js +5 -5
  53. package/dist/rules/openapi/functions/oas-example-external-value.d.ts +11 -0
  54. package/dist/rules/openapi/functions/oas-example-external-value.js +23 -0
  55. package/dist/rules/openapi/functions/oas-no-nullable.d.ts +13 -0
  56. package/dist/rules/openapi/functions/oas-no-nullable.js +22 -0
  57. package/dist/rules/openapi/functions/oas-op-id-unique.js +4 -2
  58. package/dist/rules/openapi/functions/oas-op-params.d.ts +7 -1
  59. package/dist/rules/openapi/functions/oas-op-params.js +35 -10
  60. package/dist/rules/openapi/functions/oas-op-security-defined.js +2 -2
  61. package/dist/rules/openapi/functions/oas-op-success-response.js +6 -1
  62. package/dist/rules/openapi/functions/oas-path-param.d.ts +10 -1
  63. package/dist/rules/openapi/functions/oas-path-param.js +87 -26
  64. package/dist/rules/openapi/functions/oas-server-variables.d.ts +6 -1
  65. package/dist/rules/openapi/functions/oas-server-variables.js +31 -2
  66. package/dist/rules/openapi/functions/oas-unused-component.js +14 -1
  67. package/dist/rules/openapi/oas.js +82 -25
  68. package/package.json +12 -4
@@ -1,8 +1,10 @@
1
1
  import { oasMediaExample, oasSchemaExample } from './example-validation.js';
2
2
  import { oasAdditionalOperations } from './oas-additional-operations.js';
3
3
  import { oasDiscriminator } from './oas-discriminator.js';
4
+ import { oasExampleExternalValue } from './oas-example-external-value.js';
4
5
  import { oasExampleValue } from './oas-example-value.js';
5
6
  import { oasMutuallyExclusive } from './oas-mutually-exclusive.js';
7
+ import { oasNoNullable } from './oas-no-nullable.js';
6
8
  import { oasOpFormDataConsumeCheck } from './oas-op-form-data-consume-check.js';
7
9
  import { oasOpIdUnique } from './oas-op-id-unique.js';
8
10
  import { oasOpParams } from './oas-op-params.js';
@@ -22,8 +24,10 @@ import { refSiblings } from './ref-siblings.js';
22
24
  export { oasMediaExample, oasSchemaExample } from './example-validation.js';
23
25
  export { oasAdditionalOperations } from './oas-additional-operations.js';
24
26
  export { oasDiscriminator } from './oas-discriminator.js';
27
+ export { oasExampleExternalValue } from './oas-example-external-value.js';
25
28
  export { oasExampleValue } from './oas-example-value.js';
26
29
  export { oasMutuallyExclusive } from './oas-mutually-exclusive.js';
30
+ export { oasNoNullable } from './oas-no-nullable.js';
27
31
  export { oasOpFormDataConsumeCheck } from './oas-op-form-data-consume-check.js';
28
32
  export { oasOpIdUnique } from './oas-op-id-unique.js';
29
33
  export { oasOpParams } from './oas-op-params.js';
@@ -54,9 +58,10 @@ export const oasFunctions = {
54
58
  oasDiscriminator,
55
59
  oasServerVariables,
56
60
  oasSchemaExample,
57
- oasMediaExample,
61
+ oasMediaExample: oasMediaExample,
58
62
  oasUnusedComponent,
59
63
  oasMutuallyExclusive: oasMutuallyExclusive,
64
+ oasNoNullable,
60
65
  oasSchema: oasSchema,
61
66
  oasAdditionalOperations,
62
67
  oasServerNameUnique,
@@ -64,4 +69,5 @@ export const oasFunctions = {
64
69
  oasSchemaExampleDeprecated,
65
70
  oasTagKind,
66
71
  oasExampleValue,
72
+ oasExampleExternalValue,
67
73
  };
@@ -1,8 +1,8 @@
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']);
1
+ import { isObject, OPERATION_METHODS } from './helpers.js';
2
+ // Methods that have a dedicated fixed field on the Path Item Object (the eight
3
+ // standard methods plus 3.2's `query`). These MUST NOT be redefined inside the
4
+ // new `additionalOperations` map, which is reserved for non-standard methods.
5
+ const FIXED_PATH_ITEM_METHODS = OPERATION_METHODS;
6
6
  /**
7
7
  * Flags standard HTTP methods inside an OpenAPI 3.2 `additionalOperations` map.
8
8
  * The spec reserves that map for methods without a dedicated fixed field, so a
@@ -0,0 +1,11 @@
1
+ import type { RulesetFunction } from '../../../core/index.js';
2
+ /**
3
+ * Enforces that an Example Object carries exactly one inline value source. Across
4
+ * all of OpenAPI 3.x an Example must use either `value` or `externalValue` (not
5
+ * both, not neither). OpenAPI 3.2 added `dataValue`/`serializedValue` as further
6
+ * ways to supply the example, so their presence also satisfies the "has an
7
+ * example" requirement — otherwise a valid 3.2 `dataValue`-only example would be
8
+ * wrongly flagged. The mutual exclusivity of the new fields is policed separately
9
+ * by `oas3_2-example-value`.
10
+ */
11
+ export declare const oasExampleExternalValue: RulesetFunction;
@@ -0,0 +1,23 @@
1
+ import { isObject } from './helpers.js';
2
+ /**
3
+ * Enforces that an Example Object carries exactly one inline value source. Across
4
+ * all of OpenAPI 3.x an Example must use either `value` or `externalValue` (not
5
+ * both, not neither). OpenAPI 3.2 added `dataValue`/`serializedValue` as further
6
+ * ways to supply the example, so their presence also satisfies the "has an
7
+ * example" requirement — otherwise a valid 3.2 `dataValue`-only example would be
8
+ * wrongly flagged. The mutual exclusivity of the new fields is policed separately
9
+ * by `oas3_2-example-value`.
10
+ */
11
+ export const oasExampleExternalValue = (example) => {
12
+ if (!isObject(example))
13
+ return [];
14
+ // A 3.2 dataValue/serializedValue already provides the example, so value /
15
+ // externalValue are optional in that case.
16
+ if (example['dataValue'] !== undefined || example['serializedValue'] !== undefined)
17
+ return [];
18
+ const present = ['value', 'externalValue'].filter((property) => property in example);
19
+ if (present.length !== 1) {
20
+ return [{ message: 'Example object must have exactly one of "value" or "externalValue"' }];
21
+ }
22
+ return [];
23
+ };
@@ -0,0 +1,13 @@
1
+ import type { RulesetFunction } from '../../../core/index.js';
2
+ /**
3
+ * Flags a Schema Object's `nullable` keyword, removed in OpenAPI 3.1+ (JSON
4
+ * Schema 2020-12 uses a `"null"` type instead). Targets the *parent* of a
5
+ * `nullable` key (`$..nullable^`) rather than `$..nullable` directly, which fixes
6
+ * two problems with the naive `then: falsy` approach:
7
+ * - a property literally named `nullable` (`properties: { nullable: {...} }`)
8
+ * is no longer flagged, because there the `nullable` value is a Schema Object,
9
+ * not the boolean keyword, and
10
+ * - `nullable: false` is flagged too (a boolean of either value counts as the
11
+ * keyword being present), so the migration fixer can drop it.
12
+ */
13
+ export declare const oasNoNullable: RulesetFunction;
@@ -0,0 +1,22 @@
1
+ import { isObject } from './helpers.js';
2
+ /**
3
+ * Flags a Schema Object's `nullable` keyword, removed in OpenAPI 3.1+ (JSON
4
+ * Schema 2020-12 uses a `"null"` type instead). Targets the *parent* of a
5
+ * `nullable` key (`$..nullable^`) rather than `$..nullable` directly, which fixes
6
+ * two problems with the naive `then: falsy` approach:
7
+ * - a property literally named `nullable` (`properties: { nullable: {...} }`)
8
+ * is no longer flagged, because there the `nullable` value is a Schema Object,
9
+ * not the boolean keyword, and
10
+ * - `nullable: false` is flagged too (a boolean of either value counts as the
11
+ * keyword being present), so the migration fixer can drop it.
12
+ */
13
+ export const oasNoNullable = (parent, _options, context) => {
14
+ if (!isObject(parent) || typeof parent['nullable'] !== 'boolean')
15
+ return [];
16
+ return [
17
+ {
18
+ message: 'nullable is not available in OpenAPI 3.1 or later; use a "null" type instead.',
19
+ path: [...context.path, 'nullable'],
20
+ },
21
+ ];
22
+ };
@@ -1,4 +1,4 @@
1
- import { isObject } from './helpers.js';
1
+ import { isObject, OPERATION_METHODS } from './helpers.js';
2
2
  /** Ensures `operationId` values are unique across the document. */
3
3
  export const oasOpIdUnique = (paths, _options, context) => {
4
4
  if (!isObject(paths))
@@ -9,7 +9,9 @@ export const oasOpIdUnique = (paths, _options, context) => {
9
9
  if (!isObject(item))
10
10
  continue;
11
11
  for (const [method, operation] of Object.entries(item)) {
12
- if (!isObject(operation))
12
+ // Only real operations carry an `operationId`; an `x-`extension object with
13
+ // an `operationId`-looking key would otherwise create a phantom duplicate.
14
+ if (!OPERATION_METHODS.has(method) || !isObject(operation))
13
15
  continue;
14
16
  const id = operation['operationId'];
15
17
  if (typeof id !== 'string')
@@ -1,3 +1,9 @@
1
1
  import type { RulesetFunction } from '../../../core/index.js';
2
- /** Flags duplicate `name`+`in` parameter combinations on an operation. */
2
+ /**
3
+ * Validates an operation's `parameters` array, mirroring Spectral's `oasOpParams`:
4
+ * flags duplicate `name`+`in` combinations, and — for OpenAPI 2.0, where the body
5
+ * is a parameter — flags more than one `in: body` parameter (even with different
6
+ * names) as well as mixing `in: body` with `in: formData`. `$ref` entries are
7
+ * skipped because their real `name`/`in` is only known after resolution.
8
+ */
3
9
  export declare const oasOpParams: RulesetFunction;
@@ -1,21 +1,46 @@
1
1
  import { isObject } from './helpers.js';
2
- /** Flags duplicate `name`+`in` parameter combinations on an operation. */
2
+ /**
3
+ * Validates an operation's `parameters` array, mirroring Spectral's `oasOpParams`:
4
+ * flags duplicate `name`+`in` combinations, and — for OpenAPI 2.0, where the body
5
+ * is a parameter — flags more than one `in: body` parameter (even with different
6
+ * names) as well as mixing `in: body` with `in: formData`. `$ref` entries are
7
+ * skipped because their real `name`/`in` is only known after resolution.
8
+ */
3
9
  export const oasOpParams = (params, _options, context) => {
4
10
  if (!Array.isArray(params))
5
11
  return [];
6
- const seen = new Set();
7
12
  const results = [];
13
+ const seen = new Set();
14
+ const bodyIndices = [];
15
+ const formDataIndices = [];
8
16
  params.forEach((param, index) => {
9
- if (!isObject(param) || param['name'] === undefined || param['in'] === undefined)
17
+ if (!isObject(param) || '$ref' in param)
10
18
  return;
11
- const key = `${String(param['in'])}:${String(param['name'])}`;
12
- if (seen.has(key)) {
13
- results.push({
14
- message: `Duplicate parameter "${String(param['name'])}" in "${String(param['in'])}"`,
15
- path: [...context.path, index],
16
- });
19
+ if (param['name'] !== undefined && param['in'] !== undefined) {
20
+ const key = `${String(param['in'])}:${String(param['name'])}`;
21
+ if (seen.has(key)) {
22
+ results.push({
23
+ message: `Duplicate parameter "${String(param['name'])}" in "${String(param['in'])}"`,
24
+ path: [...context.path, index],
25
+ });
26
+ }
27
+ seen.add(key);
17
28
  }
18
- seen.add(key);
29
+ if (param['in'] === 'body')
30
+ bodyIndices.push(index);
31
+ else if (param['in'] === 'formData')
32
+ formDataIndices.push(index);
19
33
  });
34
+ // OAS2 allows at most one body parameter, and body and formData are mutually
35
+ // exclusive within a single operation.
36
+ if (bodyIndices.length > 0 && formDataIndices.length > 0) {
37
+ results.push({ message: 'Operation must not have both "in:body" and "in:formData" parameters' });
38
+ }
39
+ for (let i = 1; i < bodyIndices.length; i++) {
40
+ results.push({
41
+ message: 'Operation must not have more than a single instance of the "in:body" parameter',
42
+ path: [...context.path, bodyIndices[i]],
43
+ });
44
+ }
20
45
  return results;
21
46
  };
@@ -1,4 +1,4 @@
1
- import { HTTP_METHODS, isObject } from './helpers.js';
1
+ import { isObject, OPERATION_METHODS } from './helpers.js';
2
2
  /** Walks `path` into `root`, returning the nested value or undefined. */
3
3
  const getIn = (root, path) => {
4
4
  let current = root;
@@ -35,7 +35,7 @@ export const oasOpSecurityDefined = (root, options, context) => {
35
35
  if (!isObject(item))
36
36
  continue;
37
37
  for (const [method, operation] of Object.entries(item)) {
38
- if (HTTP_METHODS.has(method) && isObject(operation)) {
38
+ if (OPERATION_METHODS.has(method) && isObject(operation)) {
39
39
  check(operation['security'], [...context.path, 'paths', path, method, 'security']);
40
40
  }
41
41
  }
@@ -1,9 +1,14 @@
1
1
  import { isObject } from './helpers.js';
2
+ // A success response is any 2xx/3xx status code, or a `2XX`/`3XX` wildcard (3.x).
3
+ // `default` is intentionally NOT counted — it can carry any status, including an
4
+ // error, so it does not prove the operation has a success response (this matches
5
+ // Spectral, which counts only codes in the [200, 400) range plus the wildcards).
6
+ const SUCCESS_CODE = /^[23](\d\d|XX)$/;
2
7
  /** Ensures every operation declares at least one 2xx or 3xx response. */
3
8
  export const oasOpSuccessResponse = (responses) => {
4
9
  if (!isObject(responses))
5
10
  return [];
6
- const hasSuccess = Object.keys(responses).some((code) => /^[23]\d\d$/.test(code) || code === 'default');
11
+ const hasSuccess = Object.keys(responses).some((code) => SUCCESS_CODE.test(code));
7
12
  if (!hasSuccess) {
8
13
  return [{ message: 'Operation must define at least one 2xx or 3xx response' }];
9
14
  }
@@ -1,3 +1,12 @@
1
1
  import type { RulesetFunction } from '../../../core/index.js';
2
- /** Validates path templating: declared `{params}` must have matching path parameters. */
2
+ /**
3
+ * Validates path templating per operation, mirroring Spectral's `oasPathParam`.
4
+ * For each operation on a path it checks that (a) every `{template}` in the path
5
+ * has a matching `in: path` definition, (b) every `in: path` definition is used
6
+ * in the template, (c) path parameters carry `required: true`, (d) a template is
7
+ * not repeated in the path key, and (e) a parameter is not defined twice. Path
8
+ * Item level parameters are merged with the operation's own parameters. Runs on
9
+ * the resolved document (the rule is `resolved: true`), so `$ref`d parameters are
10
+ * already inlined.
11
+ */
3
12
  export declare const oasPathParam: RulesetFunction;
@@ -1,5 +1,40 @@
1
- import { HTTP_METHODS, isObject } from './helpers.js';
2
- /** Validates path templating: declared `{params}` must have matching path parameters. */
1
+ import { isObject, OPERATION_METHODS } from './helpers.js';
2
+ // Path template parameters, e.g. `{id}`. Matches Spectral's grammar so matrix
3
+ // (`{;id}`), optional (`{id?}`), and explode (`{id*}`) markers are stripped off
4
+ // the captured name before comparison.
5
+ const PATH_TEMPLATE = /(\{;?\??[a-zA-Z0-9_-]+\*?\})/g;
6
+ const namedPathParam = (param) => param['in'] === 'path' && typeof param['name'] === 'string' ? param['name'] : undefined;
7
+ /**
8
+ * Records a single `in: path` definition, emitting Spectral's `required: true`
9
+ * and duplicate-definition findings. Returns the parameter name when it is the
10
+ * first definition seen (so the caller registers it as usable), else undefined.
11
+ */
12
+ const recordPathParam = (param, definitionPath, seen, results) => {
13
+ const name = namedPathParam(param);
14
+ if (name === undefined)
15
+ return undefined;
16
+ if (param['required'] !== true) {
17
+ results.push({
18
+ message: `Path parameter "${name}" must have a "required" property that is set to "true"`,
19
+ path: definitionPath,
20
+ });
21
+ }
22
+ if (name in seen) {
23
+ results.push({ message: `Path parameter "${name}" must not be defined multiple times`, path: definitionPath });
24
+ return undefined;
25
+ }
26
+ return name;
27
+ };
28
+ /**
29
+ * Validates path templating per operation, mirroring Spectral's `oasPathParam`.
30
+ * For each operation on a path it checks that (a) every `{template}` in the path
31
+ * has a matching `in: path` definition, (b) every `in: path` definition is used
32
+ * in the template, (c) path parameters carry `required: true`, (d) a template is
33
+ * not repeated in the path key, and (e) a parameter is not defined twice. Path
34
+ * Item level parameters are merged with the operation's own parameters. Runs on
35
+ * the resolved document (the rule is `resolved: true`), so `$ref`d parameters are
36
+ * already inlined.
37
+ */
3
38
  export const oasPathParam = (paths, _options, context) => {
4
39
  if (!isObject(paths))
5
40
  return [];
@@ -7,38 +42,64 @@ export const oasPathParam = (paths, _options, context) => {
7
42
  for (const [path, item] of Object.entries(paths)) {
8
43
  if (!isObject(item))
9
44
  continue;
10
- const templates = [...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
11
- const seen = new Set();
12
- for (const template of templates) {
13
- if (seen.has(template)) {
45
+ // (d) Templates declared in the path key, deduplicated (a repeat is an error).
46
+ const templates = [];
47
+ for (const match of path.matchAll(PATH_TEMPLATE)) {
48
+ const name = match[0].replace(/[{}?*;]/g, '');
49
+ if (templates.includes(name)) {
14
50
  results.push({
15
- message: `Path "${path}" uses parameter "{${template}}" more than once`,
51
+ message: `Path "${path}" must not use parameter "{${name}}" more than once`,
16
52
  path: [...context.path, path],
17
53
  });
18
54
  }
19
- seen.add(template);
20
- }
21
- const declared = new Set();
22
- const collect = (params) => {
23
- if (!Array.isArray(params))
24
- return;
25
- for (const param of params) {
26
- if (isObject(param) && param['in'] === 'path' && typeof param['name'] === 'string')
27
- declared.add(param['name']);
55
+ else {
56
+ templates.push(name);
28
57
  }
29
- };
30
- collect(item['parameters']);
31
- for (const [method, operation] of Object.entries(item)) {
32
- if (HTTP_METHODS.has(method) && isObject(operation))
33
- collect(operation['parameters']);
34
58
  }
35
- for (const template of seen) {
36
- if (!declared.has(template)) {
37
- results.push({
38
- message: `Path parameter "{${template}}" in "${path}" has no matching path parameter definition`,
39
- path: [...context.path, path],
59
+ // Path Item level parameters apply to every operation on the path.
60
+ const topParams = {};
61
+ if (Array.isArray(item['parameters'])) {
62
+ item['parameters'].forEach((param, index) => {
63
+ if (!isObject(param))
64
+ return;
65
+ const definitionPath = [...context.path, path, 'parameters', index];
66
+ const name = recordPathParam(param, definitionPath, topParams, results);
67
+ if (name !== undefined)
68
+ topParams[name] = definitionPath;
69
+ });
70
+ }
71
+ for (const [method, operation] of Object.entries(item)) {
72
+ if (method === 'parameters' || !OPERATION_METHODS.has(method) || !isObject(operation))
73
+ continue;
74
+ const operationPath = [...context.path, path, method];
75
+ // Each operation is evaluated against its own params plus the shared path-item ones.
76
+ const operationParams = {};
77
+ if (Array.isArray(operation['parameters'])) {
78
+ operation['parameters'].forEach((param, index) => {
79
+ if (!isObject(param))
80
+ return;
81
+ const definitionPath = [...operationPath, 'parameters', index];
82
+ const name = recordPathParam(param, definitionPath, operationParams, results);
83
+ if (name !== undefined)
84
+ operationParams[name] = definitionPath;
40
85
  });
41
86
  }
87
+ const defined = { ...topParams, ...operationParams };
88
+ // (b) Every defined `in: path` parameter must appear in the path template.
89
+ for (const [name, definitionPath] of Object.entries(defined)) {
90
+ if (definitionPath && !templates.includes(name)) {
91
+ results.push({ message: `Parameter "${name}" must be used in path "${path}"`, path: definitionPath });
92
+ }
93
+ }
94
+ // (a) Every `{template}` must have a matching definition on the operation.
95
+ for (const name of templates) {
96
+ if (!(name in defined)) {
97
+ results.push({
98
+ message: `Operation must define path parameter "{${name}}" as expected by path "${path}"`,
99
+ path: operationPath,
100
+ });
101
+ }
102
+ }
42
103
  }
43
104
  }
44
105
  return results;
@@ -1,3 +1,8 @@
1
1
  import type { RulesetFunction } from '../../../core/index.js';
2
- /** Validates server variables are declared and used. */
2
+ /**
3
+ * Validates a Server Object's `variables`, mirroring Spectral's `serverVariables`:
4
+ * every `{template}` in the URL must have a matching variable and vice versa, and
5
+ * each defined variable must have a `default`, a non-empty `enum` when present,
6
+ * and a `default` that is listed in that `enum`.
7
+ */
3
8
  export declare const oasServerVariables: RulesetFunction;
@@ -1,5 +1,10 @@
1
1
  import { isObject } from './helpers.js';
2
- /** Validates server variables are declared and used. */
2
+ /**
3
+ * Validates a Server Object's `variables`, mirroring Spectral's `serverVariables`:
4
+ * every `{template}` in the URL must have a matching variable and vice versa, and
5
+ * each defined variable must have a `default`, a non-empty `enum` when present,
6
+ * and a `default` that is listed in that `enum`.
7
+ */
3
8
  export const oasServerVariables = (server, _options, context) => {
4
9
  if (!isObject(server) || typeof server['url'] !== 'string')
5
10
  return [];
@@ -11,13 +16,37 @@ export const oasServerVariables = (server, _options, context) => {
11
16
  results.push({ message: `Server variable "${template}" is not defined`, path: [...context.path, 'variables'] });
12
17
  }
13
18
  }
14
- for (const name of Object.keys(variables)) {
19
+ for (const [name, variable] of Object.entries(variables)) {
15
20
  if (!templates.includes(name)) {
16
21
  results.push({
17
22
  message: `Server variable "${name}" is not used in the URL`,
18
23
  path: [...context.path, 'variables', name],
19
24
  });
20
25
  }
26
+ if (!isObject(variable))
27
+ continue;
28
+ const hasDefault = variable['default'] !== undefined;
29
+ if (!hasDefault) {
30
+ results.push({
31
+ message: `Server variable "${name}" has a missing default`,
32
+ path: [...context.path, 'variables', name],
33
+ });
34
+ }
35
+ if ('enum' in variable) {
36
+ const enumValues = Array.isArray(variable['enum']) ? variable['enum'] : [];
37
+ if (enumValues.length === 0) {
38
+ results.push({
39
+ message: `Server variable "${name}" has an empty enum`,
40
+ path: [...context.path, 'variables', name, 'enum'],
41
+ });
42
+ }
43
+ else if (hasDefault && !enumValues.includes(variable['default'])) {
44
+ results.push({
45
+ message: `Server variable "${name}" has a default not listed in the enum`,
46
+ path: [...context.path, 'variables', name, 'default'],
47
+ });
48
+ }
49
+ }
21
50
  }
22
51
  return results;
23
52
  };
@@ -1,6 +1,7 @@
1
1
  import { isObject } from './helpers.js';
2
2
  // Reusable component types that are referenced via `$ref` (securitySchemes are
3
3
  // referenced by name in `security`, not via `$ref`, so they are excluded).
4
+ // `pathItems` was added in OpenAPI 3.1 (referenced from `webhooks` / `callbacks`).
4
5
  const REUSABLE_COMPONENT_TYPES = [
5
6
  'schemas',
6
7
  'responses',
@@ -10,6 +11,7 @@ const REUSABLE_COMPONENT_TYPES = [
10
11
  'headers',
11
12
  'links',
12
13
  'callbacks',
14
+ 'pathItems',
13
15
  ];
14
16
  /** Collects every `$ref` string anywhere in `node` into `into`. */
15
17
  const collectRefs = (node, into) => {
@@ -37,13 +39,24 @@ export const oasUnusedComponent = (components, _options, context) => {
37
39
  return [];
38
40
  const refs = new Set();
39
41
  collectRefs(context.document.data, refs);
42
+ // A component counts as used when a `$ref` targets it OR points *into* it
43
+ // (e.g. `#/components/schemas/Pet/properties/id` still uses `Pet`), so match by
44
+ // prefix rather than exact string — an interior ref must not leave the parent
45
+ // flagged as unused.
46
+ const isReferenced = (base) => {
47
+ for (const ref of refs) {
48
+ if (ref === base || ref.startsWith(`${base}/`))
49
+ return true;
50
+ }
51
+ return false;
52
+ };
40
53
  const results = [];
41
54
  for (const type of REUSABLE_COMPONENT_TYPES) {
42
55
  const group = components[type];
43
56
  if (!isObject(group))
44
57
  continue;
45
58
  for (const key of Object.keys(group)) {
46
- if (!refs.has(`#/components/${type}/${key}`)) {
59
+ if (!isReferenced(`#/components/${type}/${key}`)) {
47
60
  results.push({ message: 'Potentially unused component has been detected.', path: [...context.path, type, key] });
48
61
  }
49
62
  }