@amritk/lint 0.4.7 → 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.
@@ -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
+ };
@@ -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[value];
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[name];
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[name];
72
+ const existing = ownKey(ctx.aliases, name);
72
73
  if (existing === void 0 || existing === alias) {
73
- ctx.aliases[name] = alias;
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[key];
79
+ const at = ownKey(ctx.aliases, key);
79
80
  if (at === void 0 || at === alias) {
80
- ctx.aliases[key] = alias;
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[name];
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
- getFunction: (name) => functions[name],
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 fixer = fixers[String(diagnostic.code)];
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 compare = (a, b) => {
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 (compare(items[i], items[i + 1]) > 0) {
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: applied.length > 0, remaining, applied, converged, passes };
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) => compareAlphabetical(nameOf(array[a]), nameOf(array[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 in seen) {
15
+ if (Object.hasOwn(seen, name)) {
15
16
  results.push({ message: `Path parameter "${name}" must not be defined multiple times`, path: definitionPath });
16
17
  return void 0;
17
18
  }
@@ -44,7 +45,7 @@ const oasPathParam = (paths, _options, context) => {
44
45
  const definitionPath = [...context.path, path, "parameters", index];
45
46
  const name = recordPathParam(param, definitionPath, topParams, results);
46
47
  if (name !== void 0)
47
- topParams[name] = definitionPath;
48
+ setOwnKey(topParams, name, definitionPath);
48
49
  });
49
50
  }
50
51
  for (const [method, operation] of Object.entries(item)) {
@@ -59,7 +60,7 @@ const oasPathParam = (paths, _options, context) => {
59
60
  const definitionPath = [...operationPath, "parameters", index];
60
61
  const name = recordPathParam(param, definitionPath, operationParams, results);
61
62
  if (name !== void 0)
62
- operationParams[name] = definitionPath;
63
+ setOwnKey(operationParams, name, definitionPath);
63
64
  });
64
65
  }
65
66
  const defined = { ...topParams, ...operationParams };
@@ -69,7 +70,7 @@ const oasPathParam = (paths, _options, context) => {
69
70
  }
70
71
  }
71
72
  for (const name of templates) {
72
- if (!(name in defined)) {
73
+ if (!Object.hasOwn(defined, name)) {
73
74
  results.push({
74
75
  message: `Operation must define path parameter "{${name}}" as expected by path "${path}"`,
75
76
  path: operationPath
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/lint",
3
- "version": "0.4.7",
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.10.1",
66
+ "@amritk/runtime-validators": "^0.11.0",
67
67
  "@amritk/yaml": "^0.7.1",
68
68
  "jsonc-parser": "^3.3.1"
69
69
  },
70
70
  "devDependencies": {
71
- "@amritk/resolve-refs": "^0.5.1",
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.6"
74
+ "@stoplight/spectral-rulesets": "^1.22.7"
75
75
  }
76
76
  }