@amritk/lint 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -1
- package/dist/rules/openapi/fixers.d.ts +7 -0
- package/dist/rules/openapi/fixers.js +189 -0
- package/dist/rules/openapi/formats.d.ts +13 -0
- package/dist/rules/openapi/formats.js +23 -0
- package/dist/rules/openapi/functions/example-validation.d.ts +5 -0
- package/dist/rules/openapi/functions/example-validation.js +97 -0
- package/dist/rules/openapi/functions/helpers.d.ts +3 -0
- package/dist/rules/openapi/functions/helpers.js +5 -0
- package/dist/rules/openapi/functions/index.d.ts +24 -0
- package/dist/rules/openapi/functions/index.js +67 -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-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-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 +27 -0
- package/dist/rules/openapi/functions/oas-op-params.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-params.js +21 -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 +11 -0
- package/dist/rules/openapi/functions/oas-path-param.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-path-param.js +45 -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 +3 -0
- package/dist/rules/openapi/functions/oas-server-variables.js +23 -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 +52 -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 +492 -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 +6 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/** Validates OpenAPI v2 formData operations declare a compatible `consumes`. */
|
|
3
|
+
export const oasOpFormDataConsumeCheck = (operation, _options, context) => {
|
|
4
|
+
if (!isObject(operation))
|
|
5
|
+
return [];
|
|
6
|
+
const params = Array.isArray(operation['parameters']) ? operation['parameters'] : [];
|
|
7
|
+
const hasFormData = params.some((param) => isObject(param) && param['in'] === 'formData');
|
|
8
|
+
if (!hasFormData)
|
|
9
|
+
return [];
|
|
10
|
+
const consumes = Array.isArray(operation['consumes']) ? operation['consumes'] : [];
|
|
11
|
+
const ok = consumes.some((type) => type === 'application/x-www-form-urlencoded' || type === 'multipart/form-data');
|
|
12
|
+
if (!ok) {
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
message: 'Operations with formData parameters must consume application/x-www-form-urlencoded or multipart/form-data',
|
|
16
|
+
path: [...context.path, 'consumes'],
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
return [];
|
|
21
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/** Ensures `operationId` values are unique across the document. */
|
|
3
|
+
export const oasOpIdUnique = (paths, _options, context) => {
|
|
4
|
+
if (!isObject(paths))
|
|
5
|
+
return [];
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
const results = [];
|
|
8
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
9
|
+
if (!isObject(item))
|
|
10
|
+
continue;
|
|
11
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
12
|
+
if (!isObject(operation))
|
|
13
|
+
continue;
|
|
14
|
+
const id = operation['operationId'];
|
|
15
|
+
if (typeof id !== 'string')
|
|
16
|
+
continue;
|
|
17
|
+
if (seen.has(id)) {
|
|
18
|
+
results.push({
|
|
19
|
+
message: `operationId "${id}" must be unique`,
|
|
20
|
+
path: [...context.path, path, method, 'operationId'],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
seen.add(id);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/** Flags duplicate `name`+`in` parameter combinations on an operation. */
|
|
3
|
+
export const oasOpParams = (params, _options, context) => {
|
|
4
|
+
if (!Array.isArray(params))
|
|
5
|
+
return [];
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
const results = [];
|
|
8
|
+
params.forEach((param, index) => {
|
|
9
|
+
if (!isObject(param) || param['name'] === undefined || param['in'] === undefined)
|
|
10
|
+
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
|
+
});
|
|
17
|
+
}
|
|
18
|
+
seen.add(key);
|
|
19
|
+
});
|
|
20
|
+
return results;
|
|
21
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { HTTP_METHODS, isObject } 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 (HTTP_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,11 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/** Ensures every operation declares at least one 2xx or 3xx response. */
|
|
3
|
+
export const oasOpSuccessResponse = (responses) => {
|
|
4
|
+
if (!isObject(responses))
|
|
5
|
+
return [];
|
|
6
|
+
const hasSuccess = Object.keys(responses).some((code) => /^[23]\d\d$/.test(code) || code === 'default');
|
|
7
|
+
if (!hasSuccess) {
|
|
8
|
+
return [{ message: 'Operation must define at least one 2xx or 3xx response' }];
|
|
9
|
+
}
|
|
10
|
+
return [];
|
|
11
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { HTTP_METHODS, isObject } from './helpers.js';
|
|
2
|
+
/** Validates path templating: declared `{params}` must have matching path parameters. */
|
|
3
|
+
export const oasPathParam = (paths, _options, context) => {
|
|
4
|
+
if (!isObject(paths))
|
|
5
|
+
return [];
|
|
6
|
+
const results = [];
|
|
7
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
8
|
+
if (!isObject(item))
|
|
9
|
+
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)) {
|
|
14
|
+
results.push({
|
|
15
|
+
message: `Path "${path}" uses parameter "{${template}}" more than once`,
|
|
16
|
+
path: [...context.path, path],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
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']);
|
|
28
|
+
}
|
|
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
|
+
}
|
|
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],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return results;
|
|
45
|
+
};
|
|
@@ -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,23 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/** Validates server variables are declared and used. */
|
|
3
|
+
export const oasServerVariables = (server, _options, context) => {
|
|
4
|
+
if (!isObject(server) || typeof server['url'] !== 'string')
|
|
5
|
+
return [];
|
|
6
|
+
const templates = [...server['url'].matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
|
|
7
|
+
const variables = isObject(server['variables']) ? server['variables'] : {};
|
|
8
|
+
const results = [];
|
|
9
|
+
for (const template of templates) {
|
|
10
|
+
if (!(template in variables)) {
|
|
11
|
+
results.push({ message: `Server variable "${template}" is not defined`, path: [...context.path, 'variables'] });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
for (const name of Object.keys(variables)) {
|
|
15
|
+
if (!templates.includes(name)) {
|
|
16
|
+
results.push({
|
|
17
|
+
message: `Server variable "${name}" is not used in the URL`,
|
|
18
|
+
path: [...context.path, 'variables', name],
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return results;
|
|
23
|
+
};
|
|
@@ -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,52 @@
|
|
|
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
|
+
const REUSABLE_COMPONENT_TYPES = [
|
|
5
|
+
'schemas',
|
|
6
|
+
'responses',
|
|
7
|
+
'parameters',
|
|
8
|
+
'examples',
|
|
9
|
+
'requestBodies',
|
|
10
|
+
'headers',
|
|
11
|
+
'links',
|
|
12
|
+
'callbacks',
|
|
13
|
+
];
|
|
14
|
+
/** Collects every `$ref` string anywhere in `node` into `into`. */
|
|
15
|
+
const collectRefs = (node, into) => {
|
|
16
|
+
if (Array.isArray(node)) {
|
|
17
|
+
for (const item of node)
|
|
18
|
+
collectRefs(item, into);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (isObject(node)) {
|
|
22
|
+
for (const [key, value] of Object.entries(node)) {
|
|
23
|
+
if (key === '$ref' && typeof value === 'string')
|
|
24
|
+
into.add(value);
|
|
25
|
+
else
|
|
26
|
+
collectRefs(value, into);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Flags reusable `components/*` entries that nothing `$ref`s. Spectral's
|
|
32
|
+
* `oas3-unused-component` checks every reusable component type (not just
|
|
33
|
+
* schemas), so we do too — run on the unresolved document.
|
|
34
|
+
*/
|
|
35
|
+
export const oasUnusedComponent = (components, _options, context) => {
|
|
36
|
+
if (!isObject(components))
|
|
37
|
+
return [];
|
|
38
|
+
const refs = new Set();
|
|
39
|
+
collectRefs(context.document.data, refs);
|
|
40
|
+
const results = [];
|
|
41
|
+
for (const type of REUSABLE_COMPONENT_TYPES) {
|
|
42
|
+
const group = components[type];
|
|
43
|
+
if (!isObject(group))
|
|
44
|
+
continue;
|
|
45
|
+
for (const key of Object.keys(group)) {
|
|
46
|
+
if (!refs.has(`#/components/${type}/${key}`)) {
|
|
47
|
+
results.push({ message: 'Potentially unused component has been detected.', path: [...context.path, type, key] });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return results;
|
|
52
|
+
};
|
|
@@ -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;
|