@grexx/grexxlinter 0.2.1318 → 0.2.1408

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/cli.js CHANGED
@@ -14547,8 +14547,7 @@ function buildCoverageIndex(doc) {
14547
14547
  return {
14548
14548
  clauseCovered(casetype, field, modality, when) {
14549
14549
  if (!casetype || !field) return null;
14550
- const v = map.get(key(casetype, field, modality, when));
14551
- return v === void 0 ? null : v;
14550
+ return map.get(key(casetype, field, modality, when)) ?? null;
14552
14551
  },
14553
14552
  uncoveredCount: (casetype) => uncovered.get(casetype) ?? 0
14554
14553
  };
@@ -14999,12 +14998,108 @@ function meetsThreshold(severity, failOn) {
14999
14998
  function messageKey(m) {
15000
14999
  return `${m.rule}|${m.path}|${m.severity}|${m.message}`;
15001
15000
  }
15001
+ var ANY_OF_REQUIRED = /^(.*\/anyOf)\/\d+\/required$/;
15002
+ function requireAnyKey(e) {
15003
+ if (e.keyword !== "required") return null;
15004
+ const m = ANY_OF_REQUIRED.exec(e.schemaPath);
15005
+ return m ? `${m[1]}@${e.instancePath}` : null;
15006
+ }
15007
+ function collectMapState(errors) {
15008
+ const forbidden = /* @__PURE__ */ new Set();
15009
+ const requireAny = /* @__PURE__ */ new Map();
15010
+ for (const e of errors) {
15011
+ if (e.keyword === "false schema") forbidden.add(e.instancePath);
15012
+ const key2 = requireAnyKey(e);
15013
+ if (key2 === null) continue;
15014
+ const g = requireAny.get(key2) ?? { instancePath: e.instancePath, fields: [], err: e };
15015
+ g.fields.push(String(e.params.missingProperty));
15016
+ requireAny.set(key2, g);
15017
+ }
15018
+ return { forbidden, requireAny, emittedRequireAny: /* @__PURE__ */ new Set() };
15019
+ }
15020
+ function pushRequireAny(e, state, ctx) {
15021
+ const key2 = requireAnyKey(e);
15022
+ const g = key2 === null ? null : state.requireAny.get(key2);
15023
+ if (!g || g.fields.length < 2) return false;
15024
+ if (!state.emittedRequireAny.has(key2)) {
15025
+ state.emittedRequireAny.add(key2);
15026
+ const where = describeState(ctx.schema, g.err, ctx.root, ctx.resolve);
15027
+ const suffix = where ? ` ${where}` : "";
15028
+ ctx.push("required-field", g.instancePath, null, `missing at least one of: ${g.fields.join(", ")}${suffix}`);
15029
+ }
15030
+ return true;
15031
+ }
15032
+ function pushValueError(e, loc, ctx) {
15033
+ const p = e.params;
15034
+ const allowed = e.keyword === "enum" ? p.allowedValues ?? [] : [p.allowedValue];
15035
+ const actual = valueAtPointer(ctx.root, e.instancePath);
15036
+ const want = caseOnlyMatch(actual, allowed);
15037
+ if (want != null) {
15038
+ ctx.push("value-case", loc.path, loc.field, `"${String(actual)}" should be "${want}" (case/format mismatch)`);
15039
+ return;
15040
+ }
15041
+ const got = actual === void 0 ? "absent" : JSON.stringify(actual);
15042
+ ctx.push("invalid-value", loc.path, loc.field, `got ${got} \u2014 ${humanize(e)}`);
15043
+ }
15044
+ function receivedType(v) {
15045
+ if (v === null) return "null";
15046
+ if (Array.isArray(v)) return "array";
15047
+ return typeof v;
15048
+ }
15049
+ function pushTypeError(e, loc, ctx) {
15050
+ const actual = valueAtPointer(ctx.root, e.instancePath);
15051
+ let val = JSON.stringify(actual) ?? "";
15052
+ if (val.length > 60) val = `${val.slice(0, 57)}\u2026"`;
15053
+ const expected = String(e.params.type);
15054
+ const withValue = actual === void 0 ? "" : ` with value ${val}`;
15055
+ ctx.push("invalid-type", loc.path, loc.field, `expected ${expected} but received ${receivedType(actual)}${withValue}`);
15056
+ }
15057
+ function pushSchemaError(e, loc, ctx) {
15058
+ const rule = ruleForKeyword(e.keyword);
15059
+ let message = humanize(e);
15060
+ if (rule === "forbidden-field" || rule === "required-field") {
15061
+ const where = describeState(ctx.schema, e, ctx.root, ctx.resolve);
15062
+ if (where) message += ` ${where}`;
15063
+ }
15064
+ ctx.push(rule, loc.path, loc.field, message);
15065
+ }
15066
+ function mapError(e, state, ctx) {
15067
+ if (META_KEYWORDS.has(e.keyword)) return;
15068
+ if (pushRequireAny(e, state, ctx)) return;
15069
+ const loc = locate(e);
15070
+ const isAdditional = e.keyword === "additionalProperties" || e.keyword === "unevaluatedProperties";
15071
+ if (isAdditional && state.forbidden.has(loc.path)) return;
15072
+ if (e.keyword === "enum" || e.keyword === "const") pushValueError(e, loc, ctx);
15073
+ else if (e.keyword === "type") pushTypeError(e, loc, ctx);
15074
+ else pushSchemaError(e, loc, ctx);
15075
+ }
15076
+ function mapAjvErrors(errors, ctx) {
15077
+ const state = collectMapState(errors);
15078
+ for (const e of errors) mapError(e, state, ctx);
15079
+ }
15080
+ function isSkipped(casetype, settings) {
15081
+ if (casetype != null && settings.ignoreCasetypes.includes(casetype)) return true;
15082
+ return settings.includeCasetypes.length > 0 && (casetype == null || !settings.includeCasetypes.includes(casetype));
15083
+ }
15084
+ function pushUncovered(push, coverage, casetype) {
15085
+ if (!coverage || !casetype) return;
15086
+ const n = coverage.uncoveredCount(casetype);
15087
+ if (n > 0) push("unknown-casetype", "", casetype, `${n} rule clause(s) for "${casetype}" are behaviour-only and not checked by the schema`);
15088
+ }
15089
+ function dedupeMessages(messages) {
15090
+ const seen = /* @__PURE__ */ new Set();
15091
+ return messages.filter((m) => {
15092
+ const k = messageKey(m);
15093
+ if (seen.has(k)) return false;
15094
+ seen.add(k);
15095
+ return true;
15096
+ });
15097
+ }
15002
15098
  function lintDocument(ctx, input) {
15003
15099
  const settings = normalizeSettings(ctx.settings);
15004
15100
  const casetype = detectCasetype(input);
15101
+ if (isSkipped(casetype, settings)) return { file: input.file, casetype, messages: [], skipped: true, ok: true };
15005
15102
  const messages = [];
15006
- const skipped = casetype != null && settings.ignoreCasetypes.includes(casetype) || settings.includeCasetypes.length > 0 && (casetype == null || !settings.includeCasetypes.includes(casetype));
15007
- if (skipped) return { file: input.file, casetype, messages: [], skipped: true, ok: true };
15008
15103
  const push = (rule, path, field, message) => {
15009
15104
  const sev = severityFor(rule, settings);
15010
15105
  if (sev === "off") return;
@@ -15014,82 +15109,15 @@ function lintDocument(ctx, input) {
15014
15109
  if (!validate) {
15015
15110
  push("unknown-casetype", "", casetype, casetype ? `no schema for casetype "${casetype}"` : "could not determine casetype (no `_type`)");
15016
15111
  } else if (!validate(input.value)) {
15017
- const errors = validate.errors ?? [];
15018
- const schema = validate.schema;
15019
- const forbidden = /* @__PURE__ */ new Set();
15020
- for (const e of errors) if (e.keyword === "false schema") forbidden.add(e.instancePath);
15021
- const anyOfRe = /^(.*\/anyOf)\/\d+\/required$/;
15022
- const reqAnyGroups = /* @__PURE__ */ new Map();
15023
- for (const e of errors) {
15024
- const m = e.keyword === "required" ? anyOfRe.exec(e.schemaPath) : null;
15025
- if (!m) continue;
15026
- const key2 = `${m[1]}@${e.instancePath}`;
15027
- const g = reqAnyGroups.get(key2) ?? { instancePath: e.instancePath, fields: [], err: e };
15028
- g.fields.push(String(e.params.missingProperty));
15029
- reqAnyGroups.set(key2, g);
15030
- }
15031
- const emittedReqAny = /* @__PURE__ */ new Set();
15032
- for (const e of errors) {
15033
- if (META_KEYWORDS.has(e.keyword)) continue;
15034
- if (e.keyword === "required") {
15035
- const m = anyOfRe.exec(e.schemaPath);
15036
- const key2 = m ? `${m[1]}@${e.instancePath}` : null;
15037
- const g = key2 ? reqAnyGroups.get(key2) : null;
15038
- if (g && g.fields.length >= 2) {
15039
- if (!emittedReqAny.has(key2)) {
15040
- emittedReqAny.add(key2);
15041
- const state = describeState(schema, g.err, input.value, ctx.registry.resolveRef);
15042
- push("required-field", g.instancePath, null, `missing at least one of: ${g.fields.join(", ")}${state ? ` ${state}` : ""}`);
15043
- }
15044
- continue;
15045
- }
15046
- }
15047
- const { field, path } = locate(e);
15048
- if ((e.keyword === "additionalProperties" || e.keyword === "unevaluatedProperties") && forbidden.has(path)) {
15049
- continue;
15050
- }
15051
- if (e.keyword === "enum" || e.keyword === "const") {
15052
- const p = e.params;
15053
- const allowed = e.keyword === "enum" ? p.allowedValues ?? [] : [p.allowedValue];
15054
- const actual = valueAtPointer(input.value, e.instancePath);
15055
- const want = caseOnlyMatch(actual, allowed);
15056
- if (want != null) {
15057
- push("value-case", path, field, `"${String(actual)}" should be "${want}" (case/format mismatch)`);
15058
- continue;
15059
- }
15060
- const got = actual === void 0 ? "absent" : JSON.stringify(actual);
15061
- push("invalid-value", path, field, `got ${got} \u2014 ${humanize(e)}`);
15062
- continue;
15063
- }
15064
- if (e.keyword === "type") {
15065
- const actual = valueAtPointer(input.value, e.instancePath);
15066
- const recv = actual === null ? "null" : Array.isArray(actual) ? "array" : typeof actual;
15067
- let val = JSON.stringify(actual) ?? "";
15068
- if (val.length > 60) val = `${val.slice(0, 57)}\u2026"`;
15069
- const expected = String(e.params.type);
15070
- push("invalid-type", path, field, `expected ${expected} but received ${recv}${actual === void 0 ? "" : ` with value ${val}`}`);
15071
- continue;
15072
- }
15073
- const rule = ruleForKeyword(e.keyword);
15074
- let message = humanize(e);
15075
- if (rule === "forbidden-field" || rule === "required-field") {
15076
- const state = describeState(schema, e, input.value, ctx.registry.resolveRef);
15077
- if (state) message += ` ${state}`;
15078
- }
15079
- push(rule, path, field, message);
15080
- }
15081
- }
15082
- if (settings.reportUncovered && ctx.coverage && casetype) {
15083
- const n = ctx.coverage.uncoveredCount(casetype);
15084
- if (n > 0) push("unknown-casetype", "", casetype, `${n} rule clause(s) for "${casetype}" are behaviour-only and not checked by the schema`);
15112
+ mapAjvErrors(validate.errors ?? [], {
15113
+ push,
15114
+ schema: validate.schema,
15115
+ root: input.value,
15116
+ resolve: ctx.registry.resolveRef
15117
+ });
15085
15118
  }
15086
- const seen = /* @__PURE__ */ new Set();
15087
- const deduped = messages.filter((m) => {
15088
- const k = messageKey(m);
15089
- if (seen.has(k)) return false;
15090
- seen.add(k);
15091
- return true;
15092
- });
15119
+ if (settings.reportUncovered) pushUncovered(push, ctx.coverage, casetype);
15120
+ const deduped = dedupeMessages(messages);
15093
15121
  const ok = !deduped.some((m) => meetsThreshold(m.severity, settings.failOn));
15094
15122
  return { file: input.file, casetype, messages: deduped, skipped: false, ok };
15095
15123
  }
@@ -15124,7 +15152,7 @@ function toJSOrError(doc) {
15124
15152
  // src/cli.ts
15125
15153
  var PARALLEL_THRESHOLD = 200;
15126
15154
  var MAX_WORKERS = 8;
15127
- var VERSION = true ? "0.2.1318" : "0.0.0-dev";
15155
+ var VERSION = "0.2.1408";
15128
15156
  function isNewer(latest, current) {
15129
15157
  const a = latest.split(".").map(Number);
15130
15158
  const b = current.split(".").map(Number);
@@ -15144,26 +15172,60 @@ function checkForUpdate(disabled) {
15144
15172
  return fetch("https://registry.npmjs.org/@grexx%2Fgrexxlinter/latest", { signal: ctrl.signal }).then((r) => r.ok ? r.json() : null).then((j) => j?.version && isNewer(j.version, VERSION) ? j.version : null).catch(() => null).finally(() => clearTimeout(timer));
15145
15173
  }
15146
15174
  var VALID_RULES = new Set(LINT_RULE_DESCRIPTORS.map((d) => d.id));
15175
+ function parseOffList(raw) {
15176
+ const list = raw.split(",").map((s) => s.trim()).filter(Boolean);
15177
+ for (const r of list) {
15178
+ if (!VALID_RULES.has(r)) throw new Error(`--off: unknown rule "${r}" (valid: ${[...VALID_RULES].join(", ")})`);
15179
+ }
15180
+ return list;
15181
+ }
15182
+ var VALUE_FLAGS = /* @__PURE__ */ new Map([
15183
+ ["--settings", (args, v) => {
15184
+ args.settingsFile = v;
15185
+ }],
15186
+ ["--off", (args, v) => {
15187
+ args.off.push(...parseOffList(v ?? ""));
15188
+ }],
15189
+ ["--schemas-dir", (args, v) => {
15190
+ args.schemasDir = v;
15191
+ }],
15192
+ ["--fail-on", (args, v) => {
15193
+ args.failOn = v;
15194
+ }],
15195
+ ["--format", (args, v) => {
15196
+ args.format = v;
15197
+ }],
15198
+ ["--report", (args, v) => {
15199
+ args.report = v;
15200
+ }]
15201
+ ]);
15202
+ var BOOL_FLAGS = /* @__PURE__ */ new Map([
15203
+ ["--quiet", (args) => {
15204
+ args.quiet = true;
15205
+ }],
15206
+ ["--errors-only", (args) => {
15207
+ args.errorsOnly = true;
15208
+ }],
15209
+ ["--no-update-check", (args) => {
15210
+ args.noUpdateCheck = true;
15211
+ }]
15212
+ ]);
15147
15213
  function parseArgs(argv) {
15148
15214
  const args = { paths: [], off: [], format: "text", quiet: false, errorsOnly: false, noUpdateCheck: false };
15149
15215
  for (let i = 0; i < argv.length; i++) {
15150
15216
  const a = argv[i];
15151
- if (a === "--settings") args.settingsFile = argv[++i];
15152
- else if (a === "--off") {
15153
- const list = (argv[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
15154
- for (const r of list) {
15155
- if (!VALID_RULES.has(r)) throw new Error(`--off: unknown rule "${r}" (valid: ${[...VALID_RULES].join(", ")})`);
15156
- args.off.push(r);
15157
- }
15158
- } else if (a === "--schemas-dir") args.schemasDir = argv[++i];
15159
- else if (a === "--fail-on") args.failOn = argv[++i];
15160
- else if (a === "--format") args.format = argv[++i];
15161
- else if (a === "--report") args.report = argv[++i];
15162
- else if (a === "--quiet") args.quiet = true;
15163
- else if (a === "--errors-only") args.errorsOnly = true;
15164
- else if (a === "--no-update-check") args.noUpdateCheck = true;
15165
- else if (a.startsWith("--")) throw new Error(`unknown option: ${a}`);
15166
- else args.paths.push(a);
15217
+ const withValue = VALUE_FLAGS.get(a);
15218
+ if (withValue) {
15219
+ withValue(args, argv[++i]);
15220
+ continue;
15221
+ }
15222
+ const toggle = BOOL_FLAGS.get(a);
15223
+ if (toggle) {
15224
+ toggle(args);
15225
+ continue;
15226
+ }
15227
+ if (a.startsWith("--")) throw new Error(`unknown option: ${a}`);
15228
+ args.paths.push(a);
15167
15229
  }
15168
15230
  if (args.report && args.report !== "rules" && args.report !== "fields") {
15169
15231
  throw new Error(`--report must be "rules" or "fields" (got "${args.report}")`);
@@ -15195,34 +15257,43 @@ function discover(paths, exclude) {
15195
15257
  for (const p of paths) walk(p);
15196
15258
  return files;
15197
15259
  }
15260
+ var KEY_SEP = String.fromCodePoint(0);
15198
15261
  var COLORS = { error: "\x1B[31m", warning: "\x1B[33m", info: "\x1B[36m" };
15199
15262
  var RESET = "\x1B[0m";
15200
15263
  var useColor = process.stdout.isTTY;
15201
15264
  var paint = (s, c) => useColor ? `${c}${s}${RESET}` : s;
15202
- function printText(results, settings, quiet, errorsOnly) {
15203
- for (const r of results) {
15204
- if (r.skipped) continue;
15205
- const msgs = errorsOnly ? r.messages.filter((m) => m.severity === "error") : r.messages;
15206
- if (quiet && msgs.length === 0) continue;
15207
- const rel = r.file ? relative(process.cwd(), r.file) : "<input>";
15208
- const head = `${rel}${r.casetype ? ` (${r.casetype})` : ""}`;
15209
- if (msgs.length === 0) {
15210
- if (!quiet) console.log(`${paint("ok", COLORS.info)} ${head}`);
15211
- continue;
15212
- }
15213
- console.log(head);
15214
- for (const m of msgs) {
15215
- const loc = m.path || "/";
15216
- console.log(` ${paint(m.severity.padEnd(7), COLORS[m.severity])} ${m.rule.padEnd(16)} ${loc} ${m.message}`);
15217
- }
15265
+ function printResult(r, quiet, errorsOnly) {
15266
+ if (r.skipped) return;
15267
+ const msgs = errorsOnly ? r.messages.filter((m) => m.severity === "error") : r.messages;
15268
+ if (quiet && msgs.length === 0) return;
15269
+ const rel = r.file ? relative(process.cwd(), r.file) : "<input>";
15270
+ const casetypeTag = r.casetype ? ` (${r.casetype})` : "";
15271
+ const head = `${rel}${casetypeTag}`;
15272
+ if (msgs.length === 0) {
15273
+ if (!quiet) console.log(`${paint("ok", COLORS.info)} ${head}`);
15274
+ return;
15218
15275
  }
15276
+ console.log(head);
15277
+ for (const m of msgs) {
15278
+ const loc = m.path || "/";
15279
+ console.log(` ${paint(m.severity.padEnd(7), COLORS[m.severity])} ${m.rule.padEnd(16)} ${loc} ${m.message}`);
15280
+ }
15281
+ }
15282
+ function printSummary(results, settings) {
15219
15283
  const s = summarize(results.filter((r) => !r.skipped));
15220
15284
  const skipped = results.filter((r) => r.skipped).length;
15285
+ const skippedNote = skipped ? `, ${skipped} skipped` : "";
15286
+ const errorsText = paint(`${s.errors} error(s)`, COLORS.error);
15287
+ const warningsText = paint(`${s.warnings} warning(s)`, COLORS.warning);
15221
15288
  console.log(
15222
15289
  `
15223
- ${s.files} file(s)${skipped ? `, ${skipped} skipped` : ""} \u2014 ${paint(`${s.errors} error(s)`, COLORS.error)}, ${paint(`${s.warnings} warning(s)`, COLORS.warning)}, ${s.infos} info(s) [fail-on: ${settings.failOn}]`
15290
+ ${s.files} file(s)${skippedNote} \u2014 ${errorsText}, ${warningsText}, ${s.infos} info(s) [fail-on: ${settings.failOn}]`
15224
15291
  );
15225
15292
  }
15293
+ function printText(results, settings, quiet, errorsOnly) {
15294
+ for (const r of results) printResult(r, quiet, errorsOnly);
15295
+ printSummary(results, settings);
15296
+ }
15226
15297
  function printReport(results, kind, errorsOnly) {
15227
15298
  let msgs = results.filter((r) => !r.skipped).flatMap((r) => r.messages);
15228
15299
  if (errorsOnly) msgs = msgs.filter((m) => m.severity === "error");
@@ -15243,7 +15314,7 @@ function printReport(results, kind, errorsOnly) {
15243
15314
  }
15244
15315
  const by = /* @__PURE__ */ new Map();
15245
15316
  for (const m of msgs) {
15246
- const key2 = `${m.casetype ?? "?"}\0${m.rule}\0${m.severity}\0${m.message}`;
15317
+ const key2 = [m.casetype ?? "?", m.rule, m.severity, m.message].join(KEY_SEP);
15247
15318
  const e = by.get(key2) ?? { count: 0, casetype: m.casetype ?? "?", severity: m.severity, message: m.message };
15248
15319
  e.count++;
15249
15320
  by.set(key2, e);
@@ -15257,47 +15328,42 @@ function printReport(results, kind, errorsOnly) {
15257
15328
  }
15258
15329
  }
15259
15330
  var WORKER_FILE = fileURLToPath2(import.meta.url);
15260
- function lintFiles(files, ctx, settings) {
15261
- const out = [];
15262
- const caseRefs = loadCaseRefs(ctx.schemasDir);
15263
- const parseErr = (file, message) => ({
15331
+ function parseErrorResult(file, message, settings) {
15332
+ return {
15264
15333
  file,
15265
15334
  casetype: null,
15266
15335
  skipped: false,
15267
15336
  ok: settings.failOn === "never",
15268
15337
  messages: [{ severity: "error", rule: "parse-error", casetype: null, path: "", field: null, message, file }]
15269
- });
15270
- for (const file of files) {
15271
- const text = readFileSync2(file, "utf8");
15272
- if (!isLintableYaml(text)) continue;
15273
- let docs;
15274
- try {
15275
- docs = (0, import_yaml3.parseAllDocuments)(text);
15276
- } catch (e) {
15277
- out.push(parseErr(file, e.message));
15278
- continue;
15279
- }
15280
- for (const doc of docs) {
15281
- if (doc.errors.length > 0) {
15282
- out.push(parseErr(file, doc.errors[0].message));
15283
- continue;
15284
- }
15285
- const resolved = toJSOrError(doc);
15286
- if (resolved.error !== void 0) {
15287
- out.push(parseErr(file, resolved.error));
15288
- continue;
15289
- }
15290
- const input = { value: resolved.value, file };
15291
- const result = lintDocument({ registry: ctx.registry, coverage: ctx.coverage, settings }, input);
15292
- const refs = referenceFindings(input.value, file, caseRefs, settings);
15293
- if (refs.length) {
15294
- result.messages.push(...refs);
15295
- if (result.ok && refs.some((m) => meetsThreshold(m.severity, settings.failOn))) result.ok = false;
15296
- }
15297
- out.push(result);
15298
- }
15338
+ };
15339
+ }
15340
+ function lintOneDoc(doc, file, ctx, settings, caseRefs) {
15341
+ if (doc.errors.length > 0) return parseErrorResult(file, doc.errors[0].message, settings);
15342
+ const resolved = toJSOrError(doc);
15343
+ if (resolved.error !== void 0) return parseErrorResult(file, resolved.error, settings);
15344
+ const input = { value: resolved.value, file };
15345
+ const result = lintDocument({ registry: ctx.registry, coverage: ctx.coverage, settings }, input);
15346
+ const refs = referenceFindings(input.value, file, caseRefs, settings);
15347
+ if (refs.length) {
15348
+ result.messages.push(...refs);
15349
+ if (result.ok && refs.some((m) => meetsThreshold(m.severity, settings.failOn))) result.ok = false;
15350
+ }
15351
+ return result;
15352
+ }
15353
+ function lintOneFile(file, ctx, settings, caseRefs) {
15354
+ const text = readFileSync2(file, "utf8");
15355
+ if (!isLintableYaml(text)) return [];
15356
+ let docs;
15357
+ try {
15358
+ docs = (0, import_yaml3.parseAllDocuments)(text);
15359
+ } catch (e) {
15360
+ return [parseErrorResult(file, e.message, settings)];
15299
15361
  }
15300
- return out;
15362
+ return docs.map((doc) => lintOneDoc(doc, file, ctx, settings, caseRefs));
15363
+ }
15364
+ function lintFiles(files, ctx, settings) {
15365
+ const caseRefs = loadCaseRefs(ctx.schemasDir);
15366
+ return files.flatMap((file) => lintOneFile(file, ctx, settings, caseRefs));
15301
15367
  }
15302
15368
  async function runParallel(files, settings, schemasDir) {
15303
15369
  const n = Math.min(MAX_WORKERS, availableParallelism(), files.length);
@@ -15363,5 +15429,5 @@ async function main() {
15363
15429
  }
15364
15430
  process.exit(summarize(results.filter((r) => !r.skipped)).exitCode);
15365
15431
  }
15366
- if (isMainThread) void main();
15432
+ if (isMainThread) await main();
15367
15433
  else runWorker();
package/lsp.js CHANGED
@@ -23616,8 +23616,7 @@ function buildCoverageIndex(doc) {
23616
23616
  return {
23617
23617
  clauseCovered(casetype, field, modality, when) {
23618
23618
  if (!casetype || !field) return null;
23619
- const v = map.get(key(casetype, field, modality, when));
23620
- return v === void 0 ? null : v;
23619
+ return map.get(key(casetype, field, modality, when)) ?? null;
23621
23620
  },
23622
23621
  uncoveredCount: (casetype) => uncovered.get(casetype) ?? 0
23623
23622
  };
@@ -23957,12 +23956,108 @@ function meetsThreshold(severity, failOn) {
23957
23956
  function messageKey(m) {
23958
23957
  return `${m.rule}|${m.path}|${m.severity}|${m.message}`;
23959
23958
  }
23959
+ var ANY_OF_REQUIRED = /^(.*\/anyOf)\/\d+\/required$/;
23960
+ function requireAnyKey(e) {
23961
+ if (e.keyword !== "required") return null;
23962
+ const m = ANY_OF_REQUIRED.exec(e.schemaPath);
23963
+ return m ? `${m[1]}@${e.instancePath}` : null;
23964
+ }
23965
+ function collectMapState(errors) {
23966
+ const forbidden = /* @__PURE__ */ new Set();
23967
+ const requireAny = /* @__PURE__ */ new Map();
23968
+ for (const e of errors) {
23969
+ if (e.keyword === "false schema") forbidden.add(e.instancePath);
23970
+ const key2 = requireAnyKey(e);
23971
+ if (key2 === null) continue;
23972
+ const g = requireAny.get(key2) ?? { instancePath: e.instancePath, fields: [], err: e };
23973
+ g.fields.push(String(e.params.missingProperty));
23974
+ requireAny.set(key2, g);
23975
+ }
23976
+ return { forbidden, requireAny, emittedRequireAny: /* @__PURE__ */ new Set() };
23977
+ }
23978
+ function pushRequireAny(e, state, ctx2) {
23979
+ const key2 = requireAnyKey(e);
23980
+ const g = key2 === null ? null : state.requireAny.get(key2);
23981
+ if (!g || g.fields.length < 2) return false;
23982
+ if (!state.emittedRequireAny.has(key2)) {
23983
+ state.emittedRequireAny.add(key2);
23984
+ const where = describeState(ctx2.schema, g.err, ctx2.root, ctx2.resolve);
23985
+ const suffix = where ? ` ${where}` : "";
23986
+ ctx2.push("required-field", g.instancePath, null, `missing at least one of: ${g.fields.join(", ")}${suffix}`);
23987
+ }
23988
+ return true;
23989
+ }
23990
+ function pushValueError(e, loc, ctx2) {
23991
+ const p = e.params;
23992
+ const allowed = e.keyword === "enum" ? p.allowedValues ?? [] : [p.allowedValue];
23993
+ const actual = valueAtPointer(ctx2.root, e.instancePath);
23994
+ const want = caseOnlyMatch(actual, allowed);
23995
+ if (want != null) {
23996
+ ctx2.push("value-case", loc.path, loc.field, `"${String(actual)}" should be "${want}" (case/format mismatch)`);
23997
+ return;
23998
+ }
23999
+ const got = actual === void 0 ? "absent" : JSON.stringify(actual);
24000
+ ctx2.push("invalid-value", loc.path, loc.field, `got ${got} \u2014 ${humanize(e)}`);
24001
+ }
24002
+ function receivedType(v) {
24003
+ if (v === null) return "null";
24004
+ if (Array.isArray(v)) return "array";
24005
+ return typeof v;
24006
+ }
24007
+ function pushTypeError(e, loc, ctx2) {
24008
+ const actual = valueAtPointer(ctx2.root, e.instancePath);
24009
+ let val = JSON.stringify(actual) ?? "";
24010
+ if (val.length > 60) val = `${val.slice(0, 57)}\u2026"`;
24011
+ const expected = String(e.params.type);
24012
+ const withValue = actual === void 0 ? "" : ` with value ${val}`;
24013
+ ctx2.push("invalid-type", loc.path, loc.field, `expected ${expected} but received ${receivedType(actual)}${withValue}`);
24014
+ }
24015
+ function pushSchemaError(e, loc, ctx2) {
24016
+ const rule = ruleForKeyword(e.keyword);
24017
+ let message = humanize(e);
24018
+ if (rule === "forbidden-field" || rule === "required-field") {
24019
+ const where = describeState(ctx2.schema, e, ctx2.root, ctx2.resolve);
24020
+ if (where) message += ` ${where}`;
24021
+ }
24022
+ ctx2.push(rule, loc.path, loc.field, message);
24023
+ }
24024
+ function mapError(e, state, ctx2) {
24025
+ if (META_KEYWORDS.has(e.keyword)) return;
24026
+ if (pushRequireAny(e, state, ctx2)) return;
24027
+ const loc = locate(e);
24028
+ const isAdditional = e.keyword === "additionalProperties" || e.keyword === "unevaluatedProperties";
24029
+ if (isAdditional && state.forbidden.has(loc.path)) return;
24030
+ if (e.keyword === "enum" || e.keyword === "const") pushValueError(e, loc, ctx2);
24031
+ else if (e.keyword === "type") pushTypeError(e, loc, ctx2);
24032
+ else pushSchemaError(e, loc, ctx2);
24033
+ }
24034
+ function mapAjvErrors(errors, ctx2) {
24035
+ const state = collectMapState(errors);
24036
+ for (const e of errors) mapError(e, state, ctx2);
24037
+ }
24038
+ function isSkipped(casetype, settings2) {
24039
+ if (casetype != null && settings2.ignoreCasetypes.includes(casetype)) return true;
24040
+ return settings2.includeCasetypes.length > 0 && (casetype == null || !settings2.includeCasetypes.includes(casetype));
24041
+ }
24042
+ function pushUncovered(push, coverage, casetype) {
24043
+ if (!coverage || !casetype) return;
24044
+ const n = coverage.uncoveredCount(casetype);
24045
+ if (n > 0) push("unknown-casetype", "", casetype, `${n} rule clause(s) for "${casetype}" are behaviour-only and not checked by the schema`);
24046
+ }
24047
+ function dedupeMessages(messages) {
24048
+ const seen = /* @__PURE__ */ new Set();
24049
+ return messages.filter((m) => {
24050
+ const k = messageKey(m);
24051
+ if (seen.has(k)) return false;
24052
+ seen.add(k);
24053
+ return true;
24054
+ });
24055
+ }
23960
24056
  function lintDocument(ctx2, input) {
23961
24057
  const settings2 = normalizeSettings(ctx2.settings);
23962
24058
  const casetype = detectCasetype(input);
24059
+ if (isSkipped(casetype, settings2)) return { file: input.file, casetype, messages: [], skipped: true, ok: true };
23963
24060
  const messages = [];
23964
- const skipped = casetype != null && settings2.ignoreCasetypes.includes(casetype) || settings2.includeCasetypes.length > 0 && (casetype == null || !settings2.includeCasetypes.includes(casetype));
23965
- if (skipped) return { file: input.file, casetype, messages: [], skipped: true, ok: true };
23966
24061
  const push = (rule, path, field, message) => {
23967
24062
  const sev = severityFor(rule, settings2);
23968
24063
  if (sev === "off") return;
@@ -23972,82 +24067,15 @@ function lintDocument(ctx2, input) {
23972
24067
  if (!validate) {
23973
24068
  push("unknown-casetype", "", casetype, casetype ? `no schema for casetype "${casetype}"` : "could not determine casetype (no `_type`)");
23974
24069
  } else if (!validate(input.value)) {
23975
- const errors = validate.errors ?? [];
23976
- const schema = validate.schema;
23977
- const forbidden = /* @__PURE__ */ new Set();
23978
- for (const e of errors) if (e.keyword === "false schema") forbidden.add(e.instancePath);
23979
- const anyOfRe = /^(.*\/anyOf)\/\d+\/required$/;
23980
- const reqAnyGroups = /* @__PURE__ */ new Map();
23981
- for (const e of errors) {
23982
- const m = e.keyword === "required" ? anyOfRe.exec(e.schemaPath) : null;
23983
- if (!m) continue;
23984
- const key2 = `${m[1]}@${e.instancePath}`;
23985
- const g = reqAnyGroups.get(key2) ?? { instancePath: e.instancePath, fields: [], err: e };
23986
- g.fields.push(String(e.params.missingProperty));
23987
- reqAnyGroups.set(key2, g);
23988
- }
23989
- const emittedReqAny = /* @__PURE__ */ new Set();
23990
- for (const e of errors) {
23991
- if (META_KEYWORDS.has(e.keyword)) continue;
23992
- if (e.keyword === "required") {
23993
- const m = anyOfRe.exec(e.schemaPath);
23994
- const key2 = m ? `${m[1]}@${e.instancePath}` : null;
23995
- const g = key2 ? reqAnyGroups.get(key2) : null;
23996
- if (g && g.fields.length >= 2) {
23997
- if (!emittedReqAny.has(key2)) {
23998
- emittedReqAny.add(key2);
23999
- const state = describeState(schema, g.err, input.value, ctx2.registry.resolveRef);
24000
- push("required-field", g.instancePath, null, `missing at least one of: ${g.fields.join(", ")}${state ? ` ${state}` : ""}`);
24001
- }
24002
- continue;
24003
- }
24004
- }
24005
- const { field, path } = locate(e);
24006
- if ((e.keyword === "additionalProperties" || e.keyword === "unevaluatedProperties") && forbidden.has(path)) {
24007
- continue;
24008
- }
24009
- if (e.keyword === "enum" || e.keyword === "const") {
24010
- const p = e.params;
24011
- const allowed = e.keyword === "enum" ? p.allowedValues ?? [] : [p.allowedValue];
24012
- const actual = valueAtPointer(input.value, e.instancePath);
24013
- const want = caseOnlyMatch(actual, allowed);
24014
- if (want != null) {
24015
- push("value-case", path, field, `"${String(actual)}" should be "${want}" (case/format mismatch)`);
24016
- continue;
24017
- }
24018
- const got = actual === void 0 ? "absent" : JSON.stringify(actual);
24019
- push("invalid-value", path, field, `got ${got} \u2014 ${humanize(e)}`);
24020
- continue;
24021
- }
24022
- if (e.keyword === "type") {
24023
- const actual = valueAtPointer(input.value, e.instancePath);
24024
- const recv = actual === null ? "null" : Array.isArray(actual) ? "array" : typeof actual;
24025
- let val = JSON.stringify(actual) ?? "";
24026
- if (val.length > 60) val = `${val.slice(0, 57)}\u2026"`;
24027
- const expected = String(e.params.type);
24028
- push("invalid-type", path, field, `expected ${expected} but received ${recv}${actual === void 0 ? "" : ` with value ${val}`}`);
24029
- continue;
24030
- }
24031
- const rule = ruleForKeyword(e.keyword);
24032
- let message = humanize(e);
24033
- if (rule === "forbidden-field" || rule === "required-field") {
24034
- const state = describeState(schema, e, input.value, ctx2.registry.resolveRef);
24035
- if (state) message += ` ${state}`;
24036
- }
24037
- push(rule, path, field, message);
24038
- }
24039
- }
24040
- if (settings2.reportUncovered && ctx2.coverage && casetype) {
24041
- const n = ctx2.coverage.uncoveredCount(casetype);
24042
- if (n > 0) push("unknown-casetype", "", casetype, `${n} rule clause(s) for "${casetype}" are behaviour-only and not checked by the schema`);
24070
+ mapAjvErrors(validate.errors ?? [], {
24071
+ push,
24072
+ schema: validate.schema,
24073
+ root: input.value,
24074
+ resolve: ctx2.registry.resolveRef
24075
+ });
24043
24076
  }
24044
- const seen = /* @__PURE__ */ new Set();
24045
- const deduped = messages.filter((m) => {
24046
- const k = messageKey(m);
24047
- if (seen.has(k)) return false;
24048
- seen.add(k);
24049
- return true;
24050
- });
24077
+ if (settings2.reportUncovered) pushUncovered(push, ctx2.coverage, casetype);
24078
+ const deduped = dedupeMessages(messages);
24051
24079
  const ok = !deduped.some((m) => meetsThreshold(m.severity, settings2.failOn));
24052
24080
  return { file: input.file, casetype, messages: deduped, skipped: false, ok };
24053
24081
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grexx/grexxlinter",
3
- "version": "0.2.1318",
3
+ "version": "0.2.1408",
4
4
  "description": "Lint Grexx Studio casetype YAML against the studio JSON Schemas.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -250,6 +250,280 @@
250
250
  "title": "widget",
251
251
  "type": "object",
252
252
  "allOf": [
253
+ {
254
+ "$comment": "Shape of the legacy options blob for grids — the only structure it has, since widget.options is declared `additionalProperties: true`. Closed at every level: an unknown key is an additional-field finding. The key set and the types are the union observed over 1275 grid widgets (def-yamls-142 plus three real projects); where the stored spelling and the studio's mapper disagree, both are declared, because both are live.",
255
+ "if": {
256
+ "properties": {
257
+ "widgetType": {
258
+ "enum": [
259
+ "grid",
260
+ "grid-v2"
261
+ ]
262
+ }
263
+ },
264
+ "required": [
265
+ "widgetType"
266
+ ]
267
+ },
268
+ "then": {
269
+ "properties": {
270
+ "options": {
271
+ "type": "object",
272
+ "additionalProperties": false,
273
+ "properties": {
274
+ "type": {
275
+ "type": "string"
276
+ },
277
+ "usesCases": {
278
+ "type": "boolean"
279
+ },
280
+ "dataset": {
281
+ "type": "string"
282
+ },
283
+ "extraclasses": {
284
+ "$comment": "Extra CSS classes on the widget wrapper. No editor writes it; it rides through from the legacy designer.",
285
+ "type": "string"
286
+ },
287
+ "grid": {
288
+ "type": "object",
289
+ "additionalProperties": false,
290
+ "properties": {
291
+ "showHeader": {
292
+ "type": "boolean"
293
+ },
294
+ "title": {
295
+ "type": "string"
296
+ },
297
+ "showToolbar": {
298
+ "type": "boolean"
299
+ },
300
+ "showSimpleSearch": {
301
+ "type": "boolean"
302
+ },
303
+ "showAdvancedSearch": {
304
+ "type": "boolean"
305
+ },
306
+ "showLinenumbers": {
307
+ "type": "boolean"
308
+ },
309
+ "showColumnHeaders": {
310
+ "type": "boolean"
311
+ },
312
+ "showColumnSelector": {
313
+ "type": "boolean"
314
+ },
315
+ "showFooter": {
316
+ "type": "boolean"
317
+ },
318
+ "noFullCount": {
319
+ "type": "boolean"
320
+ },
321
+ "selectColumn": {
322
+ "type": "boolean"
323
+ },
324
+ "autoHeight": {
325
+ "type": "boolean"
326
+ },
327
+ "saveSearch": {
328
+ "type": "string"
329
+ },
330
+ "orderColumn": {
331
+ "type": "string"
332
+ },
333
+ "height": {
334
+ "$comment": "Every stored value is a string (\"640\"); the studio's mapper writes a number on edit.",
335
+ "type": [
336
+ "string",
337
+ "number"
338
+ ]
339
+ },
340
+ "pagination": {
341
+ "type": "object",
342
+ "additionalProperties": false,
343
+ "properties": {
344
+ "rowsPerPage": {
345
+ "type": [
346
+ "number",
347
+ "string"
348
+ ]
349
+ },
350
+ "inifiteScroll": {
351
+ "$comment": "Misspelled in the atalanta runtime that reads it — this is the wire format.",
352
+ "type": "boolean"
353
+ }
354
+ }
355
+ },
356
+ "gridAggregationRowType": {
357
+ "$comment": "Mirror of the first-class gridAggregationRowType; stored as an empty array on all but one of 475 occurrences, so both spellings are allowed.",
358
+ "type": [
359
+ "string",
360
+ "array"
361
+ ]
362
+ },
363
+ "gridAggregationRowPosition": {
364
+ "$comment": "Mirror of the first-class gridAggregationRowPosition; stored as an empty array, as above.",
365
+ "type": [
366
+ "string",
367
+ "array"
368
+ ]
369
+ }
370
+ }
371
+ },
372
+ "row": {
373
+ "type": "object",
374
+ "additionalProperties": false,
375
+ "properties": {
376
+ "clickHandler": {
377
+ "type": "string"
378
+ },
379
+ "clickHandlerColumnCase": {
380
+ "type": "string"
381
+ },
382
+ "rowHeight": {
383
+ "type": [
384
+ "string",
385
+ "number"
386
+ ]
387
+ },
388
+ "viewToOpen": {
389
+ "$comment": "Only read when clickHandler is viewHandler; the studio drops both keys when it is not.",
390
+ "type": "string"
391
+ },
392
+ "customViewModalClasses": {
393
+ "type": "string"
394
+ }
395
+ }
396
+ },
397
+ "subgrid": {
398
+ "type": "object",
399
+ "additionalProperties": false,
400
+ "properties": {
401
+ "enabled": {
402
+ "type": "boolean"
403
+ },
404
+ "height": {
405
+ "type": [
406
+ "string",
407
+ "number"
408
+ ]
409
+ }
410
+ }
411
+ },
412
+ "columns": {
413
+ "$comment": "Keyed by the dataset column's atalanta case id. On a case-converted grid the entry degrades to a caseId pointer at the gridColumn; on a grid that was never converted it keeps the full inline column settings instead, and those 2798 legacy entries are what the types below are read off. `size` stays untyped: it is a nested map whose own key set has no second source to check against.",
414
+ "type": "object",
415
+ "additionalProperties": {
416
+ "type": "object",
417
+ "additionalProperties": false,
418
+ "properties": {
419
+ "caseId": {
420
+ "type": "string"
421
+ },
422
+ "name": {
423
+ "type": "string"
424
+ },
425
+ "column": {
426
+ "type": "string"
427
+ },
428
+ "title": {
429
+ "type": "string"
430
+ },
431
+ "hidden": {
432
+ "type": "boolean"
433
+ },
434
+ "hideable": {
435
+ "type": "boolean"
436
+ },
437
+ "searchable": {
438
+ "type": "boolean"
439
+ },
440
+ "sortable": {
441
+ "type": "boolean"
442
+ },
443
+ "resizable": {
444
+ "type": "boolean"
445
+ },
446
+ "wordwrap": {
447
+ "type": "boolean"
448
+ },
449
+ "valueAsRowClass": {
450
+ "type": "boolean"
451
+ },
452
+ "render": {
453
+ "type": "string"
454
+ },
455
+ "order": {
456
+ "type": [
457
+ "integer",
458
+ "string"
459
+ ]
460
+ },
461
+ "clickHandler": {
462
+ "type": "string"
463
+ },
464
+ "clickHandlerColumnCase": {
465
+ "type": "string"
466
+ },
467
+ "aggregationOperation": {
468
+ "$comment": "Written here by the studio's mapper, but absent from every stored legacy entry — no observed value to type it from, so the key is accepted untyped."
469
+ },
470
+ "size": {
471
+ "type": "object"
472
+ }
473
+ }
474
+ }
475
+ },
476
+ "activities": {
477
+ "$comment": "Keyed by slot — addActivityId / editActivity / finishActivity / multiNN. Unlike columns these keep their full legacy field set alongside the caseId pointer, so every key here is observed and typed.",
478
+ "type": "object",
479
+ "additionalProperties": {
480
+ "type": "object",
481
+ "additionalProperties": false,
482
+ "properties": {
483
+ "caseId": {
484
+ "type": "string"
485
+ },
486
+ "activity": {
487
+ "type": "string"
488
+ },
489
+ "enabled": {
490
+ "type": "boolean"
491
+ },
492
+ "title": {
493
+ "type": "string"
494
+ },
495
+ "useActivityTitle": {
496
+ "type": "boolean"
497
+ },
498
+ "icon": {
499
+ "type": "string"
500
+ },
501
+ "type": {
502
+ "type": "string"
503
+ },
504
+ "checkRights": {
505
+ "type": "string"
506
+ },
507
+ "whenDoneRefreshType": {
508
+ "type": "string"
509
+ },
510
+ "breaker": {
511
+ "type": "boolean"
512
+ },
513
+ "order": {
514
+ "type": [
515
+ "integer",
516
+ "string"
517
+ ]
518
+ }
519
+ }
520
+ }
521
+ }
522
+ }
523
+ }
524
+ }
525
+ }
526
+ },
253
527
  {
254
528
  "if": {
255
529
  "properties": {