@amritk/lint 0.4.7 → 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.
Files changed (112) hide show
  1. package/AI.md +73 -4
  2. package/README.md +65 -14
  3. package/dist/core/filter-expression.js +17 -4
  4. package/dist/core/filter.js +82 -35
  5. package/dist/core/glob.js +25 -31
  6. package/dist/core/jsonpath.d.ts +9 -1
  7. package/dist/core/jsonpath.js +74 -16
  8. package/dist/core/lint.d.ts +8 -1
  9. package/dist/core/lint.js +4 -18
  10. package/dist/core/order.d.ts +30 -0
  11. package/dist/core/order.js +36 -0
  12. package/dist/core/own-key.d.ts +18 -0
  13. package/dist/core/own-key.js +12 -0
  14. package/dist/core/pointers.d.ts +3 -2
  15. package/dist/core/pointers.js +27 -18
  16. package/dist/core/ruleset.js +74 -27
  17. package/dist/core/runner.js +16 -23
  18. package/dist/core/severity.d.ts +11 -0
  19. package/dist/core/severity.js +13 -0
  20. package/dist/fix/apply.js +9 -2
  21. package/dist/fix/plugin.js +2 -1
  22. package/dist/functions/alphabetical.d.ts +14 -0
  23. package/dist/functions/alphabetical.js +4 -3
  24. package/dist/functions/or.js +1 -1
  25. package/dist/functions/ref-index.d.ts +15 -0
  26. package/dist/functions/ref-index.js +33 -0
  27. package/dist/functions/schema.d.ts +15 -0
  28. package/dist/functions/schema.js +2 -0
  29. package/dist/functions/typed-enum.js +2 -1
  30. package/dist/functions/unreferenced-reusable-object.js +3 -20
  31. package/dist/functions/xor.js +1 -1
  32. package/dist/index.d.ts +16 -16
  33. package/dist/index.js +25 -94
  34. package/dist/rules/asyncapi/asyncapi.d.ts +3 -0
  35. package/dist/rules/asyncapi/asyncapi.js +563 -0
  36. package/dist/rules/asyncapi/formats.d.ts +23 -0
  37. package/dist/rules/asyncapi/formats.js +49 -0
  38. package/dist/rules/asyncapi/functions/aas-server-variables.d.ts +8 -0
  39. package/dist/rules/asyncapi/functions/aas-server-variables.js +5 -0
  40. package/dist/rules/asyncapi/functions/aas-tags-unique.d.ts +2 -0
  41. package/dist/rules/asyncapi/functions/aas-tags-unique.js +5 -0
  42. package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.d.ts +7 -0
  43. package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.js +46 -0
  44. package/dist/rules/asyncapi/functions/asyncapi-channel-servers.d.ts +27 -0
  45. package/dist/rules/asyncapi/functions/asyncapi-channel-servers.js +61 -0
  46. package/dist/rules/asyncapi/functions/asyncapi-document-schema.d.ts +17 -0
  47. package/dist/rules/asyncapi/functions/asyncapi-document-schema.js +12 -0
  48. package/dist/rules/asyncapi/functions/asyncapi-headers-object.d.ts +23 -0
  49. package/dist/rules/asyncapi/functions/asyncapi-headers-object.js +25 -0
  50. package/dist/rules/asyncapi/functions/asyncapi-message-examples.d.ts +8 -0
  51. package/dist/rules/asyncapi/functions/asyncapi-message-examples.js +42 -0
  52. package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.d.ts +3 -0
  53. package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.js +30 -0
  54. package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.d.ts +3 -0
  55. package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.js +30 -0
  56. package/dist/rules/asyncapi/functions/asyncapi-payload.d.ts +14 -0
  57. package/dist/rules/asyncapi/functions/asyncapi-payload.js +32 -0
  58. package/dist/rules/asyncapi/functions/asyncapi-schema-validation.d.ts +11 -0
  59. package/dist/rules/asyncapi/functions/asyncapi-schema-validation.js +17 -0
  60. package/dist/rules/asyncapi/functions/asyncapi-security.d.ts +22 -0
  61. package/dist/rules/asyncapi/functions/asyncapi-security.js +78 -0
  62. package/dist/rules/asyncapi/functions/helpers.d.ts +51 -0
  63. package/dist/rules/asyncapi/functions/helpers.js +91 -0
  64. package/dist/rules/asyncapi/functions/index.d.ts +15 -0
  65. package/dist/rules/asyncapi/functions/index.js +53 -0
  66. package/dist/rules/asyncapi/functions/pointer.d.ts +11 -0
  67. package/dist/rules/asyncapi/functions/pointer.js +11 -0
  68. package/dist/rules/asyncapi/functions/schema-format.d.ts +4 -0
  69. package/dist/rules/asyncapi/functions/schema-format.js +7 -0
  70. package/dist/rules/asyncapi/index.d.ts +33 -0
  71. package/dist/rules/asyncapi/index.js +60 -0
  72. package/dist/rules/asyncapi/schemas/aas20.d.ts +2 -0
  73. package/dist/rules/asyncapi/schemas/aas20.js +4 -0
  74. package/dist/rules/asyncapi/schemas/aas21.d.ts +2 -0
  75. package/dist/rules/asyncapi/schemas/aas21.js +4 -0
  76. package/dist/rules/asyncapi/schemas/aas22.d.ts +2 -0
  77. package/dist/rules/asyncapi/schemas/aas22.js +4 -0
  78. package/dist/rules/asyncapi/schemas/aas23.d.ts +2 -0
  79. package/dist/rules/asyncapi/schemas/aas23.js +4 -0
  80. package/dist/rules/asyncapi/schemas/aas24.d.ts +2 -0
  81. package/dist/rules/asyncapi/schemas/aas24.js +4 -0
  82. package/dist/rules/asyncapi/schemas/aas25.d.ts +2 -0
  83. package/dist/rules/asyncapi/schemas/aas25.js +4 -0
  84. package/dist/rules/asyncapi/schemas/aas26.d.ts +2 -0
  85. package/dist/rules/asyncapi/schemas/aas26.js +4 -0
  86. package/dist/rules/asyncapi/schemas/aas30.d.ts +2 -0
  87. package/dist/rules/asyncapi/schemas/aas30.js +4 -0
  88. package/dist/rules/asyncapi/schemas/index.d.ts +20 -0
  89. package/dist/rules/asyncapi/schemas/index.js +45 -0
  90. package/dist/rules/openapi/fixers.d.ts +6 -2
  91. package/dist/rules/openapi/fixers.js +9 -6
  92. package/dist/rules/openapi/functions/oas-discriminator.js +1 -1
  93. package/dist/rules/openapi/functions/oas-mutually-exclusive.js +1 -1
  94. package/dist/rules/openapi/functions/oas-op-security-defined.js +1 -1
  95. package/dist/rules/openapi/functions/oas-path-param.js +5 -4
  96. package/dist/rules/openapi/functions/oas-server-variables.d.ts +6 -6
  97. package/dist/rules/openapi/functions/oas-server-variables.js +2 -45
  98. package/dist/rules/openapi/functions/oas-tags-unique.d.ts +2 -3
  99. package/dist/rules/openapi/functions/oas-tags-unique.js +2 -16
  100. package/dist/rules/openapi/functions/oas-unused-component.js +3 -25
  101. package/dist/rules/openapi/index.d.ts +9 -2
  102. package/dist/rules/openapi/index.js +15 -70
  103. package/dist/rules/openapi/schemas/index.js +5 -1
  104. package/dist/rules/shared/helpers.d.ts +2 -0
  105. package/dist/rules/shared/helpers.js +4 -0
  106. package/dist/rules/shared/server-variables.d.ts +27 -0
  107. package/dist/rules/shared/server-variables.js +55 -0
  108. package/dist/rules/shared/tags-unique.d.ts +7 -0
  109. package/dist/rules/shared/tags-unique.js +19 -0
  110. package/dist/ruleset-files.d.ts +47 -0
  111. package/dist/ruleset-files.js +92 -0
  112. package/package.json +9 -5
