@amritk/lint 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -1
- package/dist/core/document.d.ts +19 -0
- package/dist/core/document.js +11 -0
- package/dist/core/formats.d.ts +9 -0
- package/dist/core/formats.js +14 -0
- package/dist/core/glob.d.ts +4 -0
- package/dist/core/glob.js +48 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.js +11 -0
- package/dist/core/jsonpath.d.ts +59 -0
- package/dist/core/jsonpath.js +415 -0
- package/dist/core/lint.d.ts +57 -0
- package/dist/core/lint.js +82 -0
- package/dist/core/plugin.d.ts +59 -0
- package/dist/core/plugin.js +25 -0
- package/dist/core/pointers.d.ts +35 -0
- package/dist/core/pointers.js +169 -0
- package/dist/core/ruleset.d.ts +52 -0
- package/dist/core/ruleset.js +170 -0
- package/dist/core/runner.d.ts +21 -0
- package/dist/core/runner.js +222 -0
- package/dist/core/types.d.ts +170 -0
- package/dist/core/types.js +1 -0
- package/dist/core/validate-ruleset.d.ts +14 -0
- package/dist/core/validate-ruleset.js +105 -0
- package/dist/fix/apply.d.ts +21 -0
- package/dist/fix/apply.js +51 -0
- package/dist/fix/index.d.ts +3 -0
- package/dist/fix/index.js +2 -0
- package/dist/fix/plugin.d.ts +18 -0
- package/dist/fix/plugin.js +21 -0
- package/dist/fix/types.d.ts +39 -0
- package/dist/fix/types.js +0 -0
- package/dist/functions/alphabetical.d.ts +8 -0
- package/dist/functions/alphabetical.js +28 -0
- package/dist/functions/casing.d.ts +14 -0
- package/dist/functions/casing.js +30 -0
- package/dist/functions/defined.d.ts +3 -0
- package/dist/functions/defined.js +6 -0
- package/dist/functions/enumeration.d.ts +5 -0
- package/dist/functions/enumeration.js +10 -0
- package/dist/functions/falsy.d.ts +3 -0
- package/dist/functions/falsy.js +6 -0
- package/dist/functions/index.d.ts +16 -0
- package/dist/functions/index.js +42 -0
- package/dist/functions/length.d.ts +6 -0
- package/dist/functions/length.js +27 -0
- package/dist/functions/pattern.d.ts +6 -0
- package/dist/functions/pattern.js +20 -0
- package/dist/functions/schema.d.ts +8 -0
- package/dist/functions/schema.js +36 -0
- package/dist/functions/truthy.d.ts +3 -0
- package/dist/functions/truthy.js +6 -0
- package/dist/functions/typed-enum.d.ts +3 -0
- package/dist/functions/typed-enum.js +34 -0
- package/dist/functions/undefined.d.ts +6 -0
- package/dist/functions/undefined.js +9 -0
- package/dist/functions/unreferenced-reusable-object.d.ts +8 -0
- package/dist/functions/unreferenced-reusable-object.js +37 -0
- package/dist/functions/xor.d.ts +7 -0
- package/dist/functions/xor.js +11 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +168 -0
- package/dist/parsers/edit-model.d.ts +69 -0
- package/dist/parsers/edit-model.js +326 -0
- package/dist/parsers/index.d.ts +18 -0
- package/dist/parsers/index.js +21 -0
- package/dist/parsers/json.d.ts +3 -0
- package/dist/parsers/json.js +38 -0
- package/dist/parsers/lines.d.ts +13 -0
- package/dist/parsers/lines.js +28 -0
- package/dist/parsers/types.d.ts +50 -0
- package/dist/parsers/types.js +8 -0
- package/dist/parsers/yaml.d.ts +6 -0
- package/dist/parsers/yaml.js +65 -0
- package/dist/rules/openapi/fixers.d.ts +7 -0
- package/dist/rules/openapi/fixers.js +189 -0
- package/dist/rules/openapi/formats.d.ts +13 -0
- package/dist/rules/openapi/formats.js +23 -0
- package/dist/rules/openapi/functions/example-validation.d.ts +5 -0
- package/dist/rules/openapi/functions/example-validation.js +97 -0
- package/dist/rules/openapi/functions/helpers.d.ts +3 -0
- package/dist/rules/openapi/functions/helpers.js +5 -0
- package/dist/rules/openapi/functions/index.d.ts +24 -0
- package/dist/rules/openapi/functions/index.js +67 -0
- package/dist/rules/openapi/functions/oas-additional-operations.d.ts +8 -0
- package/dist/rules/openapi/functions/oas-additional-operations.js +25 -0
- package/dist/rules/openapi/functions/oas-discriminator.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-discriminator.js +23 -0
- package/dist/rules/openapi/functions/oas-example-value.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-example-value.js +31 -0
- package/dist/rules/openapi/functions/oas-mutually-exclusive.d.ts +10 -0
- package/dist/rules/openapi/functions/oas-mutually-exclusive.js +20 -0
- package/dist/rules/openapi/functions/oas-op-form-data-consume-check.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-form-data-consume-check.js +21 -0
- package/dist/rules/openapi/functions/oas-op-id-unique.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-id-unique.js +27 -0
- package/dist/rules/openapi/functions/oas-op-params.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-params.js +21 -0
- package/dist/rules/openapi/functions/oas-op-security-defined.d.ts +5 -0
- package/dist/rules/openapi/functions/oas-op-security-defined.js +44 -0
- package/dist/rules/openapi/functions/oas-op-success-response.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-success-response.js +11 -0
- package/dist/rules/openapi/functions/oas-path-param.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-path-param.js +45 -0
- package/dist/rules/openapi/functions/oas-schema-example-deprecated.d.ts +10 -0
- package/dist/rules/openapi/functions/oas-schema-example-deprecated.js +42 -0
- package/dist/rules/openapi/functions/oas-schema.d.ts +15 -0
- package/dist/rules/openapi/functions/oas-schema.js +15 -0
- package/dist/rules/openapi/functions/oas-server-name-unique.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-server-name-unique.js +20 -0
- package/dist/rules/openapi/functions/oas-server-variables.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-server-variables.js +23 -0
- package/dist/rules/openapi/functions/oas-tag-defined.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-tag-defined.js +21 -0
- package/dist/rules/openapi/functions/oas-tag-kind.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-tag-kind.js +17 -0
- package/dist/rules/openapi/functions/oas-tag-parent-defined.d.ts +7 -0
- package/dist/rules/openapi/functions/oas-tag-parent-defined.js +45 -0
- package/dist/rules/openapi/functions/oas-tags-unique.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-tags-unique.js +17 -0
- package/dist/rules/openapi/functions/oas-unused-component.d.ts +7 -0
- package/dist/rules/openapi/functions/oas-unused-component.js +52 -0
- package/dist/rules/openapi/functions/ref-siblings.d.ts +3 -0
- package/dist/rules/openapi/functions/ref-siblings.js +13 -0
- package/dist/rules/openapi/index.d.ts +25 -0
- package/dist/rules/openapi/index.js +127 -0
- package/dist/rules/openapi/oas.d.ts +3 -0
- package/dist/rules/openapi/oas.js +492 -0
- package/dist/rules/openapi/schemas/index.d.ts +8 -0
- package/dist/rules/openapi/schemas/index.js +36 -0
- package/dist/rules/openapi/schemas/oas20.json +1592 -0
- package/dist/rules/openapi/schemas/oas30.json +1651 -0
- package/dist/rules/openapi/schemas/oas31.json +1412 -0
- package/dist/rules/openapi/schemas/oas32.json +1684 -0
- package/package.json +8 -5
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { IDiagnostic, JsonPath } from '../core/index.js';
|
|
2
|
+
import type { EditOp, ParserFormat } from '../parsers/index.js';
|
|
3
|
+
export type { EditOp, ParserFormat };
|
|
4
|
+
/** What a {@link Fixer} is handed for a single finding it might repair. */
|
|
5
|
+
export type FixContext = {
|
|
6
|
+
/** The finding to fix. */
|
|
7
|
+
diagnostic: IDiagnostic;
|
|
8
|
+
/** The parsed, *unresolved* document data — fixers read the real node to derive the edit. */
|
|
9
|
+
data: unknown;
|
|
10
|
+
/** The document's format, in case a fixer needs to vary its output. */
|
|
11
|
+
format: ParserFormat;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A repair for the findings of one rule. `fix` inspects the finding (and the
|
|
15
|
+
* underlying data) and returns the structural edit(s) that resolve it, or
|
|
16
|
+
* `undefined` if it can't. `safe` marks whether the fix is semantics-preserving
|
|
17
|
+
* enough to apply automatically; `lint --fix` applies safe fixes only.
|
|
18
|
+
*/
|
|
19
|
+
export type Fixer = {
|
|
20
|
+
/** Defaults to `true`. Set `false` for fixes that may change behavior (opt-in only). */
|
|
21
|
+
safe?: boolean;
|
|
22
|
+
fix: (context: FixContext) => EditOp[] | EditOp | undefined;
|
|
23
|
+
};
|
|
24
|
+
/** Fixers keyed by the rule `code` whose findings they repair. */
|
|
25
|
+
export type FixerRegistry = Record<string, Fixer>;
|
|
26
|
+
/** A finding that a fixer produced edits for. */
|
|
27
|
+
export type AppliedFix = {
|
|
28
|
+
code: string | number;
|
|
29
|
+
path: JsonPath;
|
|
30
|
+
};
|
|
31
|
+
/** The outcome of {@link applyFixes}. */
|
|
32
|
+
export type FixResult = {
|
|
33
|
+
/** The rewritten document text (identical to the input when nothing applied). */
|
|
34
|
+
output: string;
|
|
35
|
+
/** The findings that were repaired. */
|
|
36
|
+
applied: AppliedFix[];
|
|
37
|
+
/** Whether `output` differs from the input. */
|
|
38
|
+
changed: boolean;
|
|
39
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../core/index.js';
|
|
2
|
+
/** Options for {@link alphabetical}. */
|
|
3
|
+
export type IAlphabeticalOptions = {
|
|
4
|
+
/** Compare objects by this property instead of the value itself. */
|
|
5
|
+
keyedBy?: string;
|
|
6
|
+
};
|
|
7
|
+
/** Flags array items or object keys that are not in ascending (optionally `keyedBy`) order. */
|
|
8
|
+
export declare const alphabetical: RulesetFunction<unknown, IAlphabeticalOptions>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const compare = (a, b) => {
|
|
2
|
+
if (typeof a === 'number' && typeof b === 'number')
|
|
3
|
+
return a - b;
|
|
4
|
+
return String(a).localeCompare(String(b));
|
|
5
|
+
};
|
|
6
|
+
/** Flags array items or object keys that are not in ascending (optionally `keyedBy`) order. */
|
|
7
|
+
export const alphabetical = (input, options, context) => {
|
|
8
|
+
if (typeof input !== 'object' || input === null)
|
|
9
|
+
return [];
|
|
10
|
+
const isArray = Array.isArray(input);
|
|
11
|
+
const items = isArray ? input : Object.keys(input);
|
|
12
|
+
const keyedBy = options?.keyedBy;
|
|
13
|
+
const results = [];
|
|
14
|
+
for (let i = 0; i < items.length - 1; i++) {
|
|
15
|
+
let current = items[i];
|
|
16
|
+
let next = items[i + 1];
|
|
17
|
+
if (keyedBy) {
|
|
18
|
+
current = isRecord(current) ? current[keyedBy] : current;
|
|
19
|
+
next = isRecord(next) ? next[keyedBy] : next;
|
|
20
|
+
}
|
|
21
|
+
if (compare(current, next) > 0) {
|
|
22
|
+
const path = isArray ? [...context.path, i + 1] : [...context.path, items[i + 1]];
|
|
23
|
+
results.push({ message: 'The items must be in alphabetical order', path });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
};
|
|
28
|
+
const isRecord = (value) => typeof value === 'object' && value !== null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../core/index.js';
|
|
2
|
+
/** The supported casing styles a value can be checked against. */
|
|
3
|
+
export type CasingType = 'flat' | 'camel' | 'pascal' | 'kebab' | 'cobol' | 'snake' | 'macro';
|
|
4
|
+
/** Options for {@link casing}. */
|
|
5
|
+
export type ICasingOptions = {
|
|
6
|
+
type: CasingType;
|
|
7
|
+
disallowDigits?: boolean;
|
|
8
|
+
separator?: {
|
|
9
|
+
char: string;
|
|
10
|
+
allowLeading?: boolean;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/** Flags a string that does not match the configured casing style. */
|
|
14
|
+
export declare const casing: RulesetFunction<string, ICasingOptions>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const PATTERNS = {
|
|
2
|
+
flat: '[a-z][a-z{d}]*',
|
|
3
|
+
camel: '[a-z][a-z{d}]*(?:[A-Z{d}](?:[a-z{d}]+|$))*',
|
|
4
|
+
pascal: '[A-Z][a-z{d}]*(?:[A-Z{d}](?:[a-z{d}]+|$))*',
|
|
5
|
+
kebab: '[a-z][a-z{d}]*(?:-[a-z][a-z{d}]*)*',
|
|
6
|
+
cobol: '[A-Z][A-Z{d}]*(?:-[A-Z][A-Z{d}]*)*',
|
|
7
|
+
snake: '[a-z][a-z{d}]*(?:_[a-z][a-z{d}]*)*',
|
|
8
|
+
macro: '[A-Z][A-Z{d}]*(?:_[A-Z][A-Z{d}]*)*',
|
|
9
|
+
};
|
|
10
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
|
+
const buildRegExp = (options) => {
|
|
12
|
+
const digits = options.disallowDigits ? '' : '0-9';
|
|
13
|
+
const base = PATTERNS[options.type].replace(/\{d\}/g, digits);
|
|
14
|
+
if (!options.separator)
|
|
15
|
+
return new RegExp(`^${base}$`);
|
|
16
|
+
const sep = escapeRegExp(options.separator.char);
|
|
17
|
+
const leading = options.separator.allowLeading ? `${sep}?` : '';
|
|
18
|
+
return new RegExp(`^${leading}${base}(?:${sep}${base})*$`);
|
|
19
|
+
};
|
|
20
|
+
/** Flags a string that does not match the configured casing style. */
|
|
21
|
+
export const casing = (input, options) => {
|
|
22
|
+
if (typeof input !== 'string' || input.length === 0)
|
|
23
|
+
return [];
|
|
24
|
+
if (!options?.type)
|
|
25
|
+
return [];
|
|
26
|
+
if (!buildRegExp(options).test(input)) {
|
|
27
|
+
return [{ message: `The value must be in ${options.type} case` }];
|
|
28
|
+
}
|
|
29
|
+
return [];
|
|
30
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Flags a value that is not one of the allowed `values`. */
|
|
2
|
+
export const enumeration = (input, options) => {
|
|
3
|
+
const values = options?.values ?? [];
|
|
4
|
+
if (!values.includes(input)) {
|
|
5
|
+
return [
|
|
6
|
+
{ message: `The value must be one of the allowed values: ${values.map((v) => JSON.stringify(v)).join(', ')}` },
|
|
7
|
+
];
|
|
8
|
+
}
|
|
9
|
+
return [];
|
|
10
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FunctionRegistry } from '../core/index.js';
|
|
2
|
+
export { alphabetical, type IAlphabeticalOptions } from './alphabetical.js';
|
|
3
|
+
export { type CasingType, casing, type ICasingOptions } from './casing.js';
|
|
4
|
+
export { defined } from './defined.js';
|
|
5
|
+
export { enumeration } from './enumeration.js';
|
|
6
|
+
export { falsy } from './falsy.js';
|
|
7
|
+
export { length } from './length.js';
|
|
8
|
+
export { pattern } from './pattern.js';
|
|
9
|
+
export { type ISchemaOptions, schema } from './schema.js';
|
|
10
|
+
export { truthy } from './truthy.js';
|
|
11
|
+
export { typedEnum } from './typed-enum.js';
|
|
12
|
+
export { undefinedFn } from './undefined.js';
|
|
13
|
+
export { type IUnreferencedReusableObjectOptions, unreferencedReusableObject } from './unreferenced-reusable-object.js';
|
|
14
|
+
export { type IXorOptions, xor } from './xor.js';
|
|
15
|
+
/** All built-in functions, keyed by their Linter-compatible names. */
|
|
16
|
+
export declare const builtinFunctions: FunctionRegistry;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { alphabetical } from './alphabetical.js';
|
|
2
|
+
import { casing } from './casing.js';
|
|
3
|
+
import { defined } from './defined.js';
|
|
4
|
+
import { enumeration } from './enumeration.js';
|
|
5
|
+
import { falsy } from './falsy.js';
|
|
6
|
+
import { length } from './length.js';
|
|
7
|
+
import { pattern } from './pattern.js';
|
|
8
|
+
import { schema } from './schema.js';
|
|
9
|
+
import { truthy } from './truthy.js';
|
|
10
|
+
import { typedEnum } from './typed-enum.js';
|
|
11
|
+
import { undefinedFn } from './undefined.js';
|
|
12
|
+
import { unreferencedReusableObject } from './unreferenced-reusable-object.js';
|
|
13
|
+
import { xor } from './xor.js';
|
|
14
|
+
export { alphabetical } from './alphabetical.js';
|
|
15
|
+
export { casing } from './casing.js';
|
|
16
|
+
export { defined } from './defined.js';
|
|
17
|
+
export { enumeration } from './enumeration.js';
|
|
18
|
+
export { falsy } from './falsy.js';
|
|
19
|
+
export { length } from './length.js';
|
|
20
|
+
export { pattern } from './pattern.js';
|
|
21
|
+
export { schema } from './schema.js';
|
|
22
|
+
export { truthy } from './truthy.js';
|
|
23
|
+
export { typedEnum } from './typed-enum.js';
|
|
24
|
+
export { undefinedFn } from './undefined.js';
|
|
25
|
+
export { unreferencedReusableObject } from './unreferenced-reusable-object.js';
|
|
26
|
+
export { xor } from './xor.js';
|
|
27
|
+
/** All built-in functions, keyed by their Linter-compatible names. */
|
|
28
|
+
export const builtinFunctions = {
|
|
29
|
+
alphabetical: alphabetical,
|
|
30
|
+
casing: casing,
|
|
31
|
+
defined,
|
|
32
|
+
enumeration: enumeration,
|
|
33
|
+
falsy,
|
|
34
|
+
length: length,
|
|
35
|
+
pattern: pattern,
|
|
36
|
+
schema: schema,
|
|
37
|
+
truthy,
|
|
38
|
+
undefined: undefinedFn,
|
|
39
|
+
unreferencedReusableObject: unreferencedReusableObject,
|
|
40
|
+
xor: xor,
|
|
41
|
+
typedEnum: typedEnum,
|
|
42
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the comparable size of a value: string/array length, object key count,
|
|
3
|
+
* or the number itself. `undefined` for anything else (so the rule is skipped).
|
|
4
|
+
*/
|
|
5
|
+
const measure = (input) => {
|
|
6
|
+
if (typeof input === 'string' || Array.isArray(input))
|
|
7
|
+
return input.length;
|
|
8
|
+
if (typeof input === 'object' && input !== null)
|
|
9
|
+
return Object.keys(input).length;
|
|
10
|
+
if (typeof input === 'number')
|
|
11
|
+
return input;
|
|
12
|
+
return undefined;
|
|
13
|
+
};
|
|
14
|
+
/** Flags a value whose size falls outside the `min`/`max` bounds. */
|
|
15
|
+
export const length = (input, options) => {
|
|
16
|
+
const size = measure(input);
|
|
17
|
+
if (size === undefined)
|
|
18
|
+
return [];
|
|
19
|
+
const results = [];
|
|
20
|
+
if (options?.min !== undefined && size < options.min) {
|
|
21
|
+
results.push({ message: `The value must not be shorter than ${options.min}` });
|
|
22
|
+
}
|
|
23
|
+
if (options?.max !== undefined && size > options.max) {
|
|
24
|
+
results.push({ message: `The value must not be longer than ${options.max}` });
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Parses a `/pattern/flags` string (or a bare pattern) into a RegExp. */
|
|
2
|
+
const toRegExp = (pattern) => {
|
|
3
|
+
const match = /^\/(.+)\/([a-z]*)$/s.exec(pattern);
|
|
4
|
+
if (match)
|
|
5
|
+
return new RegExp(match[1], match[2]);
|
|
6
|
+
return new RegExp(pattern);
|
|
7
|
+
};
|
|
8
|
+
/** Flags a string that fails `match` or satisfies `notMatch`. */
|
|
9
|
+
export const pattern = (input, options) => {
|
|
10
|
+
if (typeof input !== 'string')
|
|
11
|
+
return [];
|
|
12
|
+
const results = [];
|
|
13
|
+
if (options?.match !== undefined && !toRegExp(options.match).test(input)) {
|
|
14
|
+
results.push({ message: `The value must match the pattern "${options.match}"` });
|
|
15
|
+
}
|
|
16
|
+
if (options?.notMatch !== undefined && toRegExp(options.notMatch).test(input)) {
|
|
17
|
+
results.push({ message: `The value must not match the pattern "${options.notMatch}"` });
|
|
18
|
+
}
|
|
19
|
+
return results;
|
|
20
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../core/index.js';
|
|
2
|
+
/** Options for {@link schema}. */
|
|
3
|
+
export type ISchemaOptions = {
|
|
4
|
+
schema: object;
|
|
5
|
+
allErrors?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** Validates a value against a JSON Schema supplied in the rule's options. */
|
|
8
|
+
export declare const schema: RulesetFunction<unknown, ISchemaOptions>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { validate as buildValidator } from '@amritk/runtime-validators';
|
|
2
|
+
// The rule's `options.schema` is a stable object, so cache the prepared
|
|
3
|
+
// validator by identity. `@amritk/runtime-validators` interprets the schema at
|
|
4
|
+
// runtime (no `new Function`), keeping this eval-free and dependency-light.
|
|
5
|
+
const validators = new WeakMap();
|
|
6
|
+
const getValidator = (schema) => {
|
|
7
|
+
let validator = validators.get(schema);
|
|
8
|
+
if (!validator) {
|
|
9
|
+
// The `schema` built-in historically enforced string formats (ajv
|
|
10
|
+
// `validateFormats: true`); keep that by opting into all formats here.
|
|
11
|
+
validator = buildValidator(schema, { formats: 'all' });
|
|
12
|
+
validators.set(schema, validator);
|
|
13
|
+
}
|
|
14
|
+
return validator;
|
|
15
|
+
};
|
|
16
|
+
const pointerToPath = (pointer) => pointer
|
|
17
|
+
.split('/')
|
|
18
|
+
.slice(1)
|
|
19
|
+
.map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
20
|
+
.map((segment) => (/^\d+$/.test(segment) ? Number(segment) : segment));
|
|
21
|
+
const formatError = (error) => {
|
|
22
|
+
const location = error.path || 'value';
|
|
23
|
+
return `${location} ${error.message}`.trim();
|
|
24
|
+
};
|
|
25
|
+
/** Validates a value against a JSON Schema supplied in the rule's options. */
|
|
26
|
+
export const schema = (input, options, context) => {
|
|
27
|
+
if (!options?.schema)
|
|
28
|
+
return [];
|
|
29
|
+
const result = getValidator(options.schema)(input);
|
|
30
|
+
if (result === true)
|
|
31
|
+
return [];
|
|
32
|
+
return result.errors.map((error) => ({
|
|
33
|
+
message: formatError(error),
|
|
34
|
+
path: [...context.path, ...pointerToPath(error.path)],
|
|
35
|
+
}));
|
|
36
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const JS_TYPES = {
|
|
2
|
+
string: (v) => typeof v === 'string',
|
|
3
|
+
number: (v) => typeof v === 'number',
|
|
4
|
+
integer: (v) => typeof v === 'number' && Number.isInteger(v),
|
|
5
|
+
boolean: (v) => typeof v === 'boolean',
|
|
6
|
+
null: (v) => v === null,
|
|
7
|
+
array: (v) => Array.isArray(v),
|
|
8
|
+
object: (v) => typeof v === 'object' && v !== null && !Array.isArray(v),
|
|
9
|
+
};
|
|
10
|
+
/** Validates that each `enum` entry matches the schema's declared `type`. */
|
|
11
|
+
export const typedEnum = (input, _options, context) => {
|
|
12
|
+
if (typeof input !== 'object' || input === null)
|
|
13
|
+
return [];
|
|
14
|
+
const declaredType = input['type'];
|
|
15
|
+
const values = input['enum'];
|
|
16
|
+
if (declaredType === undefined || !Array.isArray(values))
|
|
17
|
+
return [];
|
|
18
|
+
const types = Array.isArray(declaredType) ? declaredType : [declaredType];
|
|
19
|
+
const checkers = types
|
|
20
|
+
.map((type) => JS_TYPES[String(type)])
|
|
21
|
+
.filter((fn) => Boolean(fn));
|
|
22
|
+
if (checkers.length === 0)
|
|
23
|
+
return [];
|
|
24
|
+
const results = [];
|
|
25
|
+
values.forEach((value, index) => {
|
|
26
|
+
if (!checkers.some((check) => check(value))) {
|
|
27
|
+
results.push({
|
|
28
|
+
message: `Enum value \`${JSON.stringify(value)}\` must be of type "${types.join(' | ')}"`,
|
|
29
|
+
path: [...context.path, 'enum', index],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return results;
|
|
34
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../core/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Flags a value that is defined. Exported as `undefinedFn` because `undefined`
|
|
4
|
+
* is a reserved identifier; it is registered under the name `undefined`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const undefinedFn: RulesetFunction;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flags a value that is defined. Exported as `undefinedFn` because `undefined`
|
|
3
|
+
* is a reserved identifier; it is registered under the name `undefined`.
|
|
4
|
+
*/
|
|
5
|
+
export const undefinedFn = (input) => {
|
|
6
|
+
if (input !== undefined)
|
|
7
|
+
return [{ message: 'The value must be undefined' }];
|
|
8
|
+
return [];
|
|
9
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../core/index.js';
|
|
2
|
+
/** Options for {@link unreferencedReusableObject}. */
|
|
3
|
+
export type IUnreferencedReusableObjectOptions = {
|
|
4
|
+
/** JSON pointer to the map of reusable objects, e.g. "#/components/schemas". */
|
|
5
|
+
reusableObjectsLocation: string;
|
|
6
|
+
};
|
|
7
|
+
/** Flags entries in a reusable-object map that nothing `$ref`s. */
|
|
8
|
+
export declare const unreferencedReusableObject: RulesetFunction<Record<string, unknown>, IUnreferencedReusableObjectOptions>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Collects every `$ref` string anywhere in `node` into `into`. */
|
|
2
|
+
const collectRefs = (node, into) => {
|
|
3
|
+
if (Array.isArray(node)) {
|
|
4
|
+
for (const item of node)
|
|
5
|
+
collectRefs(item, into);
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (typeof node === 'object' && node !== null) {
|
|
9
|
+
for (const [key, value] of Object.entries(node)) {
|
|
10
|
+
if (key === '$ref' && typeof value === 'string')
|
|
11
|
+
into.add(value);
|
|
12
|
+
else
|
|
13
|
+
collectRefs(value, into);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/** Flags entries in a reusable-object map that nothing `$ref`s. */
|
|
18
|
+
export const unreferencedReusableObject = (input, options, context) => {
|
|
19
|
+
if (typeof input !== 'object' || input === null)
|
|
20
|
+
return [];
|
|
21
|
+
const location = options?.reusableObjectsLocation;
|
|
22
|
+
if (!location)
|
|
23
|
+
return [];
|
|
24
|
+
const refs = new Set();
|
|
25
|
+
collectRefs(context.document.data, refs);
|
|
26
|
+
const results = [];
|
|
27
|
+
for (const key of Object.keys(input)) {
|
|
28
|
+
const pointer = `${location}/${key}`;
|
|
29
|
+
if (!refs.has(pointer)) {
|
|
30
|
+
results.push({
|
|
31
|
+
message: 'This reusable object is never referenced',
|
|
32
|
+
path: [...context.path, key],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../core/index.js';
|
|
2
|
+
/** Options for {@link xor}. */
|
|
3
|
+
export type IXorOptions = {
|
|
4
|
+
properties: string[];
|
|
5
|
+
};
|
|
6
|
+
/** Flags an object unless exactly one of the listed `properties` is present. */
|
|
7
|
+
export declare const xor: RulesetFunction<Record<string, unknown>, IXorOptions>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Flags an object unless exactly one of the listed `properties` is present. */
|
|
2
|
+
export const xor = (input, options) => {
|
|
3
|
+
if (typeof input !== 'object' || input === null)
|
|
4
|
+
return [];
|
|
5
|
+
const properties = options?.properties ?? [];
|
|
6
|
+
const present = properties.filter((property) => property in input);
|
|
7
|
+
if (present.length !== 1) {
|
|
8
|
+
return [{ message: `Exactly one of ${properties.map((p) => `"${p}"`).join(', ')} must be defined` }];
|
|
9
|
+
}
|
|
10
|
+
return [];
|
|
11
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type IDiagnostic, type IDocumentOptions, type LintPlugin, type LintResolver, type ResolvedExtend, type Ruleset, type RulesetDefinition } from './core/index.js';
|
|
2
|
+
import { type AppliedFix, type FixerRegistry } from './fix/index.js';
|
|
3
|
+
export * from './core/index.js';
|
|
4
|
+
export * from './fix/index.js';
|
|
5
|
+
export * from './functions/index.js';
|
|
6
|
+
export { detectFormat, parseWithPointers } from './parsers/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Resolves an `extends` reference to a ruleset definition. Supports:
|
|
9
|
+
* - local file paths (relative to `basePath`, or absolute): `.yaml` / `.yml` / `.json` / `.js`,
|
|
10
|
+
* - npm package specifiers (resolved from `basePath`), including subpaths.
|
|
11
|
+
*
|
|
12
|
+
* The engine ships no named built-in rulesets, so every string `extends` target
|
|
13
|
+
* is a file path or an npm package.
|
|
14
|
+
*/
|
|
15
|
+
export declare const resolveNamedRuleset: (name: string, basePath?: string) => ResolvedExtend;
|
|
16
|
+
/**
|
|
17
|
+
* Builds a runnable {@link Ruleset} from a ruleset definition, layering the
|
|
18
|
+
* built-in functions (plus any custom ones the definition declares via
|
|
19
|
+
* `functions` / `functionsDir`) over the core engine and wiring up `extends`
|
|
20
|
+
* resolution against files and npm packages. With no definition it produces an
|
|
21
|
+
* empty ruleset (no rules run).
|
|
22
|
+
*/
|
|
23
|
+
export declare const createRuleset: (definition?: RulesetDefinition, basePath?: string) => Ruleset;
|
|
24
|
+
/** Options for {@link lintDocument}: the document options plus ruleset controls. */
|
|
25
|
+
export type ILintOptions = IDocumentOptions & {
|
|
26
|
+
/** The ruleset definition to evaluate. When omitted, no rules run. */
|
|
27
|
+
ruleset?: RulesetDefinition;
|
|
28
|
+
/** Directory that the ruleset's string `extends` references resolve relative to. */
|
|
29
|
+
rulesetBasePath?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Produces the resolved (`$ref`-dereferenced) tree for rules with
|
|
32
|
+
* `resolved: true`. The engine ships no resolver; pass one (for example
|
|
33
|
+
* wrapping `@amritk/resolve-refs`) to enable `$ref` dereferencing. When
|
|
34
|
+
* omitted, rules see the raw parsed document.
|
|
35
|
+
*/
|
|
36
|
+
resolve?: LintResolver;
|
|
37
|
+
/**
|
|
38
|
+
* Plugins run after the rule pass. Use {@link lintDocumentWithResult} (not
|
|
39
|
+
* {@link lintDocument}) to read what they return — e.g. the auto-fix plugin's
|
|
40
|
+
* rewritten `output`.
|
|
41
|
+
*/
|
|
42
|
+
plugins?: LintPlugin[];
|
|
43
|
+
};
|
|
44
|
+
/** The full result of a lint run: findings plus anything the plugins produced. */
|
|
45
|
+
export type ILintResult = {
|
|
46
|
+
diagnostics: IDiagnostic[];
|
|
47
|
+
/** A rewritten document, when a plugin (e.g. auto-fix) produced one. */
|
|
48
|
+
output?: string;
|
|
49
|
+
/** Per-plugin structured output, keyed by plugin name. */
|
|
50
|
+
pluginData: Record<string, unknown>;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Lints a JSON/YAML `input` end to end: parses with source maps and applies the
|
|
54
|
+
* ruleset. Returns just the findings; use {@link lintDocumentWithResult} for the
|
|
55
|
+
* full result.
|
|
56
|
+
*/
|
|
57
|
+
export declare const lintDocument: (input: string, options?: ILintOptions) => Promise<IDiagnostic[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Like {@link lintDocument}, but returns the full {@link ILintResult} — including
|
|
60
|
+
* anything the configured `plugins` produced (e.g. the auto-fix plugin's
|
|
61
|
+
* rewritten `output`).
|
|
62
|
+
*/
|
|
63
|
+
export declare const lintDocumentWithResult: (input: string, options?: ILintOptions) => Promise<ILintResult>;
|
|
64
|
+
/** Options for {@link fixDocument}: the lint options plus auto-fix controls. */
|
|
65
|
+
export type IFixOptions = Omit<ILintOptions, 'plugins'> & {
|
|
66
|
+
/** The fixers to apply, keyed by rule code. Defaults to an empty registry (no fixes). */
|
|
67
|
+
fixers?: FixerRegistry;
|
|
68
|
+
/** When false, also apply fixers marked `safe: false`. Defaults to true. */
|
|
69
|
+
safeOnly?: boolean;
|
|
70
|
+
};
|
|
71
|
+
/** The result of {@link fixDocument}. */
|
|
72
|
+
export type IFixResult = {
|
|
73
|
+
/** The fixed document text (identical to the input when nothing was fixed). */
|
|
74
|
+
output: string;
|
|
75
|
+
/** Whether any fix changed the document. */
|
|
76
|
+
fixed: boolean;
|
|
77
|
+
/** The findings that were repaired, across every fix pass. */
|
|
78
|
+
applied: AppliedFix[];
|
|
79
|
+
/** Findings that remain after fixing, re-linted against the fixed document. */
|
|
80
|
+
remaining: IDiagnostic[];
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Lints a document and applies the supplied `fixers` repeatedly until the
|
|
84
|
+
* document stops changing (or {@link MAX_FIX_PASSES} is reached), then re-lints
|
|
85
|
+
* so `remaining` reflects the fixed document. A one-call convenience over
|
|
86
|
+
* {@link lintDocumentWithResult} + `createFixPlugin`. With no `fixers` this is a
|
|
87
|
+
* no-op that just returns the findings.
|
|
88
|
+
*/
|
|
89
|
+
export declare const fixDocument: (input: string, options?: IFixOptions) => Promise<IFixResult>;
|