@amritk/lint 0.4.8 → 0.5.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/AI.md +73 -4
- package/README.md +65 -14
- package/dist/core/filter-expression.js +17 -4
- package/dist/core/filter.js +82 -35
- package/dist/core/glob.js +25 -31
- package/dist/core/jsonpath.d.ts +9 -1
- package/dist/core/jsonpath.js +74 -16
- package/dist/core/lint.d.ts +8 -1
- package/dist/core/lint.js +4 -18
- package/dist/core/order.d.ts +30 -0
- package/dist/core/order.js +36 -0
- package/dist/core/pointers.d.ts +3 -2
- package/dist/core/pointers.js +27 -18
- package/dist/core/ruleset.js +62 -20
- package/dist/core/runner.js +16 -23
- package/dist/core/severity.d.ts +11 -0
- package/dist/core/severity.js +13 -0
- package/dist/fix/apply.js +6 -1
- package/dist/fix/plugin.js +2 -1
- package/dist/functions/or.js +1 -1
- package/dist/functions/ref-index.d.ts +15 -0
- package/dist/functions/ref-index.js +33 -0
- package/dist/functions/schema.d.ts +15 -0
- package/dist/functions/schema.js +2 -0
- package/dist/functions/typed-enum.js +2 -1
- package/dist/functions/unreferenced-reusable-object.js +3 -20
- package/dist/functions/xor.js +1 -1
- package/dist/index.d.ts +16 -16
- package/dist/index.js +24 -93
- package/dist/rules/asyncapi/asyncapi.d.ts +3 -0
- package/dist/rules/asyncapi/asyncapi.js +563 -0
- package/dist/rules/asyncapi/formats.d.ts +23 -0
- package/dist/rules/asyncapi/formats.js +49 -0
- package/dist/rules/asyncapi/functions/aas-server-variables.d.ts +8 -0
- package/dist/rules/asyncapi/functions/aas-server-variables.js +5 -0
- package/dist/rules/asyncapi/functions/aas-tags-unique.d.ts +2 -0
- package/dist/rules/asyncapi/functions/aas-tags-unique.js +5 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.d.ts +7 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.js +46 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-servers.d.ts +27 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-servers.js +61 -0
- package/dist/rules/asyncapi/functions/asyncapi-document-schema.d.ts +17 -0
- package/dist/rules/asyncapi/functions/asyncapi-document-schema.js +12 -0
- package/dist/rules/asyncapi/functions/asyncapi-headers-object.d.ts +23 -0
- package/dist/rules/asyncapi/functions/asyncapi-headers-object.js +25 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-examples.d.ts +8 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-examples.js +42 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.d.ts +3 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.js +30 -0
- package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.d.ts +3 -0
- package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.js +30 -0
- package/dist/rules/asyncapi/functions/asyncapi-payload.d.ts +14 -0
- package/dist/rules/asyncapi/functions/asyncapi-payload.js +32 -0
- package/dist/rules/asyncapi/functions/asyncapi-schema-validation.d.ts +11 -0
- package/dist/rules/asyncapi/functions/asyncapi-schema-validation.js +17 -0
- package/dist/rules/asyncapi/functions/asyncapi-security.d.ts +22 -0
- package/dist/rules/asyncapi/functions/asyncapi-security.js +78 -0
- package/dist/rules/asyncapi/functions/helpers.d.ts +51 -0
- package/dist/rules/asyncapi/functions/helpers.js +91 -0
- package/dist/rules/asyncapi/functions/index.d.ts +15 -0
- package/dist/rules/asyncapi/functions/index.js +53 -0
- package/dist/rules/asyncapi/functions/pointer.d.ts +11 -0
- package/dist/rules/asyncapi/functions/pointer.js +11 -0
- package/dist/rules/asyncapi/functions/schema-format.d.ts +4 -0
- package/dist/rules/asyncapi/functions/schema-format.js +7 -0
- package/dist/rules/asyncapi/index.d.ts +33 -0
- package/dist/rules/asyncapi/index.js +60 -0
- package/dist/rules/asyncapi/schemas/aas20.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas20.js +4 -0
- package/dist/rules/asyncapi/schemas/aas21.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas21.js +4 -0
- package/dist/rules/asyncapi/schemas/aas22.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas22.js +4 -0
- package/dist/rules/asyncapi/schemas/aas23.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas23.js +4 -0
- package/dist/rules/asyncapi/schemas/aas24.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas24.js +4 -0
- package/dist/rules/asyncapi/schemas/aas25.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas25.js +4 -0
- package/dist/rules/asyncapi/schemas/aas26.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas26.js +4 -0
- package/dist/rules/asyncapi/schemas/aas30.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas30.js +4 -0
- package/dist/rules/asyncapi/schemas/index.d.ts +20 -0
- package/dist/rules/asyncapi/schemas/index.js +45 -0
- package/dist/rules/openapi/fixers.d.ts +6 -2
- package/dist/rules/openapi/fixers.js +2 -0
- package/dist/rules/openapi/functions/oas-discriminator.js +1 -1
- package/dist/rules/openapi/functions/oas-mutually-exclusive.js +1 -1
- package/dist/rules/openapi/functions/oas-op-security-defined.js +1 -1
- package/dist/rules/openapi/functions/oas-server-variables.d.ts +6 -6
- package/dist/rules/openapi/functions/oas-server-variables.js +2 -45
- package/dist/rules/openapi/functions/oas-tags-unique.d.ts +2 -3
- package/dist/rules/openapi/functions/oas-tags-unique.js +2 -16
- package/dist/rules/openapi/functions/oas-unused-component.js +3 -25
- package/dist/rules/openapi/index.d.ts +9 -2
- package/dist/rules/openapi/index.js +15 -70
- package/dist/rules/openapi/schemas/index.js +5 -1
- package/dist/rules/shared/helpers.d.ts +2 -0
- package/dist/rules/shared/helpers.js +4 -0
- package/dist/rules/shared/server-variables.d.ts +27 -0
- package/dist/rules/shared/server-variables.js +55 -0
- package/dist/rules/shared/tags-unique.d.ts +7 -0
- package/dist/rules/shared/tags-unique.js +19 -0
- package/dist/ruleset-files.d.ts +47 -0
- package/dist/ruleset-files.js +92 -0
- package/package.json +8 -4
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The AsyncAPI versions with a bundled structural meta-schema. */
|
|
2
|
+
export type AsyncApiVersion = '2.0' | '2.1' | '2.2' | '2.3' | '2.4' | '2.5' | '2.6' | '3.0';
|
|
3
|
+
/** The bundled versions, oldest first. `asyncapi-latest-version` reports the last one. */
|
|
4
|
+
export declare const ASYNCAPI_VERSIONS: AsyncApiVersion[];
|
|
5
|
+
/** The newest AsyncAPI release this package ships a meta-schema for, as a full `x.y.z` version. */
|
|
6
|
+
export declare const LATEST_ASYNCAPI_VERSION = "3.0.0";
|
|
7
|
+
/**
|
|
8
|
+
* Parses (and memoizes) the official structural meta-schema for one AsyncAPI
|
|
9
|
+
* version. The returned object is stable across calls, so downstream validator
|
|
10
|
+
* caches (keyed by schema identity) stay warm.
|
|
11
|
+
*/
|
|
12
|
+
export declare const loadAsyncApiSchema: (version: AsyncApiVersion) => object;
|
|
13
|
+
/**
|
|
14
|
+
* Maps a document's `asyncapi` string to the bundled meta-schema version, or
|
|
15
|
+
* `undefined` when no bundled schema covers it. Patch releases share their
|
|
16
|
+
* minor's schema (`2.6.4` → `2.6`), which is how the spec publishes them; an
|
|
17
|
+
* unbundled minor such as a future `2.7.0` returns `undefined` so callers report
|
|
18
|
+
* nothing rather than validating against the wrong version.
|
|
19
|
+
*/
|
|
20
|
+
export declare const asyncApiSchemaVersion: (declared: unknown) => AsyncApiVersion | undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { aas20Json } from "./aas20.js";
|
|
2
|
+
import { aas21Json } from "./aas21.js";
|
|
3
|
+
import { aas22Json } from "./aas22.js";
|
|
4
|
+
import { aas23Json } from "./aas23.js";
|
|
5
|
+
import { aas24Json } from "./aas24.js";
|
|
6
|
+
import { aas25Json } from "./aas25.js";
|
|
7
|
+
import { aas26Json } from "./aas26.js";
|
|
8
|
+
import { aas30Json } from "./aas30.js";
|
|
9
|
+
const SCHEMA_TEXT = {
|
|
10
|
+
"2.0": aas20Json,
|
|
11
|
+
"2.1": aas21Json,
|
|
12
|
+
"2.2": aas22Json,
|
|
13
|
+
"2.3": aas23Json,
|
|
14
|
+
"2.4": aas24Json,
|
|
15
|
+
"2.5": aas25Json,
|
|
16
|
+
"2.6": aas26Json,
|
|
17
|
+
"3.0": aas30Json
|
|
18
|
+
};
|
|
19
|
+
const ASYNCAPI_VERSIONS = Object.keys(SCHEMA_TEXT);
|
|
20
|
+
const LATEST_ASYNCAPI_VERSION = "3.0.0";
|
|
21
|
+
const cache = /* @__PURE__ */ new Map();
|
|
22
|
+
const loadAsyncApiSchema = (version) => {
|
|
23
|
+
let schema = cache.get(version);
|
|
24
|
+
if (!schema) {
|
|
25
|
+
const text = Object.hasOwn(SCHEMA_TEXT, version) ? SCHEMA_TEXT[version] : void 0;
|
|
26
|
+
if (text === void 0) {
|
|
27
|
+
throw new Error(`Unknown AsyncAPI version "${version}". Known versions are: ${Object.keys(SCHEMA_TEXT).join(", ")}`);
|
|
28
|
+
}
|
|
29
|
+
schema = JSON.parse(text);
|
|
30
|
+
cache.set(version, schema);
|
|
31
|
+
}
|
|
32
|
+
return schema;
|
|
33
|
+
};
|
|
34
|
+
const asyncApiSchemaVersion = (declared) => {
|
|
35
|
+
if (typeof declared !== "string")
|
|
36
|
+
return void 0;
|
|
37
|
+
const minor = /^(\d+\.\d+)(?:\.|$)/.exec(declared)?.[1];
|
|
38
|
+
return minor !== void 0 && Object.hasOwn(SCHEMA_TEXT, minor) ? minor : void 0;
|
|
39
|
+
};
|
|
40
|
+
export {
|
|
41
|
+
ASYNCAPI_VERSIONS,
|
|
42
|
+
LATEST_ASYNCAPI_VERSION,
|
|
43
|
+
asyncApiSchemaVersion,
|
|
44
|
+
loadAsyncApiSchema
|
|
45
|
+
};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { FixerRegistry } from '../../fix/index.js';
|
|
2
2
|
/**
|
|
3
|
-
* Auto-fixers for the mechanically-repairable OpenAPI rules, keyed by rule
|
|
4
|
-
*
|
|
3
|
+
* Auto-fixers for the mechanically-repairable OpenAPI rules, keyed by rule code.
|
|
4
|
+
*
|
|
5
|
+
* Pass these to `@amritk/lint`'s `fixDocument` as its `fixers`, together with a
|
|
6
|
+
* ruleset built by `createOpenApiRuleset` — the OpenAPI rules need that
|
|
7
|
+
* preset's functions and format detectors, so handing `fixDocument` the `oas`
|
|
8
|
+
* definition as plain data produces no findings and therefore no fixes. Or wrap
|
|
5
9
|
* them with `createFixPlugin` for a lower-level plugin.
|
|
6
10
|
*/
|
|
7
11
|
export declare const oasFixers: FixerRegistry;
|
|
@@ -6,7 +6,7 @@ const oasDiscriminator = (schema, _options, context) => {
|
|
|
6
6
|
const required = Array.isArray(schema["required"]) ? schema["required"] : [];
|
|
7
7
|
const properties = isObject(schema["properties"]) ? schema["properties"] : {};
|
|
8
8
|
const results = [];
|
|
9
|
-
if (!(property
|
|
9
|
+
if (!Object.hasOwn(properties, property)) {
|
|
10
10
|
results.push({
|
|
11
11
|
message: `Discriminator "${property}" must be defined in properties`,
|
|
12
12
|
path: [...context.path, "discriminator"]
|
|
@@ -2,7 +2,7 @@ import { isObject } from "./helpers.js";
|
|
|
2
2
|
const oasMutuallyExclusive = (input, options, context) => {
|
|
3
3
|
if (!isObject(input))
|
|
4
4
|
return [];
|
|
5
|
-
const present = (options?.properties ?? []).filter((property) => input[property] !== void 0);
|
|
5
|
+
const present = (options?.properties ?? []).filter((property) => Object.hasOwn(input, property) && input[property] !== void 0);
|
|
6
6
|
if (present.length <= 1)
|
|
7
7
|
return [];
|
|
8
8
|
const [anchor, ...conflicts] = present;
|
|
@@ -2,7 +2,7 @@ import { isObject, OPERATION_METHODS } from "./helpers.js";
|
|
|
2
2
|
const getIn = (root, path) => {
|
|
3
3
|
let current = root;
|
|
4
4
|
for (const key of path) {
|
|
5
|
-
if (!isObject(current))
|
|
5
|
+
if (!isObject(current) || !Object.hasOwn(current, key))
|
|
6
6
|
return void 0;
|
|
7
7
|
current = current[key];
|
|
8
8
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IServerVariablesOptions } from '../../shared/server-variables.js';
|
|
2
2
|
/**
|
|
3
|
-
* Validates a Server Object's `variables
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* and a `default` that is listed in that `enum`.
|
|
3
|
+
* Validates a Server Object's `variables`. The OpenAPI and AsyncAPI Server
|
|
4
|
+
* Objects are the same shape here, so the implementation is shared — see
|
|
5
|
+
* {@link serverVariables}.
|
|
7
6
|
*/
|
|
8
|
-
export declare const oasServerVariables: RulesetFunction
|
|
7
|
+
export declare const oasServerVariables: import("../../../core/types.js").RulesetFunction<unknown, IServerVariablesOptions | undefined>;
|
|
8
|
+
export type { IServerVariablesOptions };
|
|
@@ -1,48 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const oasServerVariables =
|
|
3
|
-
if (!isObject(server) || typeof server["url"] !== "string")
|
|
4
|
-
return [];
|
|
5
|
-
const templates = [...server["url"].matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
|
|
6
|
-
const variables = isObject(server["variables"]) ? server["variables"] : {};
|
|
7
|
-
const results = [];
|
|
8
|
-
for (const template of templates) {
|
|
9
|
-
if (!(template in variables)) {
|
|
10
|
-
results.push({ message: `Server variable "${template}" is not defined`, path: [...context.path, "variables"] });
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
for (const [name, variable] of Object.entries(variables)) {
|
|
14
|
-
if (!templates.includes(name)) {
|
|
15
|
-
results.push({
|
|
16
|
-
message: `Server variable "${name}" is not used in the URL`,
|
|
17
|
-
path: [...context.path, "variables", name]
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
if (!isObject(variable))
|
|
21
|
-
continue;
|
|
22
|
-
const hasDefault = variable["default"] !== void 0;
|
|
23
|
-
if (!hasDefault) {
|
|
24
|
-
results.push({
|
|
25
|
-
message: `Server variable "${name}" has a missing default`,
|
|
26
|
-
path: [...context.path, "variables", name]
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
if ("enum" in variable) {
|
|
30
|
-
const enumValues = Array.isArray(variable["enum"]) ? variable["enum"] : [];
|
|
31
|
-
if (enumValues.length === 0) {
|
|
32
|
-
results.push({
|
|
33
|
-
message: `Server variable "${name}" has an empty enum`,
|
|
34
|
-
path: [...context.path, "variables", name, "enum"]
|
|
35
|
-
});
|
|
36
|
-
} else if (hasDefault && !enumValues.includes(variable["default"])) {
|
|
37
|
-
results.push({
|
|
38
|
-
message: `Server variable "${name}" has a default not listed in the enum`,
|
|
39
|
-
path: [...context.path, "variables", name, "default"]
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return results;
|
|
45
|
-
};
|
|
1
|
+
import { serverVariables } from "../../shared/server-variables.js";
|
|
2
|
+
const oasServerVariables = serverVariables;
|
|
46
3
|
export {
|
|
47
4
|
oasServerVariables
|
|
48
5
|
};
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export declare const oasTagsUnique: RulesetFunction;
|
|
1
|
+
/** Flags duplicate global tag names. Shared with AsyncAPI — see {@link tagsUnique}. */
|
|
2
|
+
export declare const oasTagsUnique: import("../../../core/types.js").RulesetFunction;
|
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const oasTagsUnique =
|
|
3
|
-
if (!Array.isArray(tags))
|
|
4
|
-
return [];
|
|
5
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6
|
-
const results = [];
|
|
7
|
-
tags.forEach((tag, index) => {
|
|
8
|
-
if (!isObject(tag) || typeof tag["name"] !== "string")
|
|
9
|
-
return;
|
|
10
|
-
if (seen.has(tag["name"])) {
|
|
11
|
-
results.push({ message: `Duplicate tag name "${tag["name"]}"`, path: [...context.path, index, "name"] });
|
|
12
|
-
}
|
|
13
|
-
seen.add(tag["name"]);
|
|
14
|
-
});
|
|
15
|
-
return results;
|
|
16
|
-
};
|
|
1
|
+
import { tagsUnique } from "../../shared/tags-unique.js";
|
|
2
|
+
const oasTagsUnique = tagsUnique;
|
|
17
3
|
export {
|
|
18
4
|
oasTagsUnique
|
|
19
5
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectReferencedPointers } from "../../../functions/ref-index.js";
|
|
1
2
|
import { isObject } from "./helpers.js";
|
|
2
3
|
const REUSABLE_COMPONENT_TYPES = [
|
|
3
4
|
"schemas",
|
|
@@ -10,40 +11,17 @@ const REUSABLE_COMPONENT_TYPES = [
|
|
|
10
11
|
"callbacks",
|
|
11
12
|
"pathItems"
|
|
12
13
|
];
|
|
13
|
-
const collectRefs = (node, into) => {
|
|
14
|
-
if (Array.isArray(node)) {
|
|
15
|
-
for (const item of node)
|
|
16
|
-
collectRefs(item, into);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (isObject(node)) {
|
|
20
|
-
for (const [key, value] of Object.entries(node)) {
|
|
21
|
-
if (key === "$ref" && typeof value === "string")
|
|
22
|
-
into.add(value);
|
|
23
|
-
else
|
|
24
|
-
collectRefs(value, into);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
14
|
const oasUnusedComponent = (components, _options, context) => {
|
|
29
15
|
if (!isObject(components))
|
|
30
16
|
return [];
|
|
31
|
-
const
|
|
32
|
-
collectRefs(context.document.data, refs);
|
|
33
|
-
const isReferenced = (base) => {
|
|
34
|
-
for (const ref of refs) {
|
|
35
|
-
if (ref === base || ref.startsWith(`${base}/`))
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
};
|
|
17
|
+
const referenced = collectReferencedPointers(context.document.data);
|
|
40
18
|
const results = [];
|
|
41
19
|
for (const type of REUSABLE_COMPONENT_TYPES) {
|
|
42
20
|
const group = components[type];
|
|
43
21
|
if (!isObject(group))
|
|
44
22
|
continue;
|
|
45
23
|
for (const key of Object.keys(group)) {
|
|
46
|
-
if (!
|
|
24
|
+
if (!referenced.has(`#/components/${type}/${key}`)) {
|
|
47
25
|
results.push({ message: "Potentially unused component has been detected.", path: [...context.path, type, key] });
|
|
48
26
|
}
|
|
49
27
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ResolvedExtend, type Ruleset } from '../../core/index.js';
|
|
2
2
|
import type { FunctionRegistry, RulesetDefinition } from '../../core/types.js';
|
|
3
|
+
import { type IRulesetTrustOptions } from '../../ruleset-files.js';
|
|
4
|
+
export type { IRulesetTrustOptions } from '../../ruleset-files.js';
|
|
3
5
|
export { oasFixers } from './fixers.js';
|
|
4
6
|
export { oas2, oas3, oas3_0, oas3_1, oas3_2, oasFormats } from './formats.js';
|
|
5
7
|
export { oasFunctions } from './functions/index.js';
|
|
@@ -13,8 +15,13 @@ export declare const allFunctions: FunctionRegistry;
|
|
|
13
15
|
* - `oas` / `loupe:oas` / `spectral:oas` → the built-in {@link oas} ruleset,
|
|
14
16
|
* - local file paths (relative to `basePath`, or absolute): `.yaml` / `.yml` / `.json` / `.js`,
|
|
15
17
|
* - npm package specifiers (resolved from `basePath`), including subpaths.
|
|
18
|
+
*
|
|
19
|
+
* As with the core `resolveNamedRuleset`, `basePath` is where resolution starts,
|
|
20
|
+
* not a boundary: an absolute or `../`-escaping path is followed and a `.js`
|
|
21
|
+
* target is `require`d. Pass `restrictTo` to confine resolution to one directory
|
|
22
|
+
* tree when the ruleset is not fully trusted.
|
|
16
23
|
*/
|
|
17
|
-
export declare const resolveOpenApiRuleset: (name: string, basePath?: string) => ResolvedExtend;
|
|
24
|
+
export declare const resolveOpenApiRuleset: (name: string, basePath?: string, options?: IRulesetTrustOptions) => ResolvedExtend;
|
|
18
25
|
/**
|
|
19
26
|
* Builds a runnable {@link Ruleset} for OpenAPI, layering the built-in and
|
|
20
27
|
* OpenAPI functions (plus any custom ones the definition declares), the OpenAPI
|
|
@@ -23,4 +30,4 @@ export declare const resolveOpenApiRuleset: (name: string, basePath?: string) =>
|
|
|
23
30
|
* (recommended rules only). Feed the result to `@amritk/lint`'s core
|
|
24
31
|
* `lintWithResult` (with a `$ref` resolver for `resolved: true` rules).
|
|
25
32
|
*/
|
|
26
|
-
export declare const createOpenApiRuleset: (definition?: RulesetDefinition, basePath?: string) => Ruleset;
|
|
33
|
+
export declare const createOpenApiRuleset: (definition?: RulesetDefinition, basePath?: string, options?: IRulesetTrustOptions) => Ruleset;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { dirname, isAbsolute, resolve as resolvePath } from "node:path";
|
|
4
1
|
import { createRuleset as createCoreRuleset } from "../../core/index.js";
|
|
5
2
|
import { builtinFunctions } from "../../functions/index.js";
|
|
6
|
-
import {
|
|
3
|
+
import { collectCustomFunctions, resolveRulesetFile } from "../../ruleset-files.js";
|
|
7
4
|
import { oasFormats } from "./formats.js";
|
|
8
5
|
import { oasFunctions } from "./functions/index.js";
|
|
9
6
|
import { oas } from "./oas.js";
|
|
@@ -13,82 +10,30 @@ import { oasFunctions as oasFunctions2 } from "./functions/index.js";
|
|
|
13
10
|
import { oas as oas4 } from "./oas.js";
|
|
14
11
|
import { loadOasSchema } from "./schemas/index.js";
|
|
15
12
|
const allFunctions = { ...builtinFunctions, ...oasFunctions };
|
|
16
|
-
const require2 = createRequire(import.meta.url);
|
|
17
13
|
const OAS_RULESET_NAMES = /* @__PURE__ */ new Set(["oas", "loupe:oas", "spectral:oas"]);
|
|
18
|
-
const
|
|
19
|
-
if (/\.(ya?ml|json)$/i.test(file)) {
|
|
20
|
-
return parseWithPointers(readFileSync(file, "utf8")).data;
|
|
21
|
-
}
|
|
22
|
-
const module = require2(file);
|
|
23
|
-
return module.default ?? module;
|
|
24
|
-
};
|
|
25
|
-
const resolveOpenApiRuleset = (name, basePath = process.cwd()) => {
|
|
14
|
+
const resolveOpenApiRuleset = (name, basePath = process.cwd(), options = {}) => {
|
|
26
15
|
if (OAS_RULESET_NAMES.has(name))
|
|
27
16
|
return { definition: oas, basePath };
|
|
28
|
-
|
|
29
|
-
const file2 = resolvePath(basePath, name);
|
|
30
|
-
return { definition: loadRulesetFile(file2), basePath: dirname(file2) };
|
|
31
|
-
}
|
|
32
|
-
let file;
|
|
33
|
-
try {
|
|
34
|
-
file = require2.resolve(name, { paths: [basePath] });
|
|
35
|
-
} catch {
|
|
36
|
-
throw new Error(`Cannot resolve extended ruleset "${name}" from ${basePath}`);
|
|
37
|
-
}
|
|
38
|
-
return { definition: loadRulesetFile(file), basePath: dirname(file) };
|
|
39
|
-
};
|
|
40
|
-
const loadFunctionByName = (basePath, dir, name) => {
|
|
41
|
-
const baseFile = resolvePath(basePath, dir, name);
|
|
42
|
-
for (const candidate of [baseFile, `${baseFile}.js`, `${baseFile}.cjs`, `${baseFile}.mjs`]) {
|
|
43
|
-
try {
|
|
44
|
-
const resolvedFile = require2.resolve(candidate);
|
|
45
|
-
const module = require2(resolvedFile);
|
|
46
|
-
const fn = module.default ?? module;
|
|
47
|
-
if (typeof fn !== "function")
|
|
48
|
-
throw new Error(`"${name}" did not export a function`);
|
|
49
|
-
return fn;
|
|
50
|
-
} catch (error) {
|
|
51
|
-
if (error.code !== "MODULE_NOT_FOUND")
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
throw new Error(`Cannot resolve custom function "${name}" from ${resolvePath(basePath, dir)}`);
|
|
17
|
+
return resolveRulesetFile(name, basePath, options);
|
|
56
18
|
};
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (definition.extends) {
|
|
62
|
-
const entries = Array.isArray(definition.extends) ? definition.extends : [definition.extends];
|
|
63
|
-
for (const entry of entries) {
|
|
64
|
-
const target = Array.isArray(entry) ? entry[0] : entry;
|
|
65
|
-
if (typeof target === "string") {
|
|
66
|
-
if (OAS_RULESET_NAMES.has(target))
|
|
67
|
-
continue;
|
|
68
|
-
const resolved = resolveOpenApiRuleset(target, basePath);
|
|
69
|
-
collectCustomFunctions(resolved.definition, resolved.basePath, into, seen);
|
|
70
|
-
} else {
|
|
71
|
-
collectCustomFunctions(target, basePath, into, seen);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (Array.isArray(definition.functions)) {
|
|
76
|
-
const dir = definition.functionsDir ?? "functions";
|
|
77
|
-
for (const name of definition.functions)
|
|
78
|
-
into[name] = loadFunctionByName(basePath, dir, name);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
const createOpenApiRuleset = (definition, basePath) => {
|
|
82
|
-
const resolved = definition ?? { extends: [oas] };
|
|
19
|
+
const createOpenApiRuleset = (definition, basePath, options = {}) => {
|
|
20
|
+
const definitionOrDefault = definition ?? { extends: [oas] };
|
|
21
|
+
const trust = options.restrictTo !== void 0 ? { restrictTo: options.restrictTo } : {};
|
|
22
|
+
const resolve = (name, from) => resolveOpenApiRuleset(name, from, trust);
|
|
83
23
|
let functions = allFunctions;
|
|
84
24
|
const custom = {};
|
|
85
|
-
collectCustomFunctions(
|
|
25
|
+
collectCustomFunctions(definitionOrDefault, basePath ?? process.cwd(), custom, /* @__PURE__ */ new Set(), {
|
|
26
|
+
...trust,
|
|
27
|
+
// A built-in preset name has no directory of its own, so there is nothing to
|
|
28
|
+
// walk for custom functions.
|
|
29
|
+
resolveExtend: (name, from) => OAS_RULESET_NAMES.has(name) ? void 0 : resolve(name, from)
|
|
30
|
+
});
|
|
86
31
|
if (Object.keys(custom).length > 0)
|
|
87
32
|
functions = { ...allFunctions, ...custom };
|
|
88
|
-
return createCoreRuleset(
|
|
33
|
+
return createCoreRuleset(definitionOrDefault, {
|
|
89
34
|
functions,
|
|
90
35
|
formats: oasFormats,
|
|
91
|
-
resolve
|
|
36
|
+
resolve,
|
|
92
37
|
...basePath !== void 0 ? { basePath } : {}
|
|
93
38
|
});
|
|
94
39
|
};
|
|
@@ -12,7 +12,11 @@ const cache = /* @__PURE__ */ new Map();
|
|
|
12
12
|
const loadOasSchema = (version) => {
|
|
13
13
|
let schema = cache.get(version);
|
|
14
14
|
if (!schema) {
|
|
15
|
-
|
|
15
|
+
const text = Object.hasOwn(SCHEMA_TEXT, version) ? SCHEMA_TEXT[version] : void 0;
|
|
16
|
+
if (text === void 0) {
|
|
17
|
+
throw new Error(`Unknown OpenAPI version "${version}". Known versions are: ${Object.keys(SCHEMA_TEXT).join(", ")}`);
|
|
18
|
+
}
|
|
19
|
+
schema = JSON.parse(text);
|
|
16
20
|
cache.set(version, schema);
|
|
17
21
|
}
|
|
18
22
|
return schema;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates a Server Object's `variables`, mirroring Spectral's `serverVariables`:
|
|
4
|
+
* every `{template}` in the address must have a matching variable and vice
|
|
5
|
+
* versa, and each defined variable must have a `default`, a non-empty `enum`
|
|
6
|
+
* when present, and a `default` that is listed in that `enum`.
|
|
7
|
+
*
|
|
8
|
+
* All three specs model the Server Object the same way — a templated address
|
|
9
|
+
* plus a `variables` map — differing only in which field carries the address
|
|
10
|
+
* (see {@link TEMPLATED_FIELDS}), so they run this one implementation. See
|
|
11
|
+
* `oasServerVariables` and `aasServerVariables`.
|
|
12
|
+
*/
|
|
13
|
+
/** Options for {@link serverVariables}: which fields of the Server Object carry the address. */
|
|
14
|
+
export type IServerVariablesOptions = {
|
|
15
|
+
/**
|
|
16
|
+
* The address fields to read templates from. Defaults to `['url']`, which is
|
|
17
|
+
* where OpenAPI and AsyncAPI 2.x put the whole address; AsyncAPI 3.0 split it
|
|
18
|
+
* into `host` and `pathname` and passes those instead.
|
|
19
|
+
*
|
|
20
|
+
* Not a union of all three: OpenAPI's `oas3-server-variables` runs under a
|
|
21
|
+
* recursive `$..links[*].server` given, so reading `host`/`pathname` there
|
|
22
|
+
* turned any object with those keys — an example payload, say — into an
|
|
23
|
+
* error-severity finding about undefined server variables.
|
|
24
|
+
*/
|
|
25
|
+
addressFields?: readonly string[];
|
|
26
|
+
};
|
|
27
|
+
export declare const serverVariables: RulesetFunction<unknown, IServerVariablesOptions | undefined>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { isObject } from "./helpers.js";
|
|
2
|
+
const DEFAULT_ADDRESS_FIELDS = ["url"];
|
|
3
|
+
const addressNoun = (fields) => fields.length === 1 && fields[0] === "url" ? "URL" : "address";
|
|
4
|
+
const serverVariables = (server, options, context) => {
|
|
5
|
+
if (!isObject(server))
|
|
6
|
+
return [];
|
|
7
|
+
const fields = options?.addressFields ?? DEFAULT_ADDRESS_FIELDS;
|
|
8
|
+
const templated = fields.map((field) => Object.hasOwn(server, field) ? server[field] : void 0).filter((value) => typeof value === "string");
|
|
9
|
+
if (templated.length === 0)
|
|
10
|
+
return [];
|
|
11
|
+
const templates = templated.flatMap((value) => [...value.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]));
|
|
12
|
+
const templateNames = new Set(templates);
|
|
13
|
+
const variables = isObject(server["variables"]) ? server["variables"] : {};
|
|
14
|
+
const results = [];
|
|
15
|
+
for (const template of templateNames) {
|
|
16
|
+
if (!Object.hasOwn(variables, template)) {
|
|
17
|
+
results.push({ message: `Server variable "${template}" is not defined`, path: [...context.path, "variables"] });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
for (const [name, variable] of Object.entries(variables)) {
|
|
21
|
+
if (!templateNames.has(name)) {
|
|
22
|
+
results.push({
|
|
23
|
+
message: `Server variable "${name}" is not used in the ${addressNoun(fields)}`,
|
|
24
|
+
path: [...context.path, "variables", name]
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (!isObject(variable))
|
|
28
|
+
continue;
|
|
29
|
+
const hasDefault = variable["default"] !== void 0;
|
|
30
|
+
if (!hasDefault) {
|
|
31
|
+
results.push({
|
|
32
|
+
message: `Server variable "${name}" has a missing default`,
|
|
33
|
+
path: [...context.path, "variables", name]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if ("enum" in variable) {
|
|
37
|
+
const enumValues = Array.isArray(variable["enum"]) ? variable["enum"] : [];
|
|
38
|
+
if (enumValues.length === 0) {
|
|
39
|
+
results.push({
|
|
40
|
+
message: `Server variable "${name}" has an empty enum`,
|
|
41
|
+
path: [...context.path, "variables", name, "enum"]
|
|
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
|
+
};
|
|
53
|
+
export {
|
|
54
|
+
serverVariables
|
|
55
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Flags duplicate tag names within one tags array. Both specs carry Tag Objects
|
|
4
|
+
* with a `name`, and both require those names to be unique within the array they
|
|
5
|
+
* sit in — see `oasTagsUnique` and `aasTagsUnique`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const tagsUnique: RulesetFunction;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isObject } from "./helpers.js";
|
|
2
|
+
const tagsUnique = (tags, _options, context) => {
|
|
3
|
+
if (!Array.isArray(tags))
|
|
4
|
+
return [];
|
|
5
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6
|
+
const results = [];
|
|
7
|
+
tags.forEach((tag, index) => {
|
|
8
|
+
if (!isObject(tag) || typeof tag["name"] !== "string")
|
|
9
|
+
return;
|
|
10
|
+
if (seen.has(tag["name"])) {
|
|
11
|
+
results.push({ message: `Duplicate tag name "${tag["name"]}"`, path: [...context.path, index, "name"] });
|
|
12
|
+
}
|
|
13
|
+
seen.add(tag["name"]);
|
|
14
|
+
});
|
|
15
|
+
return results;
|
|
16
|
+
};
|
|
17
|
+
export {
|
|
18
|
+
tagsUnique
|
|
19
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ResolvedExtend } from './core/index.js';
|
|
2
|
+
import type { FunctionRegistry, RulesetDefinition, RulesetFunction } from './core/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* An optional directory that everything a ruleset pulls off disk — `extends`
|
|
5
|
+
* targets and custom function modules — must resolve inside.
|
|
6
|
+
*
|
|
7
|
+
* This is opt-in and off by default, because `basePath` on its own is only a
|
|
8
|
+
* *resolution origin*: an `extends` of `/etc/thing.js` or `../../../elsewhere`
|
|
9
|
+
* resolves and loads exactly as written. See the "Trust boundary" section of the
|
|
10
|
+
* README — a ruleset that can name a `.js` file can run code, restricted root or
|
|
11
|
+
* not. This narrows *which* files it can name; it is not a sandbox.
|
|
12
|
+
*/
|
|
13
|
+
export type IRulesetTrustOptions = {
|
|
14
|
+
/** Directory that `extends` files and custom functions must resolve under. */
|
|
15
|
+
restrictTo?: string;
|
|
16
|
+
};
|
|
17
|
+
/** Enforces {@link IRulesetTrustOptions.restrictTo}, if one was configured, on a resolved file path. */
|
|
18
|
+
export declare const assertWithinRoot: (file: string, restrictTo: string | undefined, what: string) => void;
|
|
19
|
+
/** Loads a ruleset definition from a file path by extension (YAML/JSON parsed, JS/CJS/MJS required). */
|
|
20
|
+
export declare const loadRulesetFile: (file: string) => RulesetDefinition;
|
|
21
|
+
/**
|
|
22
|
+
* Resolves an `extends` reference that names a file or an npm package:
|
|
23
|
+
* - local file paths (relative to `basePath`, or absolute): `.yaml` / `.yml` / `.json` / `.js`,
|
|
24
|
+
* - npm package specifiers (resolved from `basePath`), including subpaths.
|
|
25
|
+
*
|
|
26
|
+
* Callers that also recognize named presets (`@amritk/lint/rules/openapi` knows
|
|
27
|
+
* `oas`) check those first and only fall through to here.
|
|
28
|
+
*/
|
|
29
|
+
export declare const resolveRulesetFile: (name: string, basePath: string, options?: IRulesetTrustOptions) => ResolvedExtend;
|
|
30
|
+
/** Loads a single custom function module (`<dir>/<name>.{js,cjs,mjs}` or a bare path). */
|
|
31
|
+
export declare const loadFunctionByName: (basePath: string, dir: string, name: string, restrictTo: string | undefined) => RulesetFunction;
|
|
32
|
+
/** How {@link collectCustomFunctions} follows a string `extends` target. */
|
|
33
|
+
export type ICollectOptions = IRulesetTrustOptions & {
|
|
34
|
+
/**
|
|
35
|
+
* Resolves a string `extends` target to its definition and base directory.
|
|
36
|
+
* Return `undefined` for a name that is a built-in preset rather than a file —
|
|
37
|
+
* such a target has no directory of its own to load functions from.
|
|
38
|
+
*/
|
|
39
|
+
resolveExtend: (name: string, basePath: string) => ResolvedExtend | undefined;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Walks a ruleset definition (and its string `extends`) collecting custom
|
|
43
|
+
* functions declared via `functions` / `functionsDir`, each loaded relative to
|
|
44
|
+
* the directory of the ruleset that declared it. YAML/JSON rulesets reference
|
|
45
|
+
* functions by name; JS rulesets can instead pass direct references in `then`.
|
|
46
|
+
*/
|
|
47
|
+
export declare const collectCustomFunctions: (definition: RulesetDefinition, basePath: string, into: FunctionRegistry, seen: Set<unknown>, options: ICollectOptions) => void;
|