@@ -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
- * code. Pass these to `@amritk/lint`'s `fixDocument` (as its `fixers`), or wrap
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;
@@ -1,8 +1,11 @@
1
+ import { compareAlphabetically } from "../../functions/alphabetical.js";
1
2
  const getAtPath = (data, path) => {
2
3
  let current = data;
3
4
  for (const segment of path) {
4
5
  if (current == null || typeof current !== "object")
5
6
  return void 0;
7
+ if (!Object.hasOwn(current, segment))
8
+ return void 0;
6
9
  current = current[segment];
7
10
  }
8
11
  return current;
@@ -73,11 +76,6 @@ const duplicatedEnum = {
73
76
  return { op: "removeItems", path: diagnostic.path, indices: duplicates };
74
77
  }
75
78
  };
76
- const compareAlphabetical = (a, b) => {
77
- if (typeof a === "number" && typeof b === "number")
78
- return a - b;
79
- return String(a).localeCompare(String(b));
80
- };
81
79
  const tagsAlphabetical = {
82
80
  safe: true,
83
81
  fix: ({ diagnostic, data }) => {
@@ -86,9 +84,14 @@ const tagsAlphabetical = {
86
84
  if (!Array.isArray(array))
87
85
  return void 0;
88
86
  const nameOf = (item) => item != null && typeof item === "object" ? item["name"] : item;
89
- const order = array.map((_, index) => index).sort((a, b) => compareAlphabetical(nameOf(array[a]), nameOf(array[b])));
87
+ const order = array.map((_, index) => index).sort((a, b) => compareAlphabetically(nameOf(array[a]), nameOf(array[b])));
90
88
  if (order.every((value, index) => value === index))
91
89
  return void 0;
90
+ const sorted = order.map((index) => nameOf(array[index]));
91
+ for (let i = 0; i < sorted.length - 1; i++) {
92
+ if (compareAlphabetically(sorted[i], sorted[i + 1]) > 0)
93
+ return void 0;
94
+ }
92
95
  return { op: "reorderArray", path: arrayPath, order };
93
96
  }
94
97
  };
@@ -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 in properties)) {
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,3 +1,4 @@
1
+ import { setOwnKey } from "../../../core/own-key.js";
1
2
  import { isObject, OPERATION_METHODS } from "./helpers.js";
2
3
  const PATH_TEMPLATE = /(\{;?\??[a-zA-Z0-9_-]+\*?\})/g;
3
4
  const namedPathParam = (param) => param["in"] === "path" && typeof param["name"] === "string" ? param["name"] : void 0;
@@ -11,7 +12,7 @@ const recordPathParam = (param, definitionPath, seen, results) => {
11
12
  path: definitionPath
12
13
  });
