@grexx/grexxlinter 0.2.326 → 0.2.348

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.
Files changed (60) hide show
  1. package/cli.js +186 -9
  2. package/lsp.js +122 -5
  3. package/package.json +1 -1
  4. package/schemas/.coverage.json +75 -714
  5. package/schemas/activity.schema.json +0 -19
  6. package/schemas/activityAttribute.schema.json +0 -17
  7. package/schemas/activityRight.schema.json +2 -8
  8. package/schemas/attribute.schema.json +2 -17
  9. package/schemas/casetype.schema.json +0 -1
  10. package/schemas/catchBlock.schema.json +12 -5
  11. package/schemas/conditionList.schema.json +69 -50
  12. package/schemas/container.schema.json +57 -74
  13. package/schemas/customThrow.schema.json +11 -3
  14. package/schemas/dataset.schema.json +0 -52
  15. package/schemas/datasetColumn.schema.json +0 -114
  16. package/schemas/datasetCondition.schema.json +0 -77
  17. package/schemas/datasetRight.schema.json +0 -2
  18. package/schemas/directRole.schema.json +0 -7
  19. package/schemas/folderInfo.schema.json +45 -0
  20. package/schemas/forEach.schema.json +42 -27
  21. package/schemas/formAction.schema.json +1 -17
  22. package/schemas/formActionInlineJavascript.schema.json +49 -54
  23. package/schemas/formGroup.schema.json +71 -82
  24. package/schemas/formRight.schema.json +0 -2
  25. package/schemas/formfield.schema.json +0 -40
  26. package/schemas/gridActivity.schema.json +59 -78
  27. package/schemas/gridColumn.schema.json +107 -144
  28. package/schemas/ifthenelse.schema.json +0 -10
  29. package/schemas/indirectRole.schema.json +0 -7
  30. package/schemas/jobStep.schema.json +47 -64
  31. package/schemas/mapping.schema.json +33 -43
  32. package/schemas/menuItem.schema.json +43 -56
  33. package/schemas/menuItemRight.schema.json +45 -71
  34. package/schemas/metadata.schema.json +3 -18
  35. package/schemas/notCondition.schema.json +45 -66
  36. package/schemas/parallelBlock.schema.json +55 -60
  37. package/schemas/picklist.schema.json +1 -2
  38. package/schemas/picklistItem.schema.json +49 -55
  39. package/schemas/platformAttribute.schema.json +0 -22
  40. package/schemas/platformRole.schema.json +0 -11
  41. package/schemas/plugin.schema.json +71 -77
  42. package/schemas/pluginLibrary.schema.json +0 -2
  43. package/schemas/requestContext.schema.json +10 -0
  44. package/schemas/securityProfile.schema.json +53 -47
  45. package/schemas/securityRequirement.schema.json +52 -47
  46. package/schemas/simpleCondition.schema.json +0 -10
  47. package/schemas/template/date-function.schema.json +1 -0
  48. package/schemas/template/html.schema.json +1 -0
  49. package/schemas/template/if.schema.json +1 -0
  50. package/schemas/template/iterator.schema.json +1 -0
  51. package/schemas/template/multivalue.schema.json +1 -0
  52. package/schemas/template/picklist.schema.json +1 -0
  53. package/schemas/template/util.schema.json +4 -0
  54. package/schemas/template.schema.json +1 -2
  55. package/schemas/trigger.schema.json +131 -68
  56. package/schemas/tryBlock.schema.json +12 -5
  57. package/schemas/view.schema.json +0 -17
  58. package/schemas/while.schema.json +23 -43
  59. package/schemas/widget.schema.json +0 -463
  60. package/schemas/widgetRight.schema.json +1 -2
package/cli.js CHANGED
@@ -14518,9 +14518,11 @@ function createSchemaRegistry(schemas) {
14518
14518
  validateSchema: false
14519
14519
  });
14520
14520
  const casetypes = [];
14521
+ const byId = /* @__PURE__ */ new Map();
14521
14522
  for (const schema of schemas) {
14522
14523
  const id = typeof schema.$id === "string" ? schema.$id : void 0;
14523
14524
  if (!id) continue;
14525
+ byId.set(id, schema);
14524
14526
  if (!ajv.getSchema(id)) ajv.addSchema(schema, id);
14525
14527
  const m = /^https:\/\/grexx\.net\/studio-next\/schemas\/([^/]+)\.schema\.json$/.exec(id);
14526
14528
  if (m) casetypes.push(m[1]);
@@ -14538,10 +14540,32 @@ function createSchemaRegistry(schemas) {
14538
14540
  cache.set(casetype, fn);
14539
14541
  return fn;
14540
14542
  }
14543
+ function resolveRef(ref, base) {
14544
+ const hash = ref.indexOf("#");
14545
+ const path = hash === -1 ? ref : ref.slice(0, hash);
14546
+ const frag = hash === -1 ? "" : ref.slice(hash + 1);
14547
+ let schema;
14548
+ if (path === "") {
14549
+ schema = base;
14550
+ } else {
14551
+ const file = path.replace(/^\.\//, "");
14552
+ schema = byId.get(file.startsWith("http") ? file : `${SCHEMA_ID_BASE}${file}`);
14553
+ }
14554
+ if (!schema) return null;
14555
+ if (!frag) return schema;
14556
+ let cur = schema;
14557
+ for (const raw of frag.split("/").filter(Boolean)) {
14558
+ const key2 = raw.replace(/~1/g, "/").replace(/~0/g, "~");
14559
+ if (cur == null || typeof cur !== "object") return null;
14560
+ cur = cur[key2];
14561
+ }
14562
+ return cur ?? null;
14563
+ }
14541
14564
  return {
14542
14565
  casetypes,
14543
14566
  has: (casetype) => validatorFor(casetype) != null,
14544
- validatorFor
14567
+ validatorFor,
14568
+ resolveRef
14545
14569
  };
14546
14570
  }
14547
14571
 
@@ -14590,7 +14614,7 @@ var import_yaml = __toESM(require_dist(), 1);
14590
14614
  var DEFAULT_RULE_SEVERITY = {
14591
14615
  "required-field": "error",
14592
14616
  "forbidden-field": "error",
14593
- "unknown-field": "error",
14617
+ "additional-field": "warning",
14594
14618
  "invalid-value": "error",
14595
14619
  "value-case": "warning",
14596
14620
  "invalid-type": "error",
@@ -14608,6 +14632,18 @@ var DEFAULT_LINTER_SETTINGS = {
14608
14632
  include: ["**/*.yaml", "**/*.yml"],
14609
14633
  exclude: ["node_modules", "dist", ".git"]
14610
14634
  };
14635
+ var LINT_RULE_DESCRIPTORS = [
14636
+ { id: "required-field", label: "Required field", description: "A field the casetype state requires (MUST) is missing.", defaultSeverity: "error" },
14637
+ { id: "forbidden-field", label: "Forbidden field", description: "A field present that the casetype state forbids (MUST_NOT).", defaultSeverity: "error" },
14638
+ { id: "additional-field", label: "Additional field", description: "A field not defined by the schema. Allowed in principle, but flagged for review.", defaultSeverity: "warning" },
14639
+ { id: "invalid-value", label: "Invalid value", description: "Value not allowed by an enum / const.", defaultSeverity: "error" },
14640
+ { id: "value-case", label: "Value case mismatch", description: "Value matches an allowed option except for case / camelCase / separators (e.g. FORM vs form).", defaultSeverity: "warning" },
14641
+ { id: "invalid-type", label: "Invalid type", description: "Value has the wrong JSON type.", defaultSeverity: "error" },
14642
+ { id: "invalid-format", label: "Invalid format", description: "Value fails a pattern / format constraint.", defaultSeverity: "error" },
14643
+ { id: "unknown-casetype", label: "Unknown casetype", description: "No schema found for the document `_type`.", defaultSeverity: "warning" },
14644
+ { id: "parse-error", label: "Parse error", description: "The YAML document could not be parsed.", defaultSeverity: "error" },
14645
+ { id: "schema", label: "Other schema error", description: "Any other constraint reported by the validator.", defaultSeverity: "error" }
14646
+ ];
14611
14647
  function normalizeSettings(partial) {
14612
14648
  return {
14613
14649
  ...DEFAULT_LINTER_SETTINGS,
@@ -14628,7 +14664,7 @@ function ruleForKeyword(keyword) {
14628
14664
  return "forbidden-field";
14629
14665
  case "additionalProperties":
14630
14666
  case "unevaluatedProperties":
14631
- return "unknown-field";
14667
+ return "additional-field";
14632
14668
  case "const":
14633
14669
  case "enum":
14634
14670
  return "invalid-value";
@@ -14656,7 +14692,7 @@ function humanize(err) {
14656
14692
  return `field "${lastSegment(err.instancePath) ?? ""}" is not allowed for this casetype state`;
14657
14693
  case "additionalProperties":
14658
14694
  case "unevaluatedProperties":
14659
- return `unknown field "${String(p.additionalProperty)}"`;
14695
+ return `additional field "${String(p.additionalProperty)}" not defined by the schema`;
14660
14696
  case "const":
14661
14697
  return `must equal ${JSON.stringify(p.allowedValue)}`;
14662
14698
  case "enum":
@@ -14683,6 +14719,92 @@ function locate(err) {
14683
14719
  }
14684
14720
  return { field: lastSegment(err.instancePath), path: err.instancePath };
14685
14721
  }
14722
+ function collectConsts(cond, out = /* @__PURE__ */ new Map()) {
14723
+ if (!cond || typeof cond !== "object") return out;
14724
+ const c = cond;
14725
+ const props = c.properties;
14726
+ if (props) {
14727
+ for (const [k, sub] of Object.entries(props)) {
14728
+ if (sub && typeof sub === "object" && "const" in sub) {
14729
+ (out.get(k) ?? out.set(k, /* @__PURE__ */ new Set()).get(k)).add(sub.const);
14730
+ }
14731
+ }
14732
+ }
14733
+ for (const key2 of ["allOf", "anyOf", "oneOf"]) {
14734
+ if (Array.isArray(c[key2])) c[key2].forEach((s) => collectConsts(s, out));
14735
+ }
14736
+ return out;
14737
+ }
14738
+ function parentPointer(pointer) {
14739
+ const i = pointer.lastIndexOf("/");
14740
+ return i <= 0 ? "" : pointer.slice(0, i);
14741
+ }
14742
+ function fmtStateValue(v) {
14743
+ if (v === void 0 || v === null || v === "") return "null";
14744
+ if (typeof v === "string") return v;
14745
+ if (typeof v === "object") return Array.isArray(v) ? `[${v.length}]` : "{\u2026}";
14746
+ return String(v);
14747
+ }
14748
+ function deref(schema, resolve2, docRoot) {
14749
+ let cur = schema;
14750
+ for (let i = 0; cur && typeof cur === "object" && typeof cur.$ref === "string" && i < 16; i++) {
14751
+ const ref = cur.$ref;
14752
+ const next = resolve2(ref, docRoot.v ?? void 0);
14753
+ if (!next) break;
14754
+ if (!ref.startsWith("#")) docRoot.v = next;
14755
+ cur = next;
14756
+ }
14757
+ return cur ?? null;
14758
+ }
14759
+ function propSchema(schema, key2) {
14760
+ const direct = schema.properties;
14761
+ if (direct && key2 in direct) return direct[key2];
14762
+ for (const branch of schema.allOf ?? []) {
14763
+ const t = branch.then ?? branch;
14764
+ const props = t.properties;
14765
+ if (props && key2 in props) return props[key2];
14766
+ }
14767
+ return null;
14768
+ }
14769
+ function schemaAtPointer(rootSchema, pointer, resolve2) {
14770
+ const docRoot = { v: rootSchema };
14771
+ let schema = deref(rootSchema, resolve2, docRoot);
14772
+ for (const raw of pointer.split("/").filter(Boolean)) {
14773
+ if (!schema) return null;
14774
+ if (/^\d+$/.test(raw)) {
14775
+ let items = schema.items;
14776
+ if (Array.isArray(items)) items = items[Number(raw)] ?? items[0];
14777
+ schema = deref(items, resolve2, docRoot);
14778
+ } else {
14779
+ const key2 = raw.replace(/~1/g, "/").replace(/~0/g, "~");
14780
+ schema = deref(propSchema(schema, key2), resolve2, docRoot);
14781
+ }
14782
+ }
14783
+ return schema;
14784
+ }
14785
+ function describeState(rootSchema, err, root, resolve2) {
14786
+ const m = /^#\/allOf\/(\d+)\/(then|else)\b/.exec(err.schemaPath);
14787
+ if (!m || !rootSchema || typeof rootSchema !== "object") return null;
14788
+ const branch = m[2];
14789
+ const objPath = err.keyword === "false schema" ? parentPointer(err.instancePath) : err.instancePath;
14790
+ const govSchema = schemaAtPointer(rootSchema, objPath, resolve2);
14791
+ const allOf = govSchema?.allOf;
14792
+ const clause = Array.isArray(allOf) ? allOf[Number(m[1])] : void 0;
14793
+ if (!clause?.if) return null;
14794
+ const consts = collectConsts(clause.if);
14795
+ if (consts.size === 0) return null;
14796
+ const obj = valueAtPointer(root, objPath);
14797
+ if (!obj || typeof obj !== "object") return null;
14798
+ const rec = obj;
14799
+ if (branch === "then") {
14800
+ for (const [field, values] of consts) if (!values.has(rec[field])) return null;
14801
+ const parts2 = [...consts].map(([f, vs]) => `${f}=${fmtStateValue([...vs][0])}`);
14802
+ return `(${parts2.join(", ")})`;
14803
+ }
14804
+ for (const field of consts.keys()) if (!(field in rec)) return null;
14805
+ const parts = [...consts.keys()].map((f) => `${f}=${fmtStateValue(rec[f])}`);
14806
+ return `(${parts.join(", ")})`;
14807
+ }
14686
14808
  function valueAtPointer(root, pointer) {
14687
14809
  if (!pointer) return root;
14688
14810
  let cur = root;
@@ -14734,6 +14856,7 @@ function lintDocument(ctx, input) {
14734
14856
  push("unknown-casetype", "", casetype, casetype ? `no schema for casetype "${casetype}"` : "could not determine casetype (no `_type`)");
14735
14857
  } else if (!validate(input.value)) {
14736
14858
  const errors = validate.errors ?? [];
14859
+ const schema = validate.schema;
14737
14860
  const forbidden = /* @__PURE__ */ new Set();
14738
14861
  for (const e of errors) if (e.keyword === "false schema") forbidden.add(e.instancePath);
14739
14862
  for (const e of errors) {
@@ -14752,7 +14875,13 @@ function lintDocument(ctx, input) {
14752
14875
  continue;
14753
14876
  }
14754
14877
  }
14755
- push(ruleForKeyword(e.keyword), path, field, humanize(e));
14878
+ const rule = ruleForKeyword(e.keyword);
14879
+ let message = humanize(e);
14880
+ if (rule === "forbidden-field" || rule === "required-field") {
14881
+ const state = describeState(schema, e, input.value, ctx.registry.resolveRef);
14882
+ if (state) message += ` ${state}`;
14883
+ }
14884
+ push(rule, path, field, message);
14756
14885
  }
14757
14886
  }
14758
14887
  if (settings.reportUncovered && ctx.coverage && casetype) {
@@ -14793,7 +14922,7 @@ function isLintableYaml(text) {
14793
14922
  // src/cli.ts
14794
14923
  var PARALLEL_THRESHOLD = 200;
14795
14924
  var MAX_WORKERS = 8;
14796
- var VERSION = true ? "0.2.326" : "0.0.0-dev";
14925
+ var VERSION = true ? "0.2.348" : "0.0.0-dev";
14797
14926
  function isNewer(latest, current) {
14798
14927
  const a = latest.split(".").map(Number);
14799
14928
  const b = current.split(".").map(Number);
@@ -14812,19 +14941,30 @@ function checkForUpdate(disabled) {
14812
14941
  const timer = setTimeout(() => ctrl.abort(), 1500);
14813
14942
  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));
14814
14943
  }
14944
+ var VALID_RULES = new Set(LINT_RULE_DESCRIPTORS.map((d) => d.id));
14815
14945
  function parseArgs(argv) {
14816
- const args = { paths: [], format: "text", quiet: false, noUpdateCheck: false };
14946
+ const args = { paths: [], off: [], format: "text", quiet: false, noUpdateCheck: false };
14817
14947
  for (let i = 0; i < argv.length; i++) {
14818
14948
  const a = argv[i];
14819
14949
  if (a === "--settings") args.settingsFile = argv[++i];
14820
- else if (a === "--schemas-dir") args.schemasDir = argv[++i];
14950
+ else if (a === "--off") {
14951
+ const list = (argv[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
14952
+ for (const r of list) {
14953
+ if (!VALID_RULES.has(r)) throw new Error(`--off: unknown rule "${r}" (valid: ${[...VALID_RULES].join(", ")})`);
14954
+ args.off.push(r);
14955
+ }
14956
+ } else if (a === "--schemas-dir") args.schemasDir = argv[++i];
14821
14957
  else if (a === "--fail-on") args.failOn = argv[++i];
14822
14958
  else if (a === "--format") args.format = argv[++i];
14959
+ else if (a === "--report") args.report = argv[++i];
14823
14960
  else if (a === "--quiet") args.quiet = true;
14824
14961
  else if (a === "--no-update-check") args.noUpdateCheck = true;
14825
14962
  else if (a.startsWith("--")) throw new Error(`unknown option: ${a}`);
14826
14963
  else args.paths.push(a);
14827
14964
  }
14965
+ if (args.report && args.report !== "rules" && args.report !== "fields") {
14966
+ throw new Error(`--report must be "rules" or "fields" (got "${args.report}")`);
14967
+ }
14828
14968
  if (args.paths.length === 0) args.paths.push(".");
14829
14969
  return args;
14830
14970
  }
@@ -14879,6 +15019,38 @@ function printText(results, settings, quiet) {
14879
15019
  ${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}]`
14880
15020
  );
14881
15021
  }
15022
+ function printReport(results, kind) {
15023
+ const msgs = results.filter((r) => !r.skipped).flatMap((r) => r.messages);
15024
+ if (kind === "rules") {
15025
+ const by2 = /* @__PURE__ */ new Map();
15026
+ for (const m of msgs) {
15027
+ const e = by2.get(m.rule) ?? { count: 0, severity: m.severity };
15028
+ e.count++;
15029
+ by2.set(m.rule, e);
15030
+ }
15031
+ const rows2 = [...by2.entries()].map(([rule, e]) => ({ count: e.count, rule, severity: e.severity })).sort((a, b) => b.count - a.count);
15032
+ const w = Math.max(5, ...rows2.map((r) => String(r.count).length));
15033
+ console.log(`${"count".padStart(w)} rule severity`);
15034
+ for (const r of rows2) {
15035
+ console.log(`${paint(String(r.count).padStart(w), COLORS[r.severity])} ${r.rule.padEnd(16)} ${r.severity}`);
15036
+ }
15037
+ return;
15038
+ }
15039
+ const by = /* @__PURE__ */ new Map();
15040
+ for (const m of msgs) {
15041
+ const key2 = `${m.casetype ?? "?"}\0${m.rule}\0${m.severity}\0${m.message}`;
15042
+ const e = by.get(key2) ?? { count: 0, casetype: m.casetype ?? "?", severity: m.severity, message: m.message };
15043
+ e.count++;
15044
+ by.set(key2, e);
15045
+ }
15046
+ const rows = [...by.values()].sort((a, b) => b.count - a.count || a.casetype.localeCompare(b.casetype));
15047
+ const cw = Math.max(5, ...rows.map((r) => String(r.count).length));
15048
+ const tw = Math.max(8, ...rows.map((r) => r.casetype.length));
15049
+ console.log(`${"count".padStart(cw)} ${"casetype".padEnd(tw)} severity message`);
15050
+ for (const r of rows) {
15051
+ console.log(`${paint(String(r.count).padStart(cw), COLORS[r.severity])} ${r.casetype.padEnd(tw)} ${r.severity.padEnd(8)} ${r.message}`);
15052
+ }
15053
+ }
14882
15054
  var WORKER_FILE = fileURLToPath2(import.meta.url);
14883
15055
  function lintFiles(files, ctx, settings) {
14884
15056
  const out = [];
@@ -14946,6 +15118,9 @@ async function main() {
14946
15118
  let partial = {};
14947
15119
  if (args.settingsFile) partial = JSON.parse(readFileSync2(args.settingsFile, "utf8"));
14948
15120
  if (args.failOn) partial = { ...partial, failOn: args.failOn };
15121
+ if (args.off.length > 0) {
15122
+ partial = { ...partial, rules: { ...partial.rules, ...Object.fromEntries(args.off.map((r) => [r, "off"])) } };
15123
+ }
14949
15124
  const settings = normalizeSettings(partial);
14950
15125
  const files = discover(args.paths, settings.exclude);
14951
15126
  const canParallel = WORKER_FILE.endsWith(".js") && files.length > PARALLEL_THRESHOLD;
@@ -14956,7 +15131,9 @@ async function main() {
14956
15131
  const ctx = createNodeContext({ schemasDir: args.schemasDir });
14957
15132
  results = lintFiles(files, ctx, settings);
14958
15133
  }
14959
- if (args.format === "json") {
15134
+ if (args.report) {
15135
+ printReport(results, args.report);
15136
+ } else if (args.format === "json") {
14960
15137
  const active = results.filter((r) => !r.skipped);
14961
15138
  console.log(JSON.stringify({ summary: summarize(active), results }, null, 2));
14962
15139
  } else {
package/lsp.js CHANGED
@@ -23587,9 +23587,11 @@ function createSchemaRegistry(schemas) {
23587
23587
  validateSchema: false
23588
23588
  });
23589
23589
  const casetypes = [];
23590
+ const byId = /* @__PURE__ */ new Map();
23590
23591
  for (const schema of schemas) {
23591
23592
  const id = typeof schema.$id === "string" ? schema.$id : void 0;
23592
23593
  if (!id) continue;
23594
+ byId.set(id, schema);
23593
23595
  if (!ajv.getSchema(id)) ajv.addSchema(schema, id);
23594
23596
  const m = /^https:\/\/grexx\.net\/studio-next\/schemas\/([^/]+)\.schema\.json$/.exec(id);
23595
23597
  if (m) casetypes.push(m[1]);
@@ -23607,10 +23609,32 @@ function createSchemaRegistry(schemas) {
23607
23609
  cache.set(casetype, fn);
23608
23610
  return fn;
23609
23611
  }
23612
+ function resolveRef(ref, base) {
23613
+ const hash = ref.indexOf("#");
23614
+ const path = hash === -1 ? ref : ref.slice(0, hash);
23615
+ const frag = hash === -1 ? "" : ref.slice(hash + 1);
23616
+ let schema;
23617
+ if (path === "") {
23618
+ schema = base;
23619
+ } else {
23620
+ const file = path.replace(/^\.\//, "");
23621
+ schema = byId.get(file.startsWith("http") ? file : `${SCHEMA_ID_BASE}${file}`);
23622
+ }
23623
+ if (!schema) return null;
23624
+ if (!frag) return schema;
23625
+ let cur = schema;
23626
+ for (const raw of frag.split("/").filter(Boolean)) {
23627
+ const key2 = raw.replace(/~1/g, "/").replace(/~0/g, "~");
23628
+ if (cur == null || typeof cur !== "object") return null;
23629
+ cur = cur[key2];
23630
+ }
23631
+ return cur ?? null;
23632
+ }
23610
23633
  return {
23611
23634
  casetypes,
23612
23635
  has: (casetype) => validatorFor(casetype) != null,
23613
- validatorFor
23636
+ validatorFor,
23637
+ resolveRef
23614
23638
  };
23615
23639
  }
23616
23640
 
@@ -23656,7 +23680,7 @@ function createNodeContext(opts = {}) {
23656
23680
  var DEFAULT_RULE_SEVERITY = {
23657
23681
  "required-field": "error",
23658
23682
  "forbidden-field": "error",
23659
- "unknown-field": "error",
23683
+ "additional-field": "warning",
23660
23684
  "invalid-value": "error",
23661
23685
  "value-case": "warning",
23662
23686
  "invalid-type": "error",
@@ -23694,7 +23718,7 @@ function ruleForKeyword(keyword) {
23694
23718
  return "forbidden-field";
23695
23719
  case "additionalProperties":
23696
23720
  case "unevaluatedProperties":
23697
- return "unknown-field";
23721
+ return "additional-field";
23698
23722
  case "const":
23699
23723
  case "enum":
23700
23724
  return "invalid-value";
@@ -23722,7 +23746,7 @@ function humanize(err) {
23722
23746
  return `field "${lastSegment(err.instancePath) ?? ""}" is not allowed for this casetype state`;
23723
23747
  case "additionalProperties":
23724
23748
  case "unevaluatedProperties":
23725
- return `unknown field "${String(p.additionalProperty)}"`;
23749
+ return `additional field "${String(p.additionalProperty)}" not defined by the schema`;
23726
23750
  case "const":
23727
23751
  return `must equal ${JSON.stringify(p.allowedValue)}`;
23728
23752
  case "enum":
@@ -23749,6 +23773,92 @@ function locate(err) {
23749
23773
  }
23750
23774
  return { field: lastSegment(err.instancePath), path: err.instancePath };
23751
23775
  }
23776
+ function collectConsts(cond, out = /* @__PURE__ */ new Map()) {
23777
+ if (!cond || typeof cond !== "object") return out;
23778
+ const c = cond;
23779
+ const props = c.properties;
23780
+ if (props) {
23781
+ for (const [k, sub] of Object.entries(props)) {
23782
+ if (sub && typeof sub === "object" && "const" in sub) {
23783
+ (out.get(k) ?? out.set(k, /* @__PURE__ */ new Set()).get(k)).add(sub.const);
23784
+ }
23785
+ }
23786
+ }
23787
+ for (const key2 of ["allOf", "anyOf", "oneOf"]) {
23788
+ if (Array.isArray(c[key2])) c[key2].forEach((s) => collectConsts(s, out));
23789
+ }
23790
+ return out;
23791
+ }
23792
+ function parentPointer(pointer) {
23793
+ const i = pointer.lastIndexOf("/");
23794
+ return i <= 0 ? "" : pointer.slice(0, i);
23795
+ }
23796
+ function fmtStateValue(v) {
23797
+ if (v === void 0 || v === null || v === "") return "null";
23798
+ if (typeof v === "string") return v;
23799
+ if (typeof v === "object") return Array.isArray(v) ? `[${v.length}]` : "{\u2026}";
23800
+ return String(v);
23801
+ }
23802
+ function deref(schema, resolve2, docRoot) {
23803
+ let cur = schema;
23804
+ for (let i = 0; cur && typeof cur === "object" && typeof cur.$ref === "string" && i < 16; i++) {
23805
+ const ref = cur.$ref;
23806
+ const next = resolve2(ref, docRoot.v ?? void 0);
23807
+ if (!next) break;
23808
+ if (!ref.startsWith("#")) docRoot.v = next;
23809
+ cur = next;
23810
+ }
23811
+ return cur ?? null;
23812
+ }
23813
+ function propSchema(schema, key2) {
23814
+ const direct = schema.properties;
23815
+ if (direct && key2 in direct) return direct[key2];
23816
+ for (const branch of schema.allOf ?? []) {
23817
+ const t = branch.then ?? branch;
23818
+ const props = t.properties;
23819
+ if (props && key2 in props) return props[key2];
23820
+ }
23821
+ return null;
23822
+ }
23823
+ function schemaAtPointer(rootSchema, pointer, resolve2) {
23824
+ const docRoot = { v: rootSchema };
23825
+ let schema = deref(rootSchema, resolve2, docRoot);
23826
+ for (const raw of pointer.split("/").filter(Boolean)) {
23827
+ if (!schema) return null;
23828
+ if (/^\d+$/.test(raw)) {
23829
+ let items = schema.items;
23830
+ if (Array.isArray(items)) items = items[Number(raw)] ?? items[0];
23831
+ schema = deref(items, resolve2, docRoot);
23832
+ } else {
23833
+ const key2 = raw.replace(/~1/g, "/").replace(/~0/g, "~");
23834
+ schema = deref(propSchema(schema, key2), resolve2, docRoot);
23835
+ }
23836
+ }
23837
+ return schema;
23838
+ }
23839
+ function describeState(rootSchema, err, root, resolve2) {
23840
+ const m = /^#\/allOf\/(\d+)\/(then|else)\b/.exec(err.schemaPath);
23841
+ if (!m || !rootSchema || typeof rootSchema !== "object") return null;
23842
+ const branch = m[2];
23843
+ const objPath = err.keyword === "false schema" ? parentPointer(err.instancePath) : err.instancePath;
23844
+ const govSchema = schemaAtPointer(rootSchema, objPath, resolve2);
23845
+ const allOf = govSchema?.allOf;
23846
+ const clause = Array.isArray(allOf) ? allOf[Number(m[1])] : void 0;
23847
+ if (!clause?.if) return null;
23848
+ const consts = collectConsts(clause.if);
23849
+ if (consts.size === 0) return null;
23850
+ const obj = valueAtPointer(root, objPath);
23851
+ if (!obj || typeof obj !== "object") return null;
23852
+ const rec = obj;
23853
+ if (branch === "then") {
23854
+ for (const [field, values] of consts) if (!values.has(rec[field])) return null;
23855
+ const parts2 = [...consts].map(([f, vs]) => `${f}=${fmtStateValue([...vs][0])}`);
23856
+ return `(${parts2.join(", ")})`;
23857
+ }
23858
+ for (const field of consts.keys()) if (!(field in rec)) return null;
23859
+ const parts = [...consts.keys()].map((f) => `${f}=${fmtStateValue(rec[f])}`);
23860
+ return `(${parts.join(", ")})`;
23861
+ }
23752
23862
  function valueAtPointer(root, pointer) {
23753
23863
  if (!pointer) return root;
23754
23864
  let cur = root;
@@ -23800,6 +23910,7 @@ function lintDocument(ctx2, input) {
23800
23910
  push("unknown-casetype", "", casetype, casetype ? `no schema for casetype "${casetype}"` : "could not determine casetype (no `_type`)");
23801
23911
  } else if (!validate(input.value)) {
23802
23912
  const errors = validate.errors ?? [];
23913
+ const schema = validate.schema;
23803
23914
  const forbidden = /* @__PURE__ */ new Set();
23804
23915
  for (const e of errors) if (e.keyword === "false schema") forbidden.add(e.instancePath);
23805
23916
  for (const e of errors) {
@@ -23818,7 +23929,13 @@ function lintDocument(ctx2, input) {
23818
23929
  continue;
23819
23930
  }
23820
23931
  }
23821
- push(ruleForKeyword(e.keyword), path, field, humanize(e));
23932
+ const rule = ruleForKeyword(e.keyword);
23933
+ let message = humanize(e);
23934
+ if (rule === "forbidden-field" || rule === "required-field") {
23935
+ const state = describeState(schema, e, input.value, ctx2.registry.resolveRef);
23936
+ if (state) message += ` ${state}`;
23937
+ }
23938
+ push(rule, path, field, message);
23822
23939
  }
23823
23940
  }
23824
23941
  if (settings2.reportUncovered && ctx2.coverage && casetype) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grexx/grexxlinter",
3
- "version": "0.2.326",
3
+ "version": "0.2.348",
4
4
  "description": "Lint Grexx Studio casetype YAML against the studio JSON Schemas.",
5
5
  "type": "module",
6
6
  "bin": {