@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.
- package/README.md +52 -1
- package/dist/core/glob.d.ts +1 -1
- package/dist/core/glob.js +89 -5
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/jsonpath.d.ts +17 -1
- package/dist/core/jsonpath.js +218 -23
- package/dist/core/lint.d.ts +15 -8
- package/dist/core/lint.js +12 -3
- package/dist/core/plugin.d.ts +6 -0
- package/dist/core/plugin.js +6 -0
- package/dist/core/pointers.js +15 -15
- package/dist/core/ruleset.js +0 -0
- package/dist/core/runner.d.ts +6 -1
- package/dist/core/runner.js +127 -43
- package/dist/core/types.d.ts +17 -2
- package/dist/core/validate-ruleset.js +16 -0
- package/dist/fix/apply.d.ts +8 -2
- package/dist/fix/apply.js +68 -18
- package/dist/functions/alphabetical.js +40 -13
- package/dist/functions/casing.js +27 -5
- package/dist/functions/enumeration.d.ts +5 -3
- package/dist/functions/enumeration.js +18 -1
- package/dist/functions/index.d.ts +1 -0
- package/dist/functions/index.js +3 -0
- package/dist/functions/length.d.ts +13 -3
- package/dist/functions/length.js +11 -3
- package/dist/functions/or.d.ts +11 -0
- package/dist/functions/or.js +25 -0
- package/dist/functions/pattern.d.ts +5 -3
- package/dist/functions/pattern.js +42 -9
- package/dist/functions/schema.d.ts +13 -0
- package/dist/functions/schema.js +95 -2
- package/dist/functions/typed-enum.js +7 -1
- package/dist/functions/unreferenced-reusable-object.d.ts +7 -1
- package/dist/functions/unreferenced-reusable-object.js +18 -3
- package/dist/functions/xor.js +8 -1
- package/dist/index.js +9 -1
- package/dist/parsers/edit-model.d.ts +15 -0
- package/dist/parsers/edit-model.js +210 -41
- package/dist/parsers/types.d.ts +14 -2
- package/dist/parsers/yaml.d.ts +10 -0
- package/dist/parsers/yaml.js +174 -26
- package/dist/rules/openapi/fixers.d.ts +7 -0
- package/dist/rules/openapi/fixers.js +248 -0
- package/dist/rules/openapi/formats.d.ts +13 -0
- package/dist/rules/openapi/formats.js +30 -0
- package/dist/rules/openapi/functions/example-validation.d.ts +18 -0
- package/dist/rules/openapi/functions/example-validation.js +160 -0
- package/dist/rules/openapi/functions/helpers.d.ts +4 -0
- package/dist/rules/openapi/functions/helpers.js +10 -0
- package/dist/rules/openapi/functions/index.d.ts +26 -0
- package/dist/rules/openapi/functions/index.js +73 -0
- package/dist/rules/openapi/functions/oas-additional-operations.d.ts +8 -0
- package/dist/rules/openapi/functions/oas-additional-operations.js +25 -0
- package/dist/rules/openapi/functions/oas-discriminator.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-discriminator.js +23 -0
- package/dist/rules/openapi/functions/oas-example-external-value.d.ts +11 -0
- package/dist/rules/openapi/functions/oas-example-external-value.js +23 -0
- package/dist/rules/openapi/functions/oas-example-value.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-example-value.js +31 -0
- package/dist/rules/openapi/functions/oas-mutually-exclusive.d.ts +10 -0
- package/dist/rules/openapi/functions/oas-mutually-exclusive.js +20 -0
- package/dist/rules/openapi/functions/oas-no-nullable.d.ts +13 -0
- package/dist/rules/openapi/functions/oas-no-nullable.js +22 -0
- package/dist/rules/openapi/functions/oas-op-form-data-consume-check.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-form-data-consume-check.js +21 -0
- package/dist/rules/openapi/functions/oas-op-id-unique.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-id-unique.js +29 -0
- package/dist/rules/openapi/functions/oas-op-params.d.ts +9 -0
- package/dist/rules/openapi/functions/oas-op-params.js +46 -0
- package/dist/rules/openapi/functions/oas-op-security-defined.d.ts +5 -0
- package/dist/rules/openapi/functions/oas-op-security-defined.js +44 -0
- package/dist/rules/openapi/functions/oas-op-success-response.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-success-response.js +16 -0
- package/dist/rules/openapi/functions/oas-path-param.d.ts +12 -0
- package/dist/rules/openapi/functions/oas-path-param.js +106 -0
- package/dist/rules/openapi/functions/oas-schema-example-deprecated.d.ts +10 -0
- package/dist/rules/openapi/functions/oas-schema-example-deprecated.js +42 -0
- package/dist/rules/openapi/functions/oas-schema.d.ts +15 -0
- package/dist/rules/openapi/functions/oas-schema.js +15 -0
- package/dist/rules/openapi/functions/oas-server-name-unique.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-server-name-unique.js +20 -0
- package/dist/rules/openapi/functions/oas-server-variables.d.ts +8 -0
- package/dist/rules/openapi/functions/oas-server-variables.js +52 -0
- package/dist/rules/openapi/functions/oas-tag-defined.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-tag-defined.js +21 -0
- package/dist/rules/openapi/functions/oas-tag-kind.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-tag-kind.js +17 -0
- package/dist/rules/openapi/functions/oas-tag-parent-defined.d.ts +7 -0
- package/dist/rules/openapi/functions/oas-tag-parent-defined.js +45 -0
- package/dist/rules/openapi/functions/oas-tags-unique.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-tags-unique.js +17 -0
- package/dist/rules/openapi/functions/oas-unused-component.d.ts +7 -0
- package/dist/rules/openapi/functions/oas-unused-component.js +65 -0
- package/dist/rules/openapi/functions/ref-siblings.d.ts +3 -0
- package/dist/rules/openapi/functions/ref-siblings.js +13 -0
- package/dist/rules/openapi/index.d.ts +25 -0
- package/dist/rules/openapi/index.js +127 -0
- package/dist/rules/openapi/oas.d.ts +3 -0
- package/dist/rules/openapi/oas.js +549 -0
- package/dist/rules/openapi/schemas/index.d.ts +8 -0
- package/dist/rules/openapi/schemas/index.js +36 -0
- package/dist/rules/openapi/schemas/oas20.json +1592 -0
- package/dist/rules/openapi/schemas/oas30.json +1651 -0
- package/dist/rules/openapi/schemas/oas31.json +1412 -0
- package/dist/rules/openapi/schemas/oas32.json +1684 -0
- package/package.json +17 -5
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isObject, OPERATION_METHODS } from './helpers.js';
|
|
2
|
+
/** Walks `path` into `root`, returning the nested value or undefined. */
|
|
3
|
+
const getIn = (root, path) => {
|
|
4
|
+
let current = root;
|
|
5
|
+
for (const key of path) {
|
|
6
|
+
if (!isObject(current))
|
|
7
|
+
return undefined;
|
|
8
|
+
current = current[key];
|
|
9
|
+
}
|
|
10
|
+
return current;
|
|
11
|
+
};
|
|
12
|
+
/** Validates that every referenced security scheme is defined. */
|
|
13
|
+
export const oasOpSecurityDefined = (root, options, context) => {
|
|
14
|
+
if (!isObject(root))
|
|
15
|
+
return [];
|
|
16
|
+
const schemes = getIn(root, options?.schemesPath ?? []);
|
|
17
|
+
const defined = new Set(isObject(schemes) ? Object.keys(schemes) : []);
|
|
18
|
+
const results = [];
|
|
19
|
+
const check = (security, path) => {
|
|
20
|
+
if (!Array.isArray(security))
|
|
21
|
+
return;
|
|
22
|
+
security.forEach((requirement, index) => {
|
|
23
|
+
if (!isObject(requirement))
|
|
24
|
+
return;
|
|
25
|
+
for (const name of Object.keys(requirement)) {
|
|
26
|
+
if (!defined.has(name)) {
|
|
27
|
+
results.push({ message: `Security scheme "${name}" is not defined`, path: [...path, index, name] });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
check(root['security'], [...context.path, 'security']);
|
|
33
|
+
const paths = isObject(root['paths']) ? root['paths'] : {};
|
|
34
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
35
|
+
if (!isObject(item))
|
|
36
|
+
continue;
|
|
37
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
38
|
+
if (OPERATION_METHODS.has(method) && isObject(operation)) {
|
|
39
|
+
check(operation['security'], [...context.path, 'paths', path, method, 'security']);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
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)$/;
|
|
7
|
+
/** Ensures every operation declares at least one 2xx or 3xx response. */
|
|
8
|
+
export const oasOpSuccessResponse = (responses) => {
|
|
9
|
+
if (!isObject(responses))
|
|
10
|
+
return [];
|
|
11
|
+
const hasSuccess = Object.keys(responses).some((code) => SUCCESS_CODE.test(code));
|
|
12
|
+
if (!hasSuccess) {
|
|
13
|
+
return [{ message: 'Operation must define at least one 2xx or 3xx response' }];
|
|
14
|
+
}
|
|
15
|
+
return [];
|
|
16
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/index.js';
|
|
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
|
+
*/
|
|
12
|
+
export declare const oasPathParam: RulesetFunction;
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
*/
|
|
38
|
+
export const oasPathParam = (paths, _options, context) => {
|
|
39
|
+
if (!isObject(paths))
|
|
40
|
+
return [];
|
|
41
|
+
const results = [];
|
|
42
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
43
|
+
if (!isObject(item))
|
|
44
|
+
continue;
|
|
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)) {
|
|
50
|
+
results.push({
|
|
51
|
+
message: `Path "${path}" must not use parameter "{${name}}" more than once`,
|
|
52
|
+
path: [...context.path, path],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
templates.push(name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
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;
|
|
85
|
+
});
|
|
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
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return results;
|
|
106
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Flags a Schema Object's singular `example`, deprecated in OpenAPI 3.1 (JSON
|
|
4
|
+
* Schema 2020-12) in favor of the `examples` array. Targets the parent of an
|
|
5
|
+
* `example` key (`$..example^`); to avoid false positives it only fires when the
|
|
6
|
+
* parent looks like a Schema Object — it carries a JSON Schema keyword and is not
|
|
7
|
+
* a Media Type / Parameter / Header object (those have a `schema` field and keep
|
|
8
|
+
* a valid singular `example`).
|
|
9
|
+
*/
|
|
10
|
+
export declare const oasSchemaExampleDeprecated: RulesetFunction;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
// JSON Schema keywords that mark an object as a Schema Object (vs. a Media Type
|
|
3
|
+
// or Parameter Object, whose singular `example` is *not* deprecated in 3.1+).
|
|
4
|
+
const SCHEMA_KEYWORDS = new Set([
|
|
5
|
+
'type',
|
|
6
|
+
'properties',
|
|
7
|
+
'items',
|
|
8
|
+
'allOf',
|
|
9
|
+
'anyOf',
|
|
10
|
+
'oneOf',
|
|
11
|
+
'not',
|
|
12
|
+
'enum',
|
|
13
|
+
'format',
|
|
14
|
+
'required',
|
|
15
|
+
'additionalProperties',
|
|
16
|
+
'patternProperties',
|
|
17
|
+
'prefixItems',
|
|
18
|
+
'$ref',
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* Flags a Schema Object's singular `example`, deprecated in OpenAPI 3.1 (JSON
|
|
22
|
+
* Schema 2020-12) in favor of the `examples` array. Targets the parent of an
|
|
23
|
+
* `example` key (`$..example^`); to avoid false positives it only fires when the
|
|
24
|
+
* parent looks like a Schema Object — it carries a JSON Schema keyword and is not
|
|
25
|
+
* a Media Type / Parameter / Header object (those have a `schema` field and keep
|
|
26
|
+
* a valid singular `example`).
|
|
27
|
+
*/
|
|
28
|
+
export const oasSchemaExampleDeprecated = (input, _options, context) => {
|
|
29
|
+
if (!isObject(input) || input['example'] === undefined)
|
|
30
|
+
return [];
|
|
31
|
+
if ('schema' in input)
|
|
32
|
+
return [];
|
|
33
|
+
const looksLikeSchema = Object.keys(input).some((key) => SCHEMA_KEYWORDS.has(key));
|
|
34
|
+
if (!looksLikeSchema)
|
|
35
|
+
return [];
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
message: 'Schema "example" is deprecated in OpenAPI 3.1; use "examples" instead.',
|
|
39
|
+
path: [...context.path, 'example'],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/index.js';
|
|
2
|
+
import { type OasVersion } from '../schemas/index.js';
|
|
3
|
+
/** Options for {@link oasSchema}: which OpenAPI version's meta-schema to validate against. */
|
|
4
|
+
export type IOasSchemaOptions = {
|
|
5
|
+
version: OasVersion;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Validates a whole OpenAPI document against the official structural meta-schema
|
|
9
|
+
* for a given `version`, loading that version's schema lazily on first use. The
|
|
10
|
+
* `*-schema` rules are format-gated to a single version, so linting a document
|
|
11
|
+
* only ever reads its own version's schema file — the other versions' schemas
|
|
12
|
+
* are never loaded. Delegates the actual validation to the built-in `schema`
|
|
13
|
+
* function (which caches the prepared validator by schema identity).
|
|
14
|
+
*/
|
|
15
|
+
export declare const oasSchema: RulesetFunction<unknown, IOasSchemaOptions>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { schema as schemaFunction } from '../../../functions/index.js';
|
|
2
|
+
import { loadOasSchema } from '../schemas/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Validates a whole OpenAPI document against the official structural meta-schema
|
|
5
|
+
* for a given `version`, loading that version's schema lazily on first use. The
|
|
6
|
+
* `*-schema` rules are format-gated to a single version, so linting a document
|
|
7
|
+
* only ever reads its own version's schema file — the other versions' schemas
|
|
8
|
+
* are never loaded. Delegates the actual validation to the built-in `schema`
|
|
9
|
+
* function (which caches the prepared validator by schema identity).
|
|
10
|
+
*/
|
|
11
|
+
export const oasSchema = (input, options, context) => {
|
|
12
|
+
if (!options?.version)
|
|
13
|
+
return [];
|
|
14
|
+
return schemaFunction(input, { schema: loadOasSchema(options.version) }, context);
|
|
15
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/** Flags duplicate OpenAPI 3.2 Server Object `name` values across the servers array. */
|
|
3
|
+
export const oasServerNameUnique = (servers, _options, context) => {
|
|
4
|
+
if (!Array.isArray(servers))
|
|
5
|
+
return [];
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
const results = [];
|
|
8
|
+
servers.forEach((server, index) => {
|
|
9
|
+
if (!isObject(server) || typeof server['name'] !== 'string')
|
|
10
|
+
return;
|
|
11
|
+
if (seen.has(server['name'])) {
|
|
12
|
+
results.push({
|
|
13
|
+
message: `Server name "${server['name']}" must be unique`,
|
|
14
|
+
path: [...context.path, index, 'name'],
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
seen.add(server['name']);
|
|
18
|
+
});
|
|
19
|
+
return results;
|
|
20
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/index.js';
|
|
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
|
+
*/
|
|
8
|
+
export declare const oasServerVariables: RulesetFunction;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
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
|
+
*/
|
|
8
|
+
export const oasServerVariables = (server, _options, context) => {
|
|
9
|
+
if (!isObject(server) || typeof server['url'] !== 'string')
|
|
10
|
+
return [];
|
|
11
|
+
const templates = [...server['url'].matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
|
|
12
|
+
const variables = isObject(server['variables']) ? server['variables'] : {};
|
|
13
|
+
const results = [];
|
|
14
|
+
for (const template of templates) {
|
|
15
|
+
if (!(template in variables)) {
|
|
16
|
+
results.push({ message: `Server variable "${template}" is not defined`, path: [...context.path, 'variables'] });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (const [name, variable] of Object.entries(variables)) {
|
|
20
|
+
if (!templates.includes(name)) {
|
|
21
|
+
results.push({
|
|
22
|
+
message: `Server variable "${name}" is not used in the URL`,
|
|
23
|
+
path: [...context.path, 'variables', name],
|
|
24
|
+
});
|
|
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
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return results;
|
|
52
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/** Ensures each operation tag is declared in the global `tags` list. */
|
|
3
|
+
export const oasTagDefined = (input, _options, context) => {
|
|
4
|
+
const root = context.document.data;
|
|
5
|
+
if (!isObject(root) || !isObject(input))
|
|
6
|
+
return [];
|
|
7
|
+
const globalTags = new Set((Array.isArray(root['tags']) ? root['tags'] : [])
|
|
8
|
+
.map((tag) => (isObject(tag) ? tag['name'] : undefined))
|
|
9
|
+
.filter((name) => typeof name === 'string'));
|
|
10
|
+
const tags = Array.isArray(input['tags']) ? input['tags'] : [];
|
|
11
|
+
const results = [];
|
|
12
|
+
tags.forEach((tag, index) => {
|
|
13
|
+
if (typeof tag === 'string' && !globalTags.has(tag)) {
|
|
14
|
+
results.push({
|
|
15
|
+
message: `Operation tag "${tag}" is not defined in the global tags`,
|
|
16
|
+
path: [...context.path, 'tags', index],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return results;
|
|
21
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
// Registered OpenAPI 3.2 Tag `kind` values (the field is extensible via a
|
|
3
|
+
// community registry, so this rule is recommended: false / opt-in).
|
|
4
|
+
const REGISTERED_TAG_KINDS = new Set(['nav', 'badge', 'audience']);
|
|
5
|
+
/** Flags a present-but-unregistered OpenAPI 3.2 Tag Object `kind` value. */
|
|
6
|
+
export const oasTagKind = (tag, _options, context) => {
|
|
7
|
+
if (!isObject(tag) || typeof tag['kind'] !== 'string')
|
|
8
|
+
return [];
|
|
9
|
+
if (REGISTERED_TAG_KINDS.has(tag['kind']))
|
|
10
|
+
return [];
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
message: `Tag kind "${tag['kind']}" is not a registered value (${[...REGISTERED_TAG_KINDS].join(', ')})`,
|
|
14
|
+
path: [...context.path, 'kind'],
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates the OpenAPI 3.2 Tag Object `parent` hierarchy: every `parent` must
|
|
4
|
+
* name a tag that exists in the top-level `tags` list, a tag must not be its own
|
|
5
|
+
* parent, and the parent chain must not form a cycle.
|
|
6
|
+
*/
|
|
7
|
+
export declare const oasTagParentDefined: RulesetFunction;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates the OpenAPI 3.2 Tag Object `parent` hierarchy: every `parent` must
|
|
4
|
+
* name a tag that exists in the top-level `tags` list, a tag must not be its own
|
|
5
|
+
* parent, and the parent chain must not form a cycle.
|
|
6
|
+
*/
|
|
7
|
+
export const oasTagParentDefined = (tags, _options, context) => {
|
|
8
|
+
if (!Array.isArray(tags))
|
|
9
|
+
return [];
|
|
10
|
+
const byName = new Map();
|
|
11
|
+
for (const tag of tags) {
|
|
12
|
+
if (isObject(tag) && typeof tag['name'] === 'string')
|
|
13
|
+
byName.set(tag['name'], tag);
|
|
14
|
+
}
|
|
15
|
+
const results = [];
|
|
16
|
+
tags.forEach((tag, index) => {
|
|
17
|
+
if (!isObject(tag) || typeof tag['parent'] !== 'string')
|
|
18
|
+
return;
|
|
19
|
+
const parent = tag['parent'];
|
|
20
|
+
const name = typeof tag['name'] === 'string' ? tag['name'] : undefined;
|
|
21
|
+
if (!byName.has(parent)) {
|
|
22
|
+
results.push({
|
|
23
|
+
message: `Tag parent "${parent}" is not defined in the global tags`,
|
|
24
|
+
path: [...context.path, index, 'parent'],
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Walk the parent chain from this tag; a repeat means a cycle (incl. self-parent).
|
|
29
|
+
const visited = new Set(name ? [name] : []);
|
|
30
|
+
let current = parent;
|
|
31
|
+
while (current !== undefined) {
|
|
32
|
+
if (visited.has(current)) {
|
|
33
|
+
results.push({
|
|
34
|
+
message: `Tag "${name ?? '(unnamed)'}" has a circular parent reference via "${current}"`,
|
|
35
|
+
path: [...context.path, index, 'parent'],
|
|
36
|
+
});
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
visited.add(current);
|
|
40
|
+
const next = byName.get(current)?.['parent'];
|
|
41
|
+
current = typeof next === 'string' ? next : undefined;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return results;
|
|
45
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/** Flags duplicate global tag names. */
|
|
3
|
+
export const oasTagsUnique = (tags, _options, context) => {
|
|
4
|
+
if (!Array.isArray(tags))
|
|
5
|
+
return [];
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
const results = [];
|
|
8
|
+
tags.forEach((tag, index) => {
|
|
9
|
+
if (!isObject(tag) || typeof tag['name'] !== 'string')
|
|
10
|
+
return;
|
|
11
|
+
if (seen.has(tag['name'])) {
|
|
12
|
+
results.push({ message: `Duplicate tag name "${tag['name']}"`, path: [...context.path, index, 'name'] });
|
|
13
|
+
}
|
|
14
|
+
seen.add(tag['name']);
|
|
15
|
+
});
|
|
16
|
+
return results;
|
|
17
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Flags reusable `components/*` entries that nothing `$ref`s. Spectral's
|
|
4
|
+
* `oas3-unused-component` checks every reusable component type (not just
|
|
5
|
+
* schemas), so we do too — run on the unresolved document.
|
|
6
|
+
*/
|
|
7
|
+
export declare const oasUnusedComponent: RulesetFunction;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
// Reusable component types that are referenced via `$ref` (securitySchemes are
|
|
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`).
|
|
5
|
+
const REUSABLE_COMPONENT_TYPES = [
|
|
6
|
+
'schemas',
|
|
7
|
+
'responses',
|
|
8
|
+
'parameters',
|
|
9
|
+
'examples',
|
|
10
|
+
'requestBodies',
|
|
11
|
+
'headers',
|
|
12
|
+
'links',
|
|
13
|
+
'callbacks',
|
|
14
|
+
'pathItems',
|
|
15
|
+
];
|
|
16
|
+
/** Collects every `$ref` string anywhere in `node` into `into`. */
|
|
17
|
+
const collectRefs = (node, into) => {
|
|
18
|
+
if (Array.isArray(node)) {
|
|
19
|
+
for (const item of node)
|
|
20
|
+
collectRefs(item, into);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (isObject(node)) {
|
|
24
|
+
for (const [key, value] of Object.entries(node)) {
|
|
25
|
+
if (key === '$ref' && typeof value === 'string')
|
|
26
|
+
into.add(value);
|
|
27
|
+
else
|
|
28
|
+
collectRefs(value, into);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Flags reusable `components/*` entries that nothing `$ref`s. Spectral's
|
|
34
|
+
* `oas3-unused-component` checks every reusable component type (not just
|
|
35
|
+
* schemas), so we do too — run on the unresolved document.
|
|
36
|
+
*/
|
|
37
|
+
export const oasUnusedComponent = (components, _options, context) => {
|
|
38
|
+
if (!isObject(components))
|
|
39
|
+
return [];
|
|
40
|
+
const refs = new Set();
|
|
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
|
+
};
|
|
53
|
+
const results = [];
|
|
54
|
+
for (const type of REUSABLE_COMPONENT_TYPES) {
|
|
55
|
+
const group = components[type];
|
|
56
|
+
if (!isObject(group))
|
|
57
|
+
continue;
|
|
58
|
+
for (const key of Object.keys(group)) {
|
|
59
|
+
if (!isReferenced(`#/components/${type}/${key}`)) {
|
|
60
|
+
results.push({ message: 'Potentially unused component has been detected.', path: [...context.path, type, key] });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return results;
|
|
65
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/** Flags objects that mix `$ref` with sibling keys (which are ignored per spec). */
|
|
3
|
+
export const refSiblings = (input, _options, context) => {
|
|
4
|
+
if (!isObject(input) || !('$ref' in input))
|
|
5
|
+
return [];
|
|
6
|
+
const results = [];
|
|
7
|
+
for (const key of Object.keys(input)) {
|
|
8
|
+
if (key !== '$ref') {
|
|
9
|
+
results.push({ message: `$ref must not be placed next to "${key}"`, path: [...context.path, key] });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return results;
|
|
13
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type FunctionRegistry, type ResolvedExtend, type Ruleset, type RulesetDefinition } from '../../core/index.js';
|
|
2
|
+
export { oasFixers } from './fixers.js';
|
|
3
|
+
export { oas2, oas3, oas3_0, oas3_1, oas3_2, oasFormats } from './formats.js';
|
|
4
|
+
export { oasFunctions } from './functions/index.js';
|
|
5
|
+
export { oas } from './oas.js';
|
|
6
|
+
export { loadOasSchema, type OasVersion } from './schemas/index.js';
|
|
7
|
+
/** The built-in `@amritk/lint` functions plus the OpenAPI-specific ones, keyed by name. */
|
|
8
|
+
export declare const allFunctions: FunctionRegistry;
|
|
9
|
+
/**
|
|
10
|
+
* Resolves an `extends` reference to a ruleset definition. Extends the generic
|
|
11
|
+
* file/package resolution with the OpenAPI preset names:
|
|
12
|
+
* - `oas` / `loupe:oas` / `spectral:oas` → the built-in {@link oas} ruleset,
|
|
13
|
+
* - local file paths (relative to `basePath`, or absolute): `.yaml` / `.yml` / `.json` / `.js`,
|
|
14
|
+
* - npm package specifiers (resolved from `basePath`), including subpaths.
|
|
15
|
+
*/
|
|
16
|
+
export declare const resolveOpenApiRuleset: (name: string, basePath?: string) => ResolvedExtend;
|
|
17
|
+
/**
|
|
18
|
+
* Builds a runnable {@link Ruleset} for OpenAPI, layering the built-in and
|
|
19
|
+
* OpenAPI functions (plus any custom ones the definition declares), the OpenAPI
|
|
20
|
+
* `formats`, and `extends` resolution that understands the `oas` / `loupe:oas` /
|
|
21
|
+
* `spectral:oas` names. With no definition it defaults to `extends: [oas]`
|
|
22
|
+
* (recommended rules only). Feed the result to `@amritk/lint`'s core
|
|
23
|
+
* `lintWithResult` (with a `$ref` resolver for `resolved: true` rules).
|
|
24
|
+
*/
|
|
25
|
+
export declare const createOpenApiRuleset: (definition?: RulesetDefinition, basePath?: string) => Ruleset;
|