13
14
  }
14
- if (name in seen) {
15
+ if (Object.hasOwn(seen, name)) {
15
16
  results.push({ message: `Path parameter "${name}" must not be defined multiple times`, path: definitionPath });
16
17
  return void 0;
17
18
  }
@@ -44,7 +45,7 @@ const oasPathParam = (paths, _options, context) => {
44
45
  const definitionPath = [...context.path, path, "parameters", index];
45
46
  const name = recordPathParam(param, definitionPath, topParams, results);
46
47
  if (name !== void 0)
47
- topParams[name] = definitionPath;
48
+ setOwnKey(topParams, name, definitionPath);
48
49
  });
49
50
  }
50
51
  for (const [method, operation] of Object.entries(item)) {
@@ -59,7 +60,7 @@ const oasPathParam = (paths, _options, context) => {
59
60
  const definitionPath = [...operationPath, "parameters", index];
60
61
  const name = recordPathParam(param, definitionPath, operationParams, results);
61
62
  if (name !== void 0)
62
- operationParams[name] = definitionPath;
63
+ setOwnKey(operationParams, name, definitionPath);
63
64
  });
64
65
  }
65
66
  const defined = { ...topParams, ...operationParams };
@@ -69,7 +70,7 @@ const oasPathParam = (paths, _options, context) => {
69
70
  }
70
71
  }
71
72
  for (const name of templates) {
72
- if (!(name in defined)) {
73
+ if (!Object.hasOwn(defined, name)) {
73
74
  results.push({
74
75
  message: `Operation must define path parameter "{${name}}" as expected by path "${path}"`,
75
76
  path: operationPath
@@ -1,8 +1,8 @@
1
- import type { RulesetFunction } from '../../../core/types.js';
1
+ import { type IServerVariablesOptions } from '../../shared/server-variables.js';
2
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`.
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 { isObject } from "./helpers.js";
2
- const oasServerVariables = (server, _options, context) => {
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
- import type { RulesetFunction } from '../../../core/types.js';
2
- /** Flags duplicate global tag names. */
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 { isObject } from "./helpers.js";
2
- const oasTagsUnique = (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
- };
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 refs = /* @__PURE__ */ new Set();
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 (!isReferenced(`#/components/${type}/${key}`)) {
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 { parseWithPointers } from "../../parsers/index.js";
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 loadRulesetFile = (file) => {
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
- if (name.startsWith(".") || isAbsolute(name)) {
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 collectCustomFunctions = (definition, basePath, into, seen) => {
58
- if (seen.has(definition))
59
- return;
60
- seen.add(definition);
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(resolved, basePath ?? process.cwd(), custom, /* @__PURE__ */ new Set());
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(resolved, {
33
+ return createCoreRuleset(definitionOrDefault, {
89
34
  functions,
90
35
  formats: oasFormats,
91
- resolve: resolveOpenApiRuleset,
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
- schema = JSON.parse(SCHEMA_TEXT[version]);
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,2 @@
1
+ /** True for a non-null, non-array object (a spec "object" value). */
2
+ export declare const isObject: (value: unknown) => value is Record<string, unknown>;
@@ -0,0 +1,4 @@
1
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2
+ export {
3
+ isObject
4
+ };
@@ -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
+ };