@amritk/lint 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/core/glob.d.ts +1 -1
- package/dist/core/glob.js +89 -5
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/jsonpath.d.ts +17 -1
- package/dist/core/jsonpath.js +218 -23
- package/dist/core/lint.d.ts +15 -8
- package/dist/core/lint.js +12 -3
- package/dist/core/plugin.d.ts +6 -0
- package/dist/core/plugin.js +6 -0
- package/dist/core/pointers.js +15 -15
- package/dist/core/ruleset.js +0 -0
- package/dist/core/runner.d.ts +6 -1
- package/dist/core/runner.js +127 -43
- package/dist/core/types.d.ts +17 -2
- package/dist/core/validate-ruleset.js +16 -0
- package/dist/fix/apply.d.ts +8 -2
- package/dist/fix/apply.js +68 -18
- package/dist/functions/alphabetical.js +40 -13
- package/dist/functions/casing.js +27 -5
- package/dist/functions/enumeration.d.ts +5 -3
- package/dist/functions/enumeration.js +18 -1
- package/dist/functions/index.d.ts +1 -0
- package/dist/functions/index.js +3 -0
- package/dist/functions/length.d.ts +13 -3
- package/dist/functions/length.js +11 -3
- package/dist/functions/or.d.ts +11 -0
- package/dist/functions/or.js +25 -0
- package/dist/functions/pattern.d.ts +5 -3
- package/dist/functions/pattern.js +42 -9
- package/dist/functions/schema.d.ts +13 -0
- package/dist/functions/schema.js +95 -2
- package/dist/functions/typed-enum.js +7 -1
- package/dist/functions/unreferenced-reusable-object.d.ts +7 -1
- package/dist/functions/unreferenced-reusable-object.js +18 -3
- package/dist/functions/xor.js +8 -1
- package/dist/index.js +9 -1
- package/dist/parsers/edit-model.d.ts +15 -0
- package/dist/parsers/edit-model.js +210 -41
- package/dist/parsers/types.d.ts +14 -2
- package/dist/parsers/yaml.d.ts +10 -0
- package/dist/parsers/yaml.js +174 -26
- package/dist/rules/openapi/fixers.js +63 -4
- package/dist/rules/openapi/formats.js +11 -4
- package/dist/rules/openapi/functions/example-validation.d.ts +16 -3
- package/dist/rules/openapi/functions/example-validation.js +102 -39
- package/dist/rules/openapi/functions/helpers.d.ts +1 -0
- package/dist/rules/openapi/functions/helpers.js +5 -0
- package/dist/rules/openapi/functions/index.d.ts +3 -1
- package/dist/rules/openapi/functions/index.js +7 -1
- package/dist/rules/openapi/functions/oas-additional-operations.js +5 -5
- package/dist/rules/openapi/functions/oas-example-external-value.d.ts +11 -0
- package/dist/rules/openapi/functions/oas-example-external-value.js +23 -0
- package/dist/rules/openapi/functions/oas-no-nullable.d.ts +13 -0
- package/dist/rules/openapi/functions/oas-no-nullable.js +22 -0
- package/dist/rules/openapi/functions/oas-op-id-unique.js +4 -2
- package/dist/rules/openapi/functions/oas-op-params.d.ts +7 -1
- package/dist/rules/openapi/functions/oas-op-params.js +35 -10
- package/dist/rules/openapi/functions/oas-op-security-defined.js +2 -2
- package/dist/rules/openapi/functions/oas-op-success-response.js +6 -1
- package/dist/rules/openapi/functions/oas-path-param.d.ts +10 -1
- package/dist/rules/openapi/functions/oas-path-param.js +87 -26
- package/dist/rules/openapi/functions/oas-server-variables.d.ts +6 -1
- package/dist/rules/openapi/functions/oas-server-variables.js +31 -2
- package/dist/rules/openapi/functions/oas-unused-component.js +14 -1
- package/dist/rules/openapi/oas.js +82 -25
- package/package.json +12 -4
package/dist/functions/casing.js
CHANGED
|
@@ -2,11 +2,15 @@ const PATTERNS = {
|
|
|
2
2
|
flat: '[a-z][a-z{d}]*',
|
|
3
3
|
camel: '[a-z][a-z{d}]*(?:[A-Z{d}](?:[a-z{d}]+|$))*',
|
|
4
4
|
pascal: '[A-Z][a-z{d}]*(?:[A-Z{d}](?:[a-z{d}]+|$))*',
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
// Segments after a separator may start with a digit, matching Spectral (so
|
|
6
|
+
// "foo-2fa" is valid kebab case). The sub-pattern is `[a-z{d}]+`, not the
|
|
7
|
+
// stricter `[a-z][a-z{d}]*` which would require a letter right after the sep.
|
|
8
|
+
kebab: '[a-z][a-z{d}]*(?:-[a-z{d}]+)*',
|
|
9
|
+
cobol: '[A-Z][A-Z{d}]*(?:-[A-Z{d}]+)*',
|
|
10
|
+
snake: '[a-z][a-z{d}]*(?:_[a-z{d}]+)*',
|
|
11
|
+
macro: '[A-Z][A-Z{d}]*(?:_[A-Z{d}]+)*',
|
|
9
12
|
};
|
|
13
|
+
const VALID_TYPES = Object.keys(PATTERNS);
|
|
10
14
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
15
|
const buildRegExp = (options) => {
|
|
12
16
|
const digits = options.disallowDigits ? '' : '0-9';
|
|
@@ -19,10 +23,28 @@ const buildRegExp = (options) => {
|
|
|
19
23
|
};
|
|
20
24
|
/** Flags a string that does not match the configured casing style. */
|
|
21
25
|
export const casing = (input, options) => {
|
|
26
|
+
if (!options?.type)
|
|
27
|
+
return [];
|
|
28
|
+
// Guard an unknown `type` before it reaches `PATTERNS[type]`, which would be
|
|
29
|
+
// `undefined` and crash on `.replace`. Mirror Spectral's option schema by
|
|
30
|
+
// naming every accepted value in a single, clear error finding.
|
|
31
|
+
if (!VALID_TYPES.includes(options.type)) {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
message: `"casing" function and its "type" option accept the following values: ${VALID_TYPES.join(', ')}`,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
}
|
|
22
38
|
if (typeof input !== 'string' || input.length === 0)
|
|
23
39
|
return [];
|
|
24
|
-
|
|
40
|
+
// Spectral special-cases a lone separator char with `allowLeading` as valid.
|
|
41
|
+
// This is what keeps the OpenAPI root path "/" from being flagged.
|
|
42
|
+
if (input.length === 1 &&
|
|
43
|
+
options.separator !== undefined &&
|
|
44
|
+
options.separator.allowLeading === true &&
|
|
45
|
+
input === options.separator.char) {
|
|
25
46
|
return [];
|
|
47
|
+
}
|
|
26
48
|
if (!buildRegExp(options).test(input)) {
|
|
27
49
|
return [{ message: `The value must be in ${options.type} case` }];
|
|
28
50
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { RulesetFunction } from '../core/index.js';
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
2
|
+
/** Options for {@link enumeration}. */
|
|
3
|
+
export type IEnumerationOptions = {
|
|
4
4
|
values: unknown[];
|
|
5
|
-
}
|
|
5
|
+
};
|
|
6
|
+
/** Flags a value that is not one of the allowed `values`. */
|
|
7
|
+
export declare const enumeration: RulesetFunction<unknown, IEnumerationOptions>;
|
|
@@ -1,6 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True for the primitive kinds Spectral's input schema accepts
|
|
3
|
+
* (`string`, `number`, `null`, `boolean`). Objects and arrays are excluded so
|
|
4
|
+
* they cannot slip through the reference-equality `includes` check below and be
|
|
5
|
+
* flagged against a list they could never be `===` to.
|
|
6
|
+
*/
|
|
7
|
+
const isPrimitive = (value) => value === null || (typeof value !== 'object' && typeof value !== 'function');
|
|
1
8
|
/** Flags a value that is not one of the allowed `values`. */
|
|
2
9
|
export const enumeration = (input, options) => {
|
|
3
|
-
const values = options?.values
|
|
10
|
+
const values = options?.values;
|
|
11
|
+
// Without a valid list of allowed values there is nothing to compare against.
|
|
12
|
+
// Spectral requires `values` in its option schema and no-ops when it is
|
|
13
|
+
// missing, so skip rather than flag everything against an empty allow-list.
|
|
14
|
+
if (!Array.isArray(values))
|
|
15
|
+
return [];
|
|
16
|
+
// Spectral's input schema only lets primitives reach this function. `includes`
|
|
17
|
+
// uses reference equality, so a non-primitive would always be reported as
|
|
18
|
+
// "not allowed"; match Spectral by skipping objects and arrays entirely.
|
|
19
|
+
if (!isPrimitive(input))
|
|
20
|
+
return [];
|
|
4
21
|
if (!values.includes(input)) {
|
|
5
22
|
return [
|
|
6
23
|
{ message: `The value must be one of the allowed values: ${values.map((v) => JSON.stringify(v)).join(', ')}` },
|
|
@@ -5,6 +5,7 @@ export { defined } from './defined.js';
|
|
|
5
5
|
export { enumeration } from './enumeration.js';
|
|
6
6
|
export { falsy } from './falsy.js';
|
|
7
7
|
export { length } from './length.js';
|
|
8
|
+
export { type IOrOptions, or } from './or.js';
|
|
8
9
|
export { pattern } from './pattern.js';
|
|
9
10
|
export { type ISchemaOptions, schema } from './schema.js';
|
|
10
11
|
export { truthy } from './truthy.js';
|
package/dist/functions/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { defined } from './defined.js';
|
|
|
4
4
|
import { enumeration } from './enumeration.js';
|
|
5
5
|
import { falsy } from './falsy.js';
|
|
6
6
|
import { length } from './length.js';
|
|
7
|
+
import { or } from './or.js';
|
|
7
8
|
import { pattern } from './pattern.js';
|
|
8
9
|
import { schema } from './schema.js';
|
|
9
10
|
import { truthy } from './truthy.js';
|
|
@@ -17,6 +18,7 @@ export { defined } from './defined.js';
|
|
|
17
18
|
export { enumeration } from './enumeration.js';
|
|
18
19
|
export { falsy } from './falsy.js';
|
|
19
20
|
export { length } from './length.js';
|
|
21
|
+
export { or } from './or.js';
|
|
20
22
|
export { pattern } from './pattern.js';
|
|
21
23
|
export { schema } from './schema.js';
|
|
22
24
|
export { truthy } from './truthy.js';
|
|
@@ -32,6 +34,7 @@ export const builtinFunctions = {
|
|
|
32
34
|
enumeration: enumeration,
|
|
33
35
|
falsy,
|
|
34
36
|
length: length,
|
|
37
|
+
or: or,
|
|
35
38
|
pattern: pattern,
|
|
36
39
|
schema: schema,
|
|
37
40
|
truthy,
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type { RulesetFunction } from '../core/index.js';
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
2
|
+
/** Options for {@link length}. */
|
|
3
|
+
export type ILengthOptions = {
|
|
4
4
|
min?: number;
|
|
5
5
|
max?: number;
|
|
6
|
-
}
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Flags a value whose size falls outside the `min`/`max` bounds.
|
|
9
|
+
*
|
|
10
|
+
* With neither `min` nor `max` supplied this is a no-op on every node. That
|
|
11
|
+
* matches Spectral, which requires at least one bound in its option schema and
|
|
12
|
+
* simply produces no results once the options load without them. We also ignore
|
|
13
|
+
* a `min`/`max` that is not a number so a stray string like "3" cannot slip into
|
|
14
|
+
* the `<`/`>` comparison and be coerced into a misleading result.
|
|
15
|
+
*/
|
|
16
|
+
export declare const length: RulesetFunction<unknown, ILengthOptions>;
|
package/dist/functions/length.js
CHANGED
|
@@ -11,16 +11,24 @@ const measure = (input) => {
|
|
|
11
11
|
return input;
|
|
12
12
|
return undefined;
|
|
13
13
|
};
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Flags a value whose size falls outside the `min`/`max` bounds.
|
|
16
|
+
*
|
|
17
|
+
* With neither `min` nor `max` supplied this is a no-op on every node. That
|
|
18
|
+
* matches Spectral, which requires at least one bound in its option schema and
|
|
19
|
+
* simply produces no results once the options load without them. We also ignore
|
|
20
|
+
* a `min`/`max` that is not a number so a stray string like "3" cannot slip into
|
|
21
|
+
* the `<`/`>` comparison and be coerced into a misleading result.
|
|
22
|
+
*/
|
|
15
23
|
export const length = (input, options) => {
|
|
16
24
|
const size = measure(input);
|
|
17
25
|
if (size === undefined)
|
|
18
26
|
return [];
|
|
19
27
|
const results = [];
|
|
20
|
-
if (options?.min
|
|
28
|
+
if (typeof options?.min === 'number' && size < options.min) {
|
|
21
29
|
results.push({ message: `The value must not be shorter than ${options.min}` });
|
|
22
30
|
}
|
|
23
|
-
if (options?.max
|
|
31
|
+
if (typeof options?.max === 'number' && size > options.max) {
|
|
24
32
|
results.push({ message: `The value must not be longer than ${options.max}` });
|
|
25
33
|
}
|
|
26
34
|
return results;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../core/index.js';
|
|
2
|
+
/** Options for {@link or}. */
|
|
3
|
+
export type IOrOptions = {
|
|
4
|
+
properties: string[];
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Flags an object when none of the listed `properties` is defined on it. Where
|
|
8
|
+
* {@link xor} requires exactly one, `or` requires at least one — it stays quiet
|
|
9
|
+
* as soon as a single listed property is present.
|
|
10
|
+
*/
|
|
11
|
+
export declare const or: RulesetFunction<Record<string, unknown>, IOrOptions>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flags an object when none of the listed `properties` is defined on it. Where
|
|
3
|
+
* {@link xor} requires exactly one, `or` requires at least one — it stays quiet
|
|
4
|
+
* as soon as a single listed property is present.
|
|
5
|
+
*/
|
|
6
|
+
export const or = (input, options) => {
|
|
7
|
+
if (typeof input !== 'object' || input === null)
|
|
8
|
+
return [];
|
|
9
|
+
const properties = options?.properties;
|
|
10
|
+
// Spectral validates that at least two properties are supplied and no-ops when
|
|
11
|
+
// that is not the case, so we skip in silence rather than flag every node.
|
|
12
|
+
if (!Array.isArray(properties) || properties.length < 2)
|
|
13
|
+
return [];
|
|
14
|
+
const present = properties.filter((property) => property in input);
|
|
15
|
+
if (present.length > 0)
|
|
16
|
+
return [];
|
|
17
|
+
// Match Spectral's message: a long list is abbreviated to the first three
|
|
18
|
+
// properties plus a count of the rest so the finding stays readable.
|
|
19
|
+
if (properties.length > 4) {
|
|
20
|
+
const shortProps = properties.slice(0, 3);
|
|
21
|
+
const count = `${properties.length - 3} other properties must be defined`;
|
|
22
|
+
return [{ message: `At least one of "${shortProps.join('" or "')}" or ${count}` }];
|
|
23
|
+
}
|
|
24
|
+
return [{ message: `At least one of "${properties.join('" or "')}" must be defined` }];
|
|
25
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { RulesetFunction } from '../core/index.js';
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
2
|
+
/** Options for {@link pattern}. */
|
|
3
|
+
export type IPatternOptions = {
|
|
4
4
|
match?: string;
|
|
5
5
|
notMatch?: string;
|
|
6
|
-
}
|
|
6
|
+
};
|
|
7
|
+
/** Flags a string that fails `match` or satisfies `notMatch`. */
|
|
8
|
+
export declare const pattern: RulesetFunction<string, IPatternOptions>;
|
|
@@ -1,20 +1,53 @@
|
|
|
1
|
-
|
|
1
|
+
// Compiling a RegExp is not free, and rules run this function once per matched
|
|
2
|
+
// node, so we memoize by the raw pattern string (which carries its own flags in
|
|
3
|
+
// the `/re/flags` form). A failed compile is cached as its `Error` so a bad
|
|
4
|
+
// pattern is reported without being re-thrown on every node. Mirrors Spectral's
|
|
5
|
+
// own module-level cache.
|
|
6
|
+
const cache = new Map();
|
|
7
|
+
/** Parses a `/pattern/flags` string (or a bare pattern) into a RegExp, or the compile error. */
|
|
2
8
|
const toRegExp = (pattern) => {
|
|
3
|
-
const
|
|
4
|
-
if (
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
const cached = cache.get(pattern);
|
|
10
|
+
if (cached !== undefined) {
|
|
11
|
+
// A cached RegExp carrying the `g`/`y` flag keeps `lastIndex` between calls,
|
|
12
|
+
// which would make `.test` skip characters on the next node. Reset it so a
|
|
13
|
+
// reused pattern behaves the same as a freshly compiled one.
|
|
14
|
+
if (cached instanceof RegExp)
|
|
15
|
+
cached.lastIndex = 0;
|
|
16
|
+
return cached;
|
|
17
|
+
}
|
|
18
|
+
let compiled;
|
|
19
|
+
try {
|
|
20
|
+
const match = /^\/(.+)\/([a-z]*)$/s.exec(pattern);
|
|
21
|
+
compiled = match ? new RegExp(match[1], match[2]) : new RegExp(pattern);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
compiled = error instanceof Error ? error : new Error(String(error));
|
|
25
|
+
}
|
|
26
|
+
cache.set(pattern, compiled);
|
|
27
|
+
return compiled;
|
|
7
28
|
};
|
|
8
29
|
/** Flags a string that fails `match` or satisfies `notMatch`. */
|
|
9
30
|
export const pattern = (input, options) => {
|
|
10
31
|
if (typeof input !== 'string')
|
|
11
32
|
return [];
|
|
12
33
|
const results = [];
|
|
13
|
-
if (options?.match !== undefined
|
|
14
|
-
|
|
34
|
+
if (options?.match !== undefined) {
|
|
35
|
+
const re = toRegExp(options.match);
|
|
36
|
+
if (re instanceof Error) {
|
|
37
|
+
results.push({ message: `The "match" option is not a valid regular expression: ${re.message}` });
|
|
38
|
+
}
|
|
39
|
+
else if (!re.test(input)) {
|
|
40
|
+
results.push({ message: `The value must match the pattern "${options.match}"` });
|
|
41
|
+
}
|
|
15
42
|
}
|
|
16
|
-
if (options?.notMatch !== undefined
|
|
17
|
-
|
|
43
|
+
if (options?.notMatch !== undefined) {
|
|
44
|
+
const re = toRegExp(options.notMatch);
|
|
45
|
+
if (re instanceof Error) {
|
|
46
|
+
results.push({ message: `The "notMatch" option is not a valid regular expression: ${re.message}` });
|
|
47
|
+
}
|
|
48
|
+
else if (re.test(input)) {
|
|
49
|
+
results.push({ message: `The value must not match the pattern "${options.notMatch}"` });
|
|
50
|
+
}
|
|
18
51
|
}
|
|
19
52
|
return results;
|
|
20
53
|
};
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import type { RulesetFunction } from '../core/index.js';
|
|
2
2
|
/** Options for {@link schema}. */
|
|
3
3
|
export type ISchemaOptions = {
|
|
4
|
+
/** The JSON Schema to validate the matched value against. */
|
|
4
5
|
schema: object;
|
|
6
|
+
/**
|
|
7
|
+
* Report every validation error rather than stopping at the first. Defaults to
|
|
8
|
+
* `false`, matching Spectral (whose underlying ajv instance runs with
|
|
9
|
+
* `allErrors: false` unless told otherwise).
|
|
10
|
+
*/
|
|
5
11
|
allErrors?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Accepted for Spectral compatibility but intentionally ignored. The draft is
|
|
14
|
+
* auto-detected by `@amritk/runtime-validators`, so both draft-4 constructs
|
|
15
|
+
* (boolean `exclusiveMinimum`) and draft-7 ones (tuple `items`) validate
|
|
16
|
+
* without the caller naming a dialect.
|
|
17
|
+
*/
|
|
18
|
+
dialect?: string;
|
|
6
19
|
};
|
|
7
20
|
/** Validates a value against a JSON Schema supplied in the rule's options. */
|
|
8
21
|
export declare const schema: RulesetFunction<unknown, ISchemaOptions>;
|
package/dist/functions/schema.js
CHANGED
|
@@ -13,6 +13,70 @@ const getValidator = (schema) => {
|
|
|
13
13
|
}
|
|
14
14
|
return validator;
|
|
15
15
|
};
|
|
16
|
+
const KNOWN_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'null', 'object', 'array']);
|
|
17
|
+
// Keywords whose value is itself a schema, a list of schemas, or a map of
|
|
18
|
+
// schemas. We recurse through them to reach every `type` a schema declares.
|
|
19
|
+
const SCHEMA_VALUE_KEYS = ['additionalProperties', 'not', 'if', 'then', 'else', 'propertyNames', 'contains'];
|
|
20
|
+
const SCHEMA_ITEMS_KEYS = ['items', 'additionalItems'];
|
|
21
|
+
const SCHEMA_LIST_KEYS = ['allOf', 'anyOf', 'oneOf'];
|
|
22
|
+
const SCHEMA_MAP_KEYS = ['properties', 'patternProperties', 'definitions', '$defs', 'dependencies'];
|
|
23
|
+
/**
|
|
24
|
+
* Walks a schema looking for a `type` keyword whose value is not a JSON Schema
|
|
25
|
+
* type. `@amritk/runtime-validators` treats an unknown `type` as "always
|
|
26
|
+
* matches" (so it never rejects data it does not model), which means a typo like
|
|
27
|
+
* `type: "Pascal"` would silently disable the rule. Finding it up front lets us
|
|
28
|
+
* report it instead. Returns the offending type, or `undefined` when the schema
|
|
29
|
+
* only uses known types.
|
|
30
|
+
*/
|
|
31
|
+
const findInvalidType = (node) => {
|
|
32
|
+
if (Array.isArray(node) || typeof node !== 'object' || node === null)
|
|
33
|
+
return undefined;
|
|
34
|
+
const schema = node;
|
|
35
|
+
const declared = schema['type'];
|
|
36
|
+
if (declared !== undefined) {
|
|
37
|
+
const types = Array.isArray(declared) ? declared : [declared];
|
|
38
|
+
for (const type of types) {
|
|
39
|
+
if (typeof type !== 'string' || !KNOWN_TYPES.has(type)) {
|
|
40
|
+
return typeof type === 'string' ? type : String(type);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const key of SCHEMA_VALUE_KEYS) {
|
|
45
|
+
const found = findInvalidType(schema[key]);
|
|
46
|
+
if (found !== undefined)
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
for (const key of SCHEMA_ITEMS_KEYS) {
|
|
50
|
+
const value = schema[key];
|
|
51
|
+
const list = Array.isArray(value) ? value : [value];
|
|
52
|
+
for (const item of list) {
|
|
53
|
+
const found = findInvalidType(item);
|
|
54
|
+
if (found !== undefined)
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const key of SCHEMA_LIST_KEYS) {
|
|
59
|
+
const value = schema[key];
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
for (const item of value) {
|
|
62
|
+
const found = findInvalidType(item);
|
|
63
|
+
if (found !== undefined)
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const key of SCHEMA_MAP_KEYS) {
|
|
69
|
+
const value = schema[key];
|
|
70
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
71
|
+
for (const item of Object.values(value)) {
|
|
72
|
+
const found = findInvalidType(item);
|
|
73
|
+
if (found !== undefined)
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
};
|
|
16
80
|
const pointerToPath = (pointer) => pointer
|
|
17
81
|
.split('/')
|
|
18
82
|
.slice(1)
|
|
@@ -26,10 +90,39 @@ const formatError = (error) => {
|
|
|
26
90
|
export const schema = (input, options, context) => {
|
|
27
91
|
if (!options?.schema)
|
|
28
92
|
return [];
|
|
29
|
-
|
|
93
|
+
// A malformed schema would otherwise validate everything and silently disable
|
|
94
|
+
// the rule. Surface it as a finding, the way Spectral reports schema compile
|
|
95
|
+
// errors as results, so the ruleset author notices the mistake.
|
|
96
|
+
const invalidType = findInvalidType(options.schema);
|
|
97
|
+
if (invalidType !== undefined) {
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
message: `Invalid schema: unknown type "${invalidType}". Valid types are: ${[...KNOWN_TYPES].join(', ')}`,
|
|
101
|
+
path: [...context.path],
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
let result;
|
|
106
|
+
try {
|
|
107
|
+
result = getValidator(options.schema)(input);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// Preparing or running the validator can throw on a schema shape we cannot
|
|
111
|
+
// interpret. Report it rather than letting it bubble up and crash the run.
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
message: `Invalid schema: ${error instanceof Error ? error.message : String(error)}`,
|
|
115
|
+
path: [...context.path],
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}
|
|
30
119
|
if (result === true)
|
|
31
120
|
return [];
|
|
32
|
-
|
|
121
|
+
// Spectral's ajv defaults to `allErrors: false`, reporting only the first
|
|
122
|
+
// failure. Honor the same default and only expand to the full list when the
|
|
123
|
+
// caller opts in.
|
|
124
|
+
const errors = options.allErrors ? result.errors : result.errors.slice(0, 1);
|
|
125
|
+
return errors.map((error) => ({
|
|
33
126
|
message: formatError(error),
|
|
34
127
|
path: [...context.path, ...pointerToPath(error.path)],
|
|
35
128
|
}));
|
|
@@ -15,7 +15,13 @@ export const typedEnum = (input, _options, context) => {
|
|
|
15
15
|
const values = input['enum'];
|
|
16
16
|
if (declaredType === undefined || !Array.isArray(values))
|
|
17
17
|
return [];
|
|
18
|
-
const types = Array.isArray(declaredType) ? declaredType : [declaredType];
|
|
18
|
+
const types = Array.isArray(declaredType) ? [...declaredType] : [declaredType];
|
|
19
|
+
// A schema marked nullable (OpenAPI 3 `nullable` or the Swagger 2 `x-nullable`
|
|
20
|
+
// vendor extension) is allowed to hold `null` in addition to its declared
|
|
21
|
+
// type, so `null` must not be flagged as a type mismatch.
|
|
22
|
+
if ((input['nullable'] === true || input['x-nullable'] === true) && !types.includes('null')) {
|
|
23
|
+
types.push('null');
|
|
24
|
+
}
|
|
19
25
|
const checkers = types
|
|
20
26
|
.map((type) => JS_TYPES[String(type)])
|
|
21
27
|
.filter((fn) => Boolean(fn));
|
|
@@ -4,5 +4,11 @@ export type IUnreferencedReusableObjectOptions = {
|
|
|
4
4
|
/** JSON pointer to the map of reusable objects, e.g. "#/components/schemas". */
|
|
5
5
|
reusableObjectsLocation: string;
|
|
6
6
|
};
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Flags entries in a reusable-object map that nothing `$ref`s.
|
|
9
|
+
*
|
|
10
|
+
* This must run against the *unresolved* document: once `$ref`s are inlined by a
|
|
11
|
+
* resolver there are no references left to count, so every reusable object would
|
|
12
|
+
* look orphaned.
|
|
13
|
+
*/
|
|
8
14
|
export declare const unreferencedReusableObject: RulesetFunction<Record<string, unknown>, IUnreferencedReusableObjectOptions>;
|
|
@@ -14,7 +14,15 @@ const collectRefs = (node, into) => {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
|
-
/**
|
|
17
|
+
/** Escapes a key for use in a JSON pointer segment (`~` -> `~0`, `/` -> `~1`). */
|
|
18
|
+
const escapePointerSegment = (key) => key.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
19
|
+
/**
|
|
20
|
+
* Flags entries in a reusable-object map that nothing `$ref`s.
|
|
21
|
+
*
|
|
22
|
+
* This must run against the *unresolved* document: once `$ref`s are inlined by a
|
|
23
|
+
* resolver there are no references left to count, so every reusable object would
|
|
24
|
+
* look orphaned.
|
|
25
|
+
*/
|
|
18
26
|
export const unreferencedReusableObject = (input, options, context) => {
|
|
19
27
|
if (typeof input !== 'object' || input === null)
|
|
20
28
|
return [];
|
|
@@ -25,8 +33,15 @@ export const unreferencedReusableObject = (input, options, context) => {
|
|
|
25
33
|
collectRefs(context.document.data, refs);
|
|
26
34
|
const results = [];
|
|
27
35
|
for (const key of Object.keys(input)) {
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
// A key such as "a/b" appears in a pointer as "a~1b", so escape it before
|
|
37
|
+
// building the expected reference. Without this a legitimately referenced
|
|
38
|
+
// object with a special character in its name looks unreferenced.
|
|
39
|
+
const base = `${location}/${escapePointerSegment(key)}`;
|
|
40
|
+
// A reference can point straight at the object (`base`) or deeper into it
|
|
41
|
+
// (e.g. `base/properties/x`); either counts as a use, so match the exact
|
|
42
|
+
// pointer or any pointer nested beneath it.
|
|
43
|
+
const referenced = refs.has(base) || [...refs].some((ref) => ref.startsWith(`${base}/`));
|
|
44
|
+
if (!referenced) {
|
|
30
45
|
results.push({
|
|
31
46
|
message: 'This reusable object is never referenced',
|
|
32
47
|
path: [...context.path, key],
|
package/dist/functions/xor.js
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
export const xor = (input, options) => {
|
|
3
3
|
if (typeof input !== 'object' || input === null)
|
|
4
4
|
return [];
|
|
5
|
-
const properties = options?.properties
|
|
5
|
+
const properties = options?.properties;
|
|
6
|
+
// Spectral validates the option schema (an array of at least two strings)
|
|
7
|
+
// before the function runs and no-ops when it fails, so with fewer than two
|
|
8
|
+
// properties there is nothing meaningful to check. We deliberately skip in
|
|
9
|
+
// silence rather than push an error: an empty or single-element list would
|
|
10
|
+
// otherwise flag every node with a message that names nothing useful.
|
|
11
|
+
if (!Array.isArray(properties) || properties.length < 2)
|
|
12
|
+
return [];
|
|
6
13
|
const present = properties.filter((property) => property in input);
|
|
7
14
|
if (present.length !== 1) {
|
|
8
15
|
return [{ message: `Exactly one of ${properties.map((p) => `"${p}"`).join(', ')} must be defined` }];
|
package/dist/index.js
CHANGED
|
@@ -71,7 +71,11 @@ const loadFunctionByName = (basePath, dir, name) => {
|
|
|
71
71
|
* the directory of the ruleset that declared it. YAML/JSON rulesets reference
|
|
72
72
|
* functions by name; JS rulesets can instead pass direct references in `then`.
|
|
73
73
|
*/
|
|
74
|
-
const collectCustomFunctions = (definition, basePath, into,
|
|
74
|
+
const collectCustomFunctions = (definition, basePath, into,
|
|
75
|
+
// Keyed by (basePath, reference) for string extends and by object identity for
|
|
76
|
+
// inline ones. `loadRulesetFile` returns a fresh object per read, so object
|
|
77
|
+
// identity alone would never dedupe a file cycle — we key on the resolved edge.
|
|
78
|
+
seen) => {
|
|
75
79
|
if (seen.has(definition))
|
|
76
80
|
return;
|
|
77
81
|
seen.add(definition);
|
|
@@ -80,6 +84,10 @@ const collectCustomFunctions = (definition, basePath, into, seen) => {
|
|
|
80
84
|
for (const entry of entries) {
|
|
81
85
|
const target = Array.isArray(entry) ? entry[0] : entry;
|
|
82
86
|
if (typeof target === 'string') {
|
|
87
|
+
const key = `${basePath}\0${target}`;
|
|
88
|
+
if (seen.has(key))
|
|
89
|
+
continue;
|
|
90
|
+
seen.add(key);
|
|
83
91
|
const resolved = resolveNamedRuleset(target, basePath);
|
|
84
92
|
collectCustomFunctions(resolved.definition, resolved.basePath, into, seen);
|
|
85
93
|
}
|
|
@@ -5,6 +5,21 @@ import type { JsonPath } from './types.js';
|
|
|
5
5
|
* paths and values rather than raw character offsets. {@link applyEditOps} lowers
|
|
6
6
|
* each op to a minimal text edit so that untouched parts of the source — including
|
|
7
7
|
* comments, key order, and quoting — keep their original formatting.
|
|
8
|
+
*
|
|
9
|
+
* A few edits are deliberately conservative no-ops rather than risky rewrites:
|
|
10
|
+
*
|
|
11
|
+
* - Edits that traverse a YAML alias (`*ref`) or a `<<` merge key resolve to a
|
|
12
|
+
* node the edit model cannot address structurally, so they are dropped. This is
|
|
13
|
+
* safe — the source is left untouched — but it does mean a finding on data that
|
|
14
|
+
* only exists via an alias/merge is not auto-fixed.
|
|
15
|
+
* - `setValue` on an *anchored* node (`&anchor`) rewrites the anchor's own text,
|
|
16
|
+
* which every `*alias` to it re-reads; the change therefore propagates to every
|
|
17
|
+
* alias use-site. That is the correct YAML semantics, but callers should be
|
|
18
|
+
* aware the edit is not local to one path.
|
|
19
|
+
* - Multi-line *flow* collections (`[\n a,\n b\n]`) are rebuilt onto a single
|
|
20
|
+
* line by {@link rewriteYamlSeq} and the flow-map insert, and any comments that
|
|
21
|
+
* lived between their members are dropped. Block collections keep their layout
|
|
22
|
+
* and comments; only flow ones are collapsed.
|
|
8
23
|
*/
|
|
9
24
|
export type EditOp =
|
|
10
25
|
/** Replace the scalar value at `path` (the existing quoting style is preserved). */
|