@amritk/lint 0.4.6 → 0.4.8
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/dist/core/own-key.d.ts +18 -0
- package/dist/core/own-key.js +12 -0
- package/dist/core/ruleset.js +13 -8
- package/dist/fix/apply.js +3 -1
- package/dist/functions/alphabetical.d.ts +14 -0
- package/dist/functions/alphabetical.js +4 -3
- package/dist/index.js +1 -1
- package/dist/rules/openapi/fixers.js +7 -6
- package/dist/rules/openapi/functions/oas-path-param.js +5 -4
- package/package.json +5 -5
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Own-property reads and writes for maps keyed by names the ruleset or the
|
|
3
|
+
* linted document supplies.
|
|
4
|
+
*
|
|
5
|
+
* Those names are author-chosen, so `constructor`, `toString` and `__proto__`
|
|
6
|
+
* are all names someone may legitimately use — and a bare index answers them
|
|
7
|
+
* from `Object.prototype` while a bare assignment on `__proto__` sets the map's
|
|
8
|
+
* prototype instead of adding a key. Neither is ever what the caller meant.
|
|
9
|
+
*
|
|
10
|
+
* `@amritk/helpers` carries the same pair, but `@amritk/lint` depends on
|
|
11
|
+
* nothing beyond `@amritk/runtime-validators` and `@amritk/yaml` by design (see
|
|
12
|
+
* `.claude/architecture.md`), so it keeps its own. One copy inside the package,
|
|
13
|
+
* not one per call site.
|
|
14
|
+
*/
|
|
15
|
+
/** The value at `key`, or `undefined` when the map does not own that name. */
|
|
16
|
+
export declare const ownKey: <T>(source: Readonly<Record<string, T>>, key: string) => T | undefined;
|
|
17
|
+
/** Assigns `value` under `key` as an own data property, `__proto__` included. */
|
|
18
|
+
export declare const setOwnKey: <T>(target: Record<string, T>, key: string, value: NoInfer<T>) => void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const ownKey = (source, key) => Object.hasOwn(source, key) ? source[key] : void 0;
|
|
2
|
+
const setOwnKey = (target, key, value) => {
|
|
3
|
+
if (key === "__proto__") {
|
|
4
|
+
Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
|
|
5
|
+
} else {
|
|
6
|
+
target[key] = value;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
export {
|
|
10
|
+
ownKey,
|
|
11
|
+
setOwnKey
|
|
12
|
+
};
|
package/dist/core/ruleset.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DiagnosticSeverity } from "../parsers/index.js";
|
|
2
2
|
import { matchesGlob } from "./glob.js";
|
|
3
3
|
import { compileQuery } from "./jsonpath.js";
|
|
4
|
+
import { ownKey, setOwnKey } from "./own-key.js";
|
|
4
5
|
const SEVERITY_NAMES = {
|
|
5
6
|
error: DiagnosticSeverity.Error,
|
|
6
7
|
warn: DiagnosticSeverity.Warning,
|
|
@@ -13,7 +14,7 @@ const parseSeverity = (value) => {
|
|
|
13
14
|
return { severity: DiagnosticSeverity.Warning, enabled: true };
|
|
14
15
|
if (typeof value === "number")
|
|
15
16
|
return { severity: value, enabled: true };
|
|
16
|
-
const mapped = SEVERITY_NAMES
|
|
17
|
+
const mapped = ownKey(SEVERITY_NAMES, value);
|
|
17
18
|
if (mapped === "off")
|
|
18
19
|
return { severity: DiagnosticSeverity.Warning, enabled: false };
|
|
19
20
|
if (mapped === void 0)
|
|
@@ -65,19 +66,19 @@ const applyEntry = (rules, name, entry, throwOnMissing = false) => {
|
|
|
65
66
|
rules.set(name, normalizeRule(name, entry, "all"));
|
|
66
67
|
};
|
|
67
68
|
const registerAlias = (ctx, declaringAliases, name, ruleName) => {
|
|
68
|
-
const alias = declaringAliases
|
|
69
|
+
const alias = ownKey(declaringAliases, name);
|
|
69
70
|
if (!alias)
|
|
70
71
|
throw new Error(`Rule "${ruleName}" references undefined alias "#${name}"`);
|
|
71
|
-
const existing = ctx.aliases
|
|
72
|
+
const existing = ownKey(ctx.aliases, name);
|
|
72
73
|
if (existing === void 0 || existing === alias) {
|
|
73
|
-
ctx.aliases
|
|
74
|
+
setOwnKey(ctx.aliases, name, alias);
|
|
74
75
|
return name;
|
|
75
76
|
}
|
|
76
77
|
for (let n = 0; ; n++) {
|
|
77
78
|
const key = `${name}__${n}`;
|
|
78
|
-
const at = ctx.aliases
|
|
79
|
+
const at = ownKey(ctx.aliases, key);
|
|
79
80
|
if (at === void 0 || at === alias) {
|
|
80
|
-
ctx.aliases
|
|
81
|
+
setOwnKey(ctx.aliases, key, alias);
|
|
81
82
|
return key;
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -178,7 +179,7 @@ const createRuleset = (definition, options = {}) => {
|
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
181
|
const resolveAlias = (name, documentFormats) => {
|
|
181
|
-
const alias = aliases
|
|
182
|
+
const alias = ownKey(aliases, name);
|
|
182
183
|
if (!alias)
|
|
183
184
|
return [];
|
|
184
185
|
if (Array.isArray(alias))
|
|
@@ -199,7 +200,11 @@ const createRuleset = (definition, options = {}) => {
|
|
|
199
200
|
overrides,
|
|
200
201
|
parserOptions,
|
|
201
202
|
enabledRules: rules.filter((rule) => rule.enabled),
|
|
202
|
-
|
|
203
|
+
// `Object.hasOwn`, not a bare index: `then.function` is ruleset input, so
|
|
204
|
+
// `"toString"` otherwise resolved to `Function.prototype.toString` and ran
|
|
205
|
+
// as a rule function instead of being reported as unknown — its string
|
|
206
|
+
// return value then read as an iterable of per-character diagnostics.
|
|
207
|
+
getFunction: (name) => ownKey(functions, name),
|
|
203
208
|
rulesForSource: (source) => {
|
|
204
209
|
if (!source || !hasOverrides)
|
|
205
210
|
return rules;
|
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,7 +13,8 @@ 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)
|
|
@@ -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/index.js
CHANGED
|
@@ -172,7 +172,7 @@ const fixDocument = async (input, options = {}) => {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
const remaining = (await runLint(current, ruleset, lintOptions)).diagnostics;
|
|
175
|
-
return { output: current, fixed:
|
|
175
|
+
return { output: current, fixed: current !== input, remaining, applied, converged, passes };
|
|
176
176
|
};
|
|
177
177
|
export {
|
|
178
178
|
FIX_PLUGIN_NAME2 as FIX_PLUGIN_NAME,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compareAlphabetically } from "../../functions/alphabetical.js";
|
|
1
2
|
const getAtPath = (data, path) => {
|
|
2
3
|
let current = data;
|
|
3
4
|
for (const segment of path) {
|
|
@@ -73,11 +74,6 @@ const duplicatedEnum = {
|
|
|
73
74
|
return { op: "removeItems", path: diagnostic.path, indices: duplicates };
|
|
74
75
|
}
|
|
75
76
|
};
|
|
76
|
-
const compareAlphabetical = (a, b) => {
|
|
77
|
-
if (typeof a === "number" && typeof b === "number")
|
|
78
|
-
return a - b;
|
|
79
|
-
return String(a).localeCompare(String(b));
|
|
80
|
-
};
|
|
81
77
|
const tagsAlphabetical = {
|
|
82
78
|
safe: true,
|
|
83
79
|
fix: ({ diagnostic, data }) => {
|
|
@@ -86,9 +82,14 @@ const tagsAlphabetical = {
|
|
|
86
82
|
if (!Array.isArray(array))
|
|
87
83
|
return void 0;
|
|
88
84
|
const nameOf = (item) => item != null && typeof item === "object" ? item["name"] : item;
|
|
89
|
-
const order = array.map((_, index) => index).sort((a, b) =>
|
|
85
|
+
const order = array.map((_, index) => index).sort((a, b) => compareAlphabetically(nameOf(array[a]), nameOf(array[b])));
|
|
90
86
|
if (order.every((value, index) => value === index))
|
|
91
87
|
return void 0;
|
|
88
|
+
const sorted = order.map((index) => nameOf(array[index]));
|
|
89
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
90
|
+
if (compareAlphabetically(sorted[i], sorted[i + 1]) > 0)
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
92
93
|
return { op: "reorderArray", path: arrayPath, order };
|
|
93
94
|
}
|
|
94
95
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setOwnKey } from "../../../core/own-key.js";
|
|
1
2
|
import { isObject, OPERATION_METHODS } from "./helpers.js";
|
|
2
3
|
const PATH_TEMPLATE = /(\{;?\??[a-zA-Z0-9_-]+\*?\})/g;
|
|
3
4
|
const namedPathParam = (param) => param["in"] === "path" && typeof param["name"] === "string" ? param["name"] : void 0;
|
|
@@ -11,7 +12,7 @@ const recordPathParam = (param, definitionPath, seen, results) => {
|
|
|
11
12
|
path: definitionPath
|
|
12
13
|
});
|
|
13
14
|
}
|
|
14
|
-
if (name
|
|
15
|
+
if (Object.hasOwn(seen, name)) {
|
|
15
16
|
results.push({ message: `Path parameter "${name}" must not be defined multiple times`, path: definitionPath });
|
|
16
17
|
return void 0;
|
|
17
18
|
}
|
|
@@ -44,7 +45,7 @@ const oasPathParam = (paths, _options, context) => {
|
|
|
44
45
|
const definitionPath = [...context.path, path, "parameters", index];
|
|
45
46
|
const name = recordPathParam(param, definitionPath, topParams, results);
|
|
46
47
|
if (name !== void 0)
|
|
47
|
-
topParams
|
|
48
|
+
setOwnKey(topParams, name, definitionPath);
|
|
48
49
|
});
|
|
49
50
|
}
|
|
50
51
|
for (const [method, operation] of Object.entries(item)) {
|
|
@@ -59,7 +60,7 @@ const oasPathParam = (paths, _options, context) => {
|
|
|
59
60
|
const definitionPath = [...operationPath, "parameters", index];
|
|
60
61
|
const name = recordPathParam(param, definitionPath, operationParams, results);
|
|
61
62
|
if (name !== void 0)
|
|
62
|
-
operationParams
|
|
63
|
+
setOwnKey(operationParams, name, definitionPath);
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
const defined = { ...topParams, ...operationParams };
|
|
@@ -69,7 +70,7 @@ const oasPathParam = (paths, _options, context) => {
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
for (const name of templates) {
|
|
72
|
-
if (!(name
|
|
73
|
+
if (!Object.hasOwn(defined, name)) {
|
|
73
74
|
results.push({
|
|
74
75
|
message: `Operation must define path parameter "{${name}}" as expected by path "${path}"`,
|
|
75
76
|
path: operationPath
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/lint",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "A fast, format-agnostic JSON/YAML style-guide linter with JSON Schema and custom rules.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -63,14 +63,14 @@
|
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@amritk/runtime-validators": "^0.
|
|
67
|
-
"@amritk/yaml": "^0.7.
|
|
66
|
+
"@amritk/runtime-validators": "^0.11.0",
|
|
67
|
+
"@amritk/yaml": "^0.7.1",
|
|
68
68
|
"jsonc-parser": "^3.3.1"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@amritk/resolve-refs": "^0.
|
|
71
|
+
"@amritk/resolve-refs": "^0.7.0",
|
|
72
72
|
"@stoplight/spectral-core": "^1.23.1",
|
|
73
73
|
"@stoplight/spectral-parsers": "^1.0.5",
|
|
74
|
-
"@stoplight/spectral-rulesets": "^1.22.
|
|
74
|
+
"@stoplight/spectral-rulesets": "^1.22.7"
|
|
75
75
|
}
|
|
76
76
|
}
|