@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.
- package/AI.md +73 -4
- package/README.md +65 -14
- package/dist/core/filter-expression.js +17 -4
- package/dist/core/filter.js +82 -35
- package/dist/core/glob.js +25 -31
- package/dist/core/jsonpath.d.ts +9 -1
- package/dist/core/jsonpath.js +74 -16
- package/dist/core/lint.d.ts +8 -1
- package/dist/core/lint.js +4 -18
- package/dist/core/order.d.ts +30 -0
- package/dist/core/order.js +36 -0
- package/dist/core/own-key.d.ts +18 -0
- package/dist/core/own-key.js +12 -0
- package/dist/core/pointers.d.ts +3 -2
- package/dist/core/pointers.js +27 -18
- package/dist/core/ruleset.js +74 -27
- package/dist/core/runner.js +16 -23
- package/dist/core/severity.d.ts +11 -0
- package/dist/core/severity.js +13 -0
- package/dist/fix/apply.js +9 -2
- package/dist/fix/plugin.js +2 -1
- package/dist/functions/alphabetical.d.ts +14 -0
- package/dist/functions/alphabetical.js +4 -3
- package/dist/functions/or.js +1 -1
- package/dist/functions/ref-index.d.ts +15 -0
- package/dist/functions/ref-index.js +33 -0
- package/dist/functions/schema.d.ts +15 -0
- package/dist/functions/schema.js +2 -0
- package/dist/functions/typed-enum.js +2 -1
- package/dist/functions/unreferenced-reusable-object.js +3 -20
- package/dist/functions/xor.js +1 -1
- package/dist/index.d.ts +16 -16
- package/dist/index.js +25 -94
- package/dist/rules/asyncapi/asyncapi.d.ts +3 -0
- package/dist/rules/asyncapi/asyncapi.js +563 -0
- package/dist/rules/asyncapi/formats.d.ts +23 -0
- package/dist/rules/asyncapi/formats.js +49 -0
- package/dist/rules/asyncapi/functions/aas-server-variables.d.ts +8 -0
- package/dist/rules/asyncapi/functions/aas-server-variables.js +5 -0
- package/dist/rules/asyncapi/functions/aas-tags-unique.d.ts +2 -0
- package/dist/rules/asyncapi/functions/aas-tags-unique.js +5 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.d.ts +7 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.js +46 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-servers.d.ts +27 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-servers.js +61 -0
- package/dist/rules/asyncapi/functions/asyncapi-document-schema.d.ts +17 -0
- package/dist/rules/asyncapi/functions/asyncapi-document-schema.js +12 -0
- package/dist/rules/asyncapi/functions/asyncapi-headers-object.d.ts +23 -0
- package/dist/rules/asyncapi/functions/asyncapi-headers-object.js +25 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-examples.d.ts +8 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-examples.js +42 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.d.ts +3 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.js +30 -0
- package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.d.ts +3 -0
- package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.js +30 -0
- package/dist/rules/asyncapi/functions/asyncapi-payload.d.ts +14 -0
- package/dist/rules/asyncapi/functions/asyncapi-payload.js +32 -0
- package/dist/rules/asyncapi/functions/asyncapi-schema-validation.d.ts +11 -0
- package/dist/rules/asyncapi/functions/asyncapi-schema-validation.js +17 -0
- package/dist/rules/asyncapi/functions/asyncapi-security.d.ts +22 -0
- package/dist/rules/asyncapi/functions/asyncapi-security.js +78 -0
- package/dist/rules/asyncapi/functions/helpers.d.ts +51 -0
- package/dist/rules/asyncapi/functions/helpers.js +91 -0
- package/dist/rules/asyncapi/functions/index.d.ts +15 -0
- package/dist/rules/asyncapi/functions/index.js +53 -0
- package/dist/rules/asyncapi/functions/pointer.d.ts +11 -0
- package/dist/rules/asyncapi/functions/pointer.js +11 -0
- package/dist/rules/asyncapi/functions/schema-format.d.ts +4 -0
- package/dist/rules/asyncapi/functions/schema-format.js +7 -0
- package/dist/rules/asyncapi/index.d.ts +33 -0
- package/dist/rules/asyncapi/index.js +60 -0
- package/dist/rules/asyncapi/schemas/aas20.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas20.js +4 -0
- package/dist/rules/asyncapi/schemas/aas21.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas21.js +4 -0
- package/dist/rules/asyncapi/schemas/aas22.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas22.js +4 -0
- package/dist/rules/asyncapi/schemas/aas23.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas23.js +4 -0
- package/dist/rules/asyncapi/schemas/aas24.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas24.js +4 -0
- package/dist/rules/asyncapi/schemas/aas25.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas25.js +4 -0
- package/dist/rules/asyncapi/schemas/aas26.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas26.js +4 -0
- package/dist/rules/asyncapi/schemas/aas30.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas30.js +4 -0
- package/dist/rules/asyncapi/schemas/index.d.ts +20 -0
- package/dist/rules/asyncapi/schemas/index.js +45 -0
- package/dist/rules/openapi/fixers.d.ts +6 -2
- package/dist/rules/openapi/fixers.js +9 -6
- package/dist/rules/openapi/functions/oas-discriminator.js +1 -1
- package/dist/rules/openapi/functions/oas-mutually-exclusive.js +1 -1
- package/dist/rules/openapi/functions/oas-op-security-defined.js +1 -1
- package/dist/rules/openapi/functions/oas-path-param.js +5 -4
- package/dist/rules/openapi/functions/oas-server-variables.d.ts +6 -6
- package/dist/rules/openapi/functions/oas-server-variables.js +2 -45
- package/dist/rules/openapi/functions/oas-tags-unique.d.ts +2 -3
- package/dist/rules/openapi/functions/oas-tags-unique.js +2 -16
- package/dist/rules/openapi/functions/oas-unused-component.js +3 -25
- package/dist/rules/openapi/index.d.ts +9 -2
- package/dist/rules/openapi/index.js +15 -70
- package/dist/rules/openapi/schemas/index.js +5 -1
- package/dist/rules/shared/helpers.d.ts +2 -0
- package/dist/rules/shared/helpers.js +4 -0
- package/dist/rules/shared/server-variables.d.ts +27 -0
- package/dist/rules/shared/server-variables.js +55 -0
- package/dist/rules/shared/tags-unique.d.ts +7 -0
- package/dist/rules/shared/tags-unique.js +19 -0
- package/dist/ruleset-files.d.ts +47 -0
- package/dist/ruleset-files.js +92 -0
- package/package.json +9 -5
package/dist/core/runner.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { detectFormats } from "./formats.js";
|
|
2
2
|
import { matchesGlob } from "./glob.js";
|
|
3
3
|
import { compileQuery, query, queryMany } from "./jsonpath.js";
|
|
4
|
+
import { bySourceThenPosition } from "./order.js";
|
|
5
|
+
import { ownKey } from "./own-key.js";
|
|
4
6
|
import { pointerToPath, resolveSourcePath } from "./pointers.js";
|
|
7
|
+
import { severityFromName } from "./severity.js";
|
|
5
8
|
import { DiagnosticSeverity } from "./types.js";
|
|
6
|
-
const SEVERITY_NAMES = {
|
|
7
|
-
error: DiagnosticSeverity.Error,
|
|
8
|
-
warn: DiagnosticSeverity.Warning,
|
|
9
|
-
info: DiagnosticSeverity.Information,
|
|
10
|
-
hint: DiagnosticSeverity.Hint
|
|
11
|
-
};
|
|
12
9
|
const ZERO_RANGE = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
|
|
13
10
|
const isIndexable = (value) => typeof value === "object" && value !== null;
|
|
14
11
|
const isThenable = (value) => typeof value?.then === "function";
|
|
@@ -27,7 +24,8 @@ const resolveTargets = (value, path, field) => {
|
|
|
27
24
|
return query(value, field).map((match) => ({ value: match.value, path: [...path, ...match.path] }));
|
|
28
25
|
}
|
|
29
26
|
const key = Array.isArray(value) && /^\d+$/.test(field) ? Number(field) : field;
|
|
30
|
-
|
|
27
|
+
const own = Object.hasOwn(value, key) ? value[key] : void 0;
|
|
28
|
+
return [{ value: own, path: [...path, key] }];
|
|
31
29
|
};
|
|
32
30
|
const stringify = (value) => {
|
|
33
31
|
if (value === void 0)
|
|
@@ -38,7 +36,7 @@ const stringify = (value) => {
|
|
|
38
36
|
};
|
|
39
37
|
const applyTemplate = (template, ctx) => template.replace(/\{\{([^}]+)\}\}/g, (_match, raw) => {
|
|
40
38
|
const key = raw.trim();
|
|
41
|
-
return key
|
|
39
|
+
return Object.hasOwn(ctx, key) ? stringify(ctx[key]) : "";
|
|
42
40
|
});
|
|
43
41
|
const locate = (rule, path, document, sources) => {
|
|
44
42
|
let originDocument = document;
|
|
@@ -51,7 +49,7 @@ const locate = (rule, path, document, sources) => {
|
|
|
51
49
|
sourcePath = rule.resolved ? resolveSourcePath(document.data, path) : path;
|
|
52
50
|
}
|
|
53
51
|
const location = originDocument.getLocationForJsonPath(sourcePath, true);
|
|
54
|
-
return { range: location?.range ?? ZERO_RANGE, source: originDocument.source };
|
|
52
|
+
return { sourcePath, range: location?.range ?? ZERO_RANGE, source: originDocument.source };
|
|
55
53
|
};
|
|
56
54
|
const errorDiagnostic = (rule, path, message, range, source) => {
|
|
57
55
|
const diagnostic = {
|
|
@@ -135,7 +133,7 @@ const applyScopedOverrides = (ruleset, source, diagnostics) => {
|
|
|
135
133
|
for (const { path, rules } of scoped) {
|
|
136
134
|
if (!isPrefix(path, diagnostic.path))
|
|
137
135
|
continue;
|
|
138
|
-
const entry = rules
|
|
136
|
+
const entry = ownKey(rules, String(diagnostic.code));
|
|
139
137
|
if (entry === void 0)
|
|
140
138
|
continue;
|
|
141
139
|
if (entry === false || entry === "off") {
|
|
@@ -144,8 +142,10 @@ const applyScopedOverrides = (ruleset, source, diagnostics) => {
|
|
|
144
142
|
}
|
|
145
143
|
if (typeof entry === "number")
|
|
146
144
|
diagnostic.severity = entry;
|
|
147
|
-
else if (typeof entry === "string"
|
|
148
|
-
|
|
145
|
+
else if (typeof entry === "string") {
|
|
146
|
+
const mapped = severityFromName(entry);
|
|
147
|
+
if (typeof mapped === "number")
|
|
148
|
+
diagnostic.severity = mapped;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
if (!dropped)
|
|
@@ -171,9 +171,9 @@ const runThen = async (ruleset, rule, then, value, path, document, out, sources)
|
|
|
171
171
|
});
|
|
172
172
|
results = isThenable(raw) ? await raw : raw;
|
|
173
173
|
} catch (error) {
|
|
174
|
-
const { range, source } = locate(rule, target.path, document, sources);
|
|
174
|
+
const { sourcePath, range, source } = locate(rule, target.path, document, sources);
|
|
175
175
|
const reason = error instanceof Error ? error.message : String(error);
|
|
176
|
-
out.push(errorDiagnostic(rule,
|
|
176
|
+
out.push(errorDiagnostic(rule, sourcePath, `Rule "${rule.name}" threw: ${reason}`, range, source));
|
|
177
177
|
continue;
|
|
178
178
|
}
|
|
179
179
|
if (!results)
|
|
@@ -193,11 +193,11 @@ const toDiagnostic = (rule, result, target, document, sources) => {
|
|
|
193
193
|
error: result.message,
|
|
194
194
|
description: rule.description ?? ""
|
|
195
195
|
}) : result.message;
|
|
196
|
-
const { range, source } = locate(rule, path, document, sources);
|
|
196
|
+
const { sourcePath, range, source } = locate(rule, path, document, sources);
|
|
197
197
|
const diagnostic = {
|
|
198
198
|
code: rule.name,
|
|
199
199
|
message,
|
|
200
|
-
path,
|
|
200
|
+
path: sourcePath,
|
|
201
201
|
severity: rule.severity,
|
|
202
202
|
range
|
|
203
203
|
};
|
|
@@ -212,13 +212,6 @@ const hasIntersection = (a, b) => {
|
|
|
212
212
|
}
|
|
213
213
|
return false;
|
|
214
214
|
};
|
|
215
|
-
const bySourceThenPosition = (a, b) => {
|
|
216
|
-
const sa = a.source ?? "";
|
|
217
|
-
const sb = b.source ?? "";
|
|
218
|
-
if (sa !== sb)
|
|
219
|
-
return sa < sb ? -1 : 1;
|
|
220
|
-
return a.range.start.line - b.range.start.line || a.range.start.character - b.range.start.character;
|
|
221
|
-
};
|
|
222
215
|
const createLinter = (ruleset) => ({
|
|
223
216
|
run: async (document, options = {}) => {
|
|
224
217
|
const documentFormats = detectFormats(document.data, ruleset.formats);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DiagnosticSeverity } from '../parsers/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a severity name, or `undefined` when it is not one we know.
|
|
4
|
+
*
|
|
5
|
+
* `ownKey`, never a bare index or `in`: these names come from rulesets, so
|
|
6
|
+
* `severity: 'constructor'` otherwise resolved to `Object` itself — and a
|
|
7
|
+
* severity that is a function is neither `'off'` nor `undefined`, so it sailed
|
|
8
|
+
* past both fallbacks and every later comparison against
|
|
9
|
+
* `DiagnosticSeverity.Error` read false.
|
|
10
|
+
*/
|
|
11
|
+
export declare const severityFromName: (name: string) => DiagnosticSeverity | 'off' | undefined;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DiagnosticSeverity } from "../parsers/index.js";
|
|
2
|
+
import { ownKey } from "./own-key.js";
|
|
3
|
+
const SEVERITY_NAMES = {
|
|
4
|
+
error: DiagnosticSeverity.Error,
|
|
5
|
+
warn: DiagnosticSeverity.Warning,
|
|
6
|
+
info: DiagnosticSeverity.Information,
|
|
7
|
+
hint: DiagnosticSeverity.Hint,
|
|
8
|
+
off: "off"
|
|
9
|
+
};
|
|
10
|
+
const severityFromName = (name) => ownKey(SEVERITY_NAMES, name);
|
|
11
|
+
export {
|
|
12
|
+
severityFromName
|
|
13
|
+
};
|
package/dist/fix/apply.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ownKey } from "../core/own-key.js";
|
|
1
2
|
import { applyEditOpsWithChanges } from "../parsers/index.js";
|
|
2
3
|
const STRUCTURAL_ARRAY_OPS = /* @__PURE__ */ new Set(["removeItems", "reorderArray", "insertItem"]);
|
|
3
4
|
const isStructuralArrayOp = (op) => STRUCTURAL_ARRAY_OPS.has(op.op);
|
|
@@ -12,12 +13,18 @@ const applyFixes = (input, format, data, diagnostics, fixers, options = {}) => {
|
|
|
12
13
|
const safeOnly = options.safeOnly !== false;
|
|
13
14
|
const candidates = [];
|
|
14
15
|
for (const diagnostic of diagnostics) {
|
|
15
|
-
const
|
|
16
|
+
const code = String(diagnostic.code);
|
|
17
|
+
const fixer = ownKey(fixers, code);
|
|
16
18
|
if (!fixer)
|
|
17
19
|
continue;
|
|
18
20
|
if (safeOnly && fixer.safe === false)
|
|
19
21
|
continue;
|
|
20
|
-
|
|
22
|
+
let produced;
|
|
23
|
+
try {
|
|
24
|
+
produced = fixer.fix({ diagnostic, data, format });
|
|
25
|
+
} catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
21
28
|
if (!produced)
|
|
22
29
|
continue;
|
|
23
30
|
const ops2 = Array.isArray(produced) ? produced : [produced];
|
package/dist/fix/plugin.js
CHANGED
|
@@ -3,7 +3,8 @@ const FIX_PLUGIN_NAME = "fix";
|
|
|
3
3
|
const createFixPlugin = (fixers, options = {}) => ({
|
|
4
4
|
name: FIX_PLUGIN_NAME,
|
|
5
5
|
afterLint: (diagnostics, context) => {
|
|
6
|
-
const
|
|
6
|
+
const own = diagnostics.filter((diagnostic) => diagnostic.source === context.source);
|
|
7
|
+
const result = applyFixes(context.input, context.format, context.document.data, own, fixers, options);
|
|
7
8
|
if (!result.changed)
|
|
8
9
|
return void 0;
|
|
9
10
|
const data = { applied: result.applied };
|
|
@@ -4,5 +4,19 @@ export type IAlphabeticalOptions = {
|
|
|
4
4
|
/** Compare objects by this property instead of the value itself. */
|
|
5
5
|
keyedBy?: string;
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* Exported so the `openapi-tags-alphabetical` fixer sorts by the very function
|
|
9
|
+
* the rule judges with. A second comparator that merely looked equivalent
|
|
10
|
+
* drifted: it missed the numeric-string cases below, so `["10", "2"]` was
|
|
11
|
+
* flagged by the rule and left untouched by the fixer, forever.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately **not** a total order. Numeric-vs-textual is decided per pair,
|
|
14
|
+
* which is what keeps both `["2", "10"]` and `["0x10", "9"]` reading as
|
|
15
|
+
* ordered — two requirements no single total order can satisfy, since
|
|
16
|
+
* lexicographically `"1e2"` falls between `"10"` and `"2"` while numerically
|
|
17
|
+
* `"2"` precedes `"10"`. A caller that feeds this to `Array.prototype.sort`
|
|
18
|
+
* therefore has to check the result rather than trust it; see the tags fixer.
|
|
19
|
+
*/
|
|
20
|
+
export declare const compareAlphabetically: (a: unknown, b: unknown) => number;
|
|
7
21
|
/** Flags array items or object keys that are not in ascending (optionally `keyedBy`) order. */
|
|
8
22
|
export declare const alphabetical: RulesetFunction<unknown, IAlphabeticalOptions>;
|
|
@@ -3,7 +3,7 @@ const isStringOrNumber = (value) => typeof value === "string" || typeof value ==
|
|
|
3
3
|
const isIntegerLike = (value) => typeof value === "string" && /^(?:0|[1-9]\d*)$/.test(value);
|
|
4
4
|
const DECIMAL_NUMBER = /^-?\d+(?:\.\d+)?$/;
|
|
5
5
|
const isNumeric = (value) => typeof value === "number" || typeof value === "string" && DECIMAL_NUMBER.test(value);
|
|
6
|
-
const
|
|
6
|
+
const compareAlphabetically = (a, b) => {
|
|
7
7
|
if (isIntegerLike(a) && isIntegerLike(b))
|
|
8
8
|
return Math.sign(Number(a) - Number(b));
|
|
9
9
|
if (isNumeric(a) && isNumeric(b)) {
|
|
@@ -34,7 +34,7 @@ const alphabetical = (input, options, context) => {
|
|
|
34
34
|
}
|
|
35
35
|
const results = [];
|
|
36
36
|
for (let i = 0; i < items.length - 1; i++) {
|
|
37
|
-
if (
|
|
37
|
+
if (compareAlphabetically(items[i], items[i + 1]) > 0) {
|
|
38
38
|
const path = isArray ? [...context.path, i + 1] : [...context.path, rawItems[i + 1]];
|
|
39
39
|
results.push({ message: "The items must be in alphabetical order", path });
|
|
40
40
|
}
|
|
@@ -42,5 +42,6 @@ const alphabetical = (input, options, context) => {
|
|
|
42
42
|
return results;
|
|
43
43
|
};
|
|
44
44
|
export {
|
|
45
|
-
alphabetical
|
|
45
|
+
alphabetical,
|
|
46
|
+
compareAlphabetically
|
|
46
47
|
};
|
package/dist/functions/or.js
CHANGED
|
@@ -4,7 +4,7 @@ const or = (input, options) => {
|
|
|
4
4
|
const properties = options?.properties;
|
|
5
5
|
if (!Array.isArray(properties) || properties.length < 2)
|
|
6
6
|
return [];
|
|
7
|
-
const present = properties.filter((property) => property
|
|
7
|
+
const present = properties.filter((property) => Object.hasOwn(input, property));
|
|
8
8
|
if (present.length > 0)
|
|
9
9
|
return [];
|
|
10
10
|
if (properties.length > 4) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indexes every `$ref` target in a document, plus each of its `/`-delimited
|
|
3
|
+
* ancestors, so "is this reusable object referenced?" is a single set lookup.
|
|
4
|
+
*
|
|
5
|
+
* A component counts as used when a `$ref` targets it *or* points into it
|
|
6
|
+
* (`#/components/schemas/Pet/properties/id` still uses `Pet`). Testing that
|
|
7
|
+
* per component meant scanning every ref for a prefix match — O(components ×
|
|
8
|
+
* refs), and the unreferenced-object rule copied the whole ref set into a fresh
|
|
9
|
+
* array on each component to do it. Recording the ancestors once turns the whole
|
|
10
|
+
* rule into O(refs + components).
|
|
11
|
+
*
|
|
12
|
+
* Shared by `unreferencedReusableObject` and the OpenAPI `oasUnusedComponent`,
|
|
13
|
+
* which had a byte-identical copy of the walk between them.
|
|
14
|
+
*/
|
|
15
|
+
export declare const collectReferencedPointers: (document: unknown) => Set<string>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const collectReferencedPointers = (document) => {
|
|
2
|
+
const pointers = /* @__PURE__ */ new Set();
|
|
3
|
+
const add = (ref) => {
|
|
4
|
+
pointers.add(ref);
|
|
5
|
+
for (let slash = ref.lastIndexOf("/"); slash > 0; slash = ref.lastIndexOf("/", slash - 1)) {
|
|
6
|
+
const prefix = ref.slice(0, slash);
|
|
7
|
+
if (pointers.has(prefix))
|
|
8
|
+
return;
|
|
9
|
+
pointers.add(prefix);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const stack = [document];
|
|
13
|
+
while (stack.length > 0) {
|
|
14
|
+
const node = stack.pop();
|
|
15
|
+
if (Array.isArray(node)) {
|
|
16
|
+
for (const item of node)
|
|
17
|
+
stack.push(item);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (typeof node !== "object" || node === null)
|
|
21
|
+
continue;
|
|
22
|
+
for (const [key, value] of Object.entries(node)) {
|
|
23
|
+
if (key === "$ref" && typeof value === "string")
|
|
24
|
+
add(value);
|
|
25
|
+
else
|
|
26
|
+
stack.push(value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return pointers;
|
|
30
|
+
};
|
|
31
|
+
export {
|
|
32
|
+
collectReferencedPointers
|
|
33
|
+
};
|
|
@@ -16,6 +16,21 @@ export type ISchemaOptions = {
|
|
|
16
16
|
* without the caller naming a dialect.
|
|
17
17
|
*/
|
|
18
18
|
dialect?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Report nothing when the schema itself cannot be built or run, instead of
|
|
21
|
+
* reporting why.
|
|
22
|
+
*
|
|
23
|
+
* The default suits a schema written in the *ruleset*: if that one is
|
|
24
|
+
* unusable, its author wants to hear about it. A schema taken from the
|
|
25
|
+
* *document* — a message payload, a parameter's schema — is different. It can
|
|
26
|
+
* legitimately carry a `$ref` this package cannot resolve (an external file, or
|
|
27
|
+
* anything at all when no resolver was injected), and there the validator's own
|
|
28
|
+
* complaint is not a finding about the document: it is an error-severity
|
|
29
|
+
* diagnostic, on a valid document, whose text describes this package's API.
|
|
30
|
+
* Callers validating document-supplied schemas pass `true` and get silence
|
|
31
|
+
* where they cannot judge.
|
|
32
|
+
*/
|
|
33
|
+
skipUnusableSchema?: boolean;
|
|
19
34
|
};
|
|
20
35
|
/** Validates a value against a JSON Schema supplied in the rule's options. */
|
|
21
36
|
export declare const schema: RulesetFunction<unknown, ISchemaOptions>;
|
package/dist/functions/schema.js
CHANGED
|
@@ -83,6 +83,8 @@ const schema = (input, options, context) => {
|
|
|
83
83
|
try {
|
|
84
84
|
result = getValidator(options.schema)(input);
|
|
85
85
|
} catch (error) {
|
|
86
|
+
if (options.skipUnusableSchema)
|
|
87
|
+
return [];
|
|
86
88
|
return [
|
|
87
89
|
{
|
|
88
90
|
message: `Invalid schema: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ownKey } from "../core/own-key.js";
|
|
1
2
|
const JS_TYPES = {
|
|
2
3
|
string: (v) => typeof v === "string",
|
|
3
4
|
number: (v) => typeof v === "number",
|
|
@@ -18,7 +19,7 @@ const typedEnum = (input, _options, context) => {
|
|
|
18
19
|
if ((input["nullable"] === true || input["x-nullable"] === true) && !types.includes("null")) {
|
|
19
20
|
types.push("null");
|
|
20
21
|
}
|
|
21
|
-
const checkers = types.map((type) => JS_TYPES
|
|
22
|
+
const checkers = types.map((type) => ownKey(JS_TYPES, String(type))).filter((fn) => fn !== void 0);
|
|
22
23
|
if (checkers.length === 0)
|
|
23
24
|
return [];
|
|
24
25
|
const results = [];
|
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
if (Array.isArray(node)) {
|
|
3
|
-
for (const item of node)
|
|
4
|
-
collectRefs(item, into);
|
|
5
|
-
return;
|
|
6
|
-
}
|
|
7
|
-
if (typeof node === "object" && node !== null) {
|
|
8
|
-
for (const [key, value] of Object.entries(node)) {
|
|
9
|
-
if (key === "$ref" && typeof value === "string")
|
|
10
|
-
into.add(value);
|
|
11
|
-
else
|
|
12
|
-
collectRefs(value, into);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
};
|
|
1
|
+
import { collectReferencedPointers } from "./ref-index.js";
|
|
16
2
|
const escapePointerSegment = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
17
3
|
const unreferencedReusableObject = (input, options, context) => {
|
|
18
4
|
if (typeof input !== "object" || input === null)
|
|
@@ -20,13 +6,10 @@ const unreferencedReusableObject = (input, options, context) => {
|
|
|
20
6
|
const location = options?.reusableObjectsLocation;
|
|
21
7
|
if (!location)
|
|
22
8
|
return [];
|
|
23
|
-
const
|
|
24
|
-
collectRefs(context.document.data, refs);
|
|
9
|
+
const referenced = collectReferencedPointers(context.document.data);
|
|
25
10
|
const results = [];
|
|
26
11
|
for (const key of Object.keys(input)) {
|
|
27
|
-
|
|
28
|
-
const referenced = refs.has(base) || [...refs].some((ref) => ref.startsWith(`${base}/`));
|
|
29
|
-
if (!referenced) {
|
|
12
|
+
if (!referenced.has(`${location}/${escapePointerSegment(key)}`)) {
|
|
30
13
|
results.push({
|
|
31
14
|
message: "This reusable object is never referenced",
|
|
32
15
|
path: [...context.path, key]
|
package/dist/functions/xor.js
CHANGED
|
@@ -4,7 +4,7 @@ const xor = (input, options) => {
|
|
|
4
4
|
const properties = options?.properties;
|
|
5
5
|
if (!Array.isArray(properties) || properties.length < 2)
|
|
6
6
|
return [];
|
|
7
|
-
const present = properties.filter((property) => property
|
|
7
|
+
const present = properties.filter((property) => Object.hasOwn(input, property));
|
|
8
8
|
if (present.length !== 1) {
|
|
9
9
|
return [{ message: `Exactly one of ${properties.map((p) => `"${p}"`).join(", ")} must be defined` }];
|
|
10
10
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import { type IDocumentOptions, type LintPlugin, type LintResolver, type ResolvedExtend, type Ruleset } from './core/index.js';
|
|
2
2
|
import type { IDiagnostic, RulesetDefinition } from './core/types.js';
|
|
3
3
|
import { type AppliedFix, type FixerRegistry } from './fix/index.js';
|
|
4
|
+
import { type IRulesetTrustOptions } from './ruleset-files.js';
|
|
4
5
|
export { type AliasDefinition, type CompiledPath, compileQuery, createDocument, createLinter, type Document, detectFormats, type ExtendModifier, type ExtendResolver, type Format, globToRegExp, type IDocumentOptions, type IQueryMatch, type IRulesetProblem, type IRunOptions, type Linter, type LintOptions, type LintPlugin, type LintPluginContext, type LintPluginResult, type LintResolver, type LintResolverResult, type LintResult, lint, lintWithResult, matchesGlob, type PluginRunResult, pointerToPath, query, queryCompiled, queryMany, type ResolvedExtend, type Ruleset, type RulesetOptions, resolveSourceOrigin, resolveSourceOriginFromMap, resolveSourcePath, runPlugins, validateRuleset, } from './core/index.js';
|
|
5
6
|
export { type AppliedFix, type ApplyFixesOptions, applyFixes, createFixPlugin, type EditOp, FIX_PLUGIN_NAME, type FixContext, type Fixer, type FixerRegistry, type FixPluginData, type FixResult, type ParserFormat, } from './fix/index.js';
|
|
6
7
|
export { alphabetical, builtinFunctions, type CasingType, casing, defined, enumeration, falsy, type IAlphabeticalOptions, type ICasingOptions, type IOrOptions, type ISchemaOptions, type IUnreferencedReusableObjectOptions, type IXorOptions, length, or, pattern, schema, truthy, typedEnum, undefinedFn, unreferencedReusableObject, xor, } from './functions/index.js';
|
|
7
8
|
export { detectFormat, parseWithPointers } from './parsers/index.js';
|
|
8
|
-
|
|
9
|
-
* An optional directory that everything a ruleset pulls off disk — `extends`
|
|
10
|
-
* targets and custom function modules — must resolve inside.
|
|
11
|
-
*
|
|
12
|
-
* This is opt-in and off by default, because `basePath` on its own is only a
|
|
13
|
-
* *resolution origin*: an `extends` of `/etc/thing.js` or `../../../elsewhere`
|
|
14
|
-
* resolves and loads exactly as written. See the "Trust boundary" section of the
|
|
15
|
-
* README — a ruleset that can name a `.js` file can run code, restricted root or
|
|
16
|
-
* not. This narrows *which* files it can name; it is not a sandbox.
|
|
17
|
-
*/
|
|
18
|
-
export type IRulesetTrustOptions = {
|
|
19
|
-
/** Directory that `extends` files and custom functions must resolve under. */
|
|
20
|
-
restrictTo?: string;
|
|
21
|
-
};
|
|
9
|
+
export type { IRulesetTrustOptions } from './ruleset-files.js';
|
|
22
10
|
/**
|
|
23
11
|
* Resolves an `extends` reference to a ruleset definition. Supports:
|
|
24
12
|
* - local file paths (relative to `basePath`, or absolute): `.yaml` / `.yml` / `.json` / `.js`,
|
|
@@ -49,8 +37,20 @@ export declare const resolveNamedRuleset: (name: string, basePath?: string, opti
|
|
|
49
37
|
export declare const createRuleset: (definition?: RulesetDefinition, basePath?: string, options?: IRulesetTrustOptions) => Ruleset;
|
|
50
38
|
/** Options for {@link lintDocument}: the document options plus ruleset controls. */
|
|
51
39
|
export type ILintOptions = IDocumentOptions & {
|
|
52
|
-
/**
|
|
53
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The rules to evaluate: either a definition to build with the built-in
|
|
42
|
+
* functions, or a {@link Ruleset} that is already built. When omitted, no rules
|
|
43
|
+
* run.
|
|
44
|
+
*
|
|
45
|
+
* Pass a built one to use a preset that brings its own functions and format
|
|
46
|
+
* detectors — `createOpenApiRuleset()` from `@amritk/lint/rules/openapi`, say.
|
|
47
|
+
* A definition alone cannot express those, so an OpenAPI ruleset handed over as
|
|
48
|
+
* data would have every rule skipped: its functions are unknown here, and its
|
|
49
|
+
* `formats` gate matches nothing against an empty format registry.
|
|
50
|
+
* `rulesetBasePath` and `restrictTo` only apply while *building*, so they are
|
|
51
|
+
* ignored for a ruleset that already is.
|
|
52
|
+
*/
|
|
53
|
+
ruleset?: RulesetDefinition | Ruleset;
|
|
54
54
|
/** Directory that the ruleset's string `extends` references resolve relative to. */
|
|
55
55
|
rulesetBasePath?: string;
|
|
56
56
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,99 +1,26 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { dirname, isAbsolute, resolve as resolvePath, sep } from "node:path";
|
|
4
1
|
import { createBoundedCache, createRuleset as createCoreRuleset, lintWithResult } from "./core/index.js";
|
|
5
2
|
import { createFixPlugin, FIX_PLUGIN_NAME } from "./fix/index.js";
|
|
6
3
|
import { builtinFunctions } from "./functions/index.js";
|
|
7
|
-
import {
|
|
4
|
+
import { collectCustomFunctions, resolveRulesetFile } from "./ruleset-files.js";
|
|
8
5
|
import { compileQuery, createDocument, createLinter, detectFormats, globToRegExp, lint, lintWithResult as lintWithResult2, matchesGlob, pointerToPath, query, queryCompiled, queryMany, resolveSourceOrigin, resolveSourceOriginFromMap, resolveSourcePath, runPlugins, validateRuleset } from "./core/index.js";
|
|
9
6
|
import { applyFixes, createFixPlugin as createFixPlugin2, FIX_PLUGIN_NAME as FIX_PLUGIN_NAME2 } from "./fix/index.js";
|
|
10
7
|
import { alphabetical, builtinFunctions as builtinFunctions2, casing, defined, enumeration, falsy, length, or, pattern, schema, truthy, typedEnum, undefinedFn, unreferencedReusableObject, xor } from "./functions/index.js";
|
|
11
|
-
import { detectFormat, parseWithPointers
|
|
12
|
-
const
|
|
13
|
-
const loadRulesetFile = (file) => {
|
|
14
|
-
if (/\.(ya?ml|json)$/i.test(file)) {
|
|
15
|
-
return parseWithPointers(readFileSync(file, "utf8")).data;
|
|
16
|
-
}
|
|
17
|
-
const module = require2(file);
|
|
18
|
-
return module.default ?? module;
|
|
19
|
-
};
|
|
20
|
-
const assertWithinRoot = (file, restrictTo, what) => {
|
|
21
|
-
if (restrictTo === void 0)
|
|
22
|
-
return;
|
|
23
|
-
const root = resolvePath(restrictTo);
|
|
24
|
-
const resolved = resolvePath(file);
|
|
25
|
-
if (resolved !== root && !resolved.startsWith(root.endsWith(sep) ? root : `${root}${sep}`)) {
|
|
26
|
-
throw new Error(`${what} "${file}" resolves outside the permitted root ${root}`);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
const resolveNamedRuleset = (name, basePath = process.cwd(), options = {}) => {
|
|
30
|
-
if (name.startsWith(".") || isAbsolute(name)) {
|
|
31
|
-
const file2 = resolvePath(basePath, name);
|
|
32
|
-
assertWithinRoot(file2, options.restrictTo, "Extended ruleset");
|
|
33
|
-
return { definition: loadRulesetFile(file2), basePath: dirname(file2) };
|
|
34
|
-
}
|
|
35
|
-
let file;
|
|
36
|
-
try {
|
|
37
|
-
file = require2.resolve(name, { paths: [basePath] });
|
|
38
|
-
} catch {
|
|
39
|
-
throw new Error(`Cannot resolve extended ruleset "${name}" from ${basePath}`);
|
|
40
|
-
}
|
|
41
|
-
assertWithinRoot(file, options.restrictTo, "Extended ruleset");
|
|
42
|
-
return { definition: loadRulesetFile(file), basePath: dirname(file) };
|
|
43
|
-
};
|
|
44
|
-
const loadFunctionByName = (basePath, dir, name, restrictTo) => {
|
|
45
|
-
const baseFile = resolvePath(basePath, dir, name);
|
|
46
|
-
assertWithinRoot(baseFile, restrictTo, "Custom function");
|
|
47
|
-
for (const candidate of [baseFile, `${baseFile}.js`, `${baseFile}.cjs`, `${baseFile}.mjs`]) {
|
|
48
|
-
try {
|
|
49
|
-
const resolvedFile = require2.resolve(candidate);
|
|
50
|
-
const module = require2(resolvedFile);
|
|
51
|
-
const fn = module.default ?? module;
|
|
52
|
-
if (typeof fn !== "function")
|
|
53
|
-
throw new Error(`"${name}" did not export a function`);
|
|
54
|
-
return fn;
|
|
55
|
-
} catch (error) {
|
|
56
|
-
if (error.code !== "MODULE_NOT_FOUND")
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
throw new Error(`Cannot resolve custom function "${name}" from ${resolvePath(basePath, dir)}`);
|
|
61
|
-
};
|
|
62
|
-
const collectCustomFunctions = (definition, basePath, into, seen, restrictTo) => {
|
|
63
|
-
if (seen.has(definition))
|
|
64
|
-
return;
|
|
65
|
-
seen.add(definition);
|
|
66
|
-
if (definition.extends) {
|
|
67
|
-
const entries = Array.isArray(definition.extends) ? definition.extends : [definition.extends];
|
|
68
|
-
for (const entry of entries) {
|
|
69
|
-
const target = Array.isArray(entry) ? entry[0] : entry;
|
|
70
|
-
if (typeof target === "string") {
|
|
71
|
-
const key = `${basePath}\0${target}`;
|
|
72
|
-
if (seen.has(key))
|
|
73
|
-
continue;
|
|
74
|
-
seen.add(key);
|
|
75
|
-
const resolved = resolveNamedRuleset(target, basePath, { ...restrictTo !== void 0 ? { restrictTo } : {} });
|
|
76
|
-
collectCustomFunctions(resolved.definition, resolved.basePath, into, seen, restrictTo);
|
|
77
|
-
} else {
|
|
78
|
-
collectCustomFunctions(target, basePath, into, seen, restrictTo);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (Array.isArray(definition.functions)) {
|
|
83
|
-
const dir = definition.functionsDir ?? "functions";
|
|
84
|
-
for (const name of definition.functions)
|
|
85
|
-
into[name] = loadFunctionByName(basePath, dir, name, restrictTo);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
8
|
+
import { detectFormat, parseWithPointers } from "./parsers/index.js";
|
|
9
|
+
const resolveNamedRuleset = (name, basePath = process.cwd(), options = {}) => resolveRulesetFile(name, basePath, options);
|
|
88
10
|
const buildRuleset = (definition, basePath, restrictTo) => {
|
|
11
|
+
const trust = restrictTo !== void 0 ? { restrictTo } : {};
|
|
12
|
+
const resolve = (name, from) => resolveNamedRuleset(name, from, trust);
|
|
89
13
|
let functions = builtinFunctions;
|
|
90
14
|
const custom = {};
|
|
91
|
-
collectCustomFunctions(definition, basePath ?? process.cwd(), custom, /* @__PURE__ */ new Set(),
|
|
15
|
+
collectCustomFunctions(definition, basePath ?? process.cwd(), custom, /* @__PURE__ */ new Set(), {
|
|
16
|
+
...trust,
|
|
17
|
+
resolveExtend: resolve
|
|
18
|
+
});
|
|
92
19
|
if (Object.keys(custom).length > 0)
|
|
93
20
|
functions = { ...builtinFunctions, ...custom };
|
|
94
21
|
return createCoreRuleset(definition, {
|
|
95
22
|
functions,
|
|
96
|
-
resolve
|
|
23
|
+
resolve,
|
|
97
24
|
...basePath !== void 0 ? { basePath } : {}
|
|
98
25
|
});
|
|
99
26
|
};
|
|
@@ -102,6 +29,9 @@ const emptyRulesets = createBoundedCache(16);
|
|
|
102
29
|
const createRuleset = (definition, basePath, options = {}) => {
|
|
103
30
|
const { restrictTo } = options;
|
|
104
31
|
const key = `${restrictTo ?? ""}\0${basePath ?? process.cwd()}`;
|
|
32
|
+
if (definition !== void 0 && (typeof definition !== "object" || definition === null)) {
|
|
33
|
+
return buildRuleset(definition, basePath, restrictTo);
|
|
34
|
+
}
|
|
105
35
|
if (definition === void 0) {
|
|
106
36
|
const cached2 = emptyRulesets.get(key);
|
|
107
37
|
if (cached2)
|
|
@@ -122,13 +52,16 @@ const createRuleset = (definition, basePath, options = {}) => {
|
|
|
122
52
|
byBasePath.set(key, built);
|
|
123
53
|
return built;
|
|
124
54
|
};
|
|
55
|
+
const isBuiltRuleset = (ruleset) => typeof ruleset.getFunction === "function";
|
|
125
56
|
const lintDocument = async (input, options = {}) => (await lintDocumentWithResult(input, options)).diagnostics;
|
|
126
57
|
const lintDocumentWithResult = async (input, options = {}) => {
|
|
127
|
-
const { ruleset
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
58
|
+
const { ruleset, rulesetBasePath, restrictTo, ...rest } = options;
|
|
59
|
+
return runLint(input, toRuleset(ruleset, rulesetBasePath, restrictTo), rest);
|
|
60
|
+
};
|
|
61
|
+
const toRuleset = (ruleset, basePath, restrictTo) => {
|
|
62
|
+
if (ruleset !== void 0 && isBuiltRuleset(ruleset))
|
|
63
|
+
return ruleset;
|
|
64
|
+
return createRuleset(ruleset, basePath, { ...restrictTo !== void 0 ? { restrictTo } : {} });
|
|
132
65
|
};
|
|
133
66
|
const runLint = async (input, ruleset, options) => {
|
|
134
67
|
const { resolve, plugins, ...documentOptions } = options;
|
|
@@ -142,11 +75,9 @@ const runLint = async (input, ruleset, options) => {
|
|
|
142
75
|
const appliedKey = (fix) => `${fix.code}\0${fix.path.map((segment) => typeof segment === "number" ? `[${segment}]` : `.${segment}`).join("")}`;
|
|
143
76
|
const MAX_FIX_PASSES = 10;
|
|
144
77
|
const fixDocument = async (input, options = {}) => {
|
|
145
|
-
const { fixers = {}, safeOnly, ruleset:
|
|
78
|
+
const { fixers = {}, safeOnly, ruleset: rulesetOption, rulesetBasePath, restrictTo, ...lintOptions } = options;
|
|
146
79
|
const plugin = createFixPlugin(fixers, { safeOnly: safeOnly !== false });
|
|
147
|
-
const ruleset =
|
|
148
|
-
...restrictTo !== void 0 ? { restrictTo } : {}
|
|
149
|
-
});
|
|
80
|
+
const ruleset = toRuleset(rulesetOption, rulesetBasePath, restrictTo);
|
|
150
81
|
let current = input;
|
|
151
82
|
let passes = 0;
|
|
152
83
|
let converged = false;
|
|
@@ -172,7 +103,7 @@ const fixDocument = async (input, options = {}) => {
|
|
|
172
103
|
}
|
|
173
104
|
}
|
|
174
105
|
const remaining = (await runLint(current, ruleset, lintOptions)).diagnostics;
|
|
175
|
-
return { output: current, fixed:
|
|
106
|
+
return { output: current, fixed: current !== input, remaining, applied, converged, passes };
|
|
176
107
|
};
|
|
177
108
|
export {
|
|
178
109
|
FIX_PLUGIN_NAME2 as FIX_PLUGIN_NAME,
|
|
@@ -199,7 +130,7 @@ export {
|
|
|
199
130
|
lintWithResult2 as lintWithResult,
|
|
200
131
|
matchesGlob,
|
|
201
132
|
or,
|
|
202
|
-
|
|
133
|
+
parseWithPointers,
|
|
203
134
|
pattern,
|
|
204
135
|
pointerToPath,
|
|
205
136
|
query,
|