@grexx/grexxlinter 0.2.348 → 0.2.351

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
@@ -14456,7 +14456,7 @@ var require__ = __commonJS({
14456
14456
  });
14457
14457
 
14458
14458
  // src/cli.ts
14459
- var import_yaml2 = __toESM(require_dist(), 1);
14459
+ var import_yaml3 = __toESM(require_dist(), 1);
14460
14460
  import { readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
14461
14461
  import { availableParallelism } from "node:os";
14462
14462
  import { extname, join as join2, relative } from "node:path";
@@ -14464,6 +14464,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
14464
14464
  import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
14465
14465
 
14466
14466
  // src/node.ts
14467
+ var import_yaml = __toESM(require_dist(), 1);
14467
14468
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
14468
14469
  import { createRequire } from "node:module";
14469
14470
  import { dirname, join, resolve } from "node:path";
@@ -14569,47 +14570,6 @@ function createSchemaRegistry(schemas) {
14569
14570
  };
14570
14571
  }
14571
14572
 
14572
- // src/node.ts
14573
- var require2 = createRequire(import.meta.url);
14574
- function resolveSchemasDir(override) {
14575
- if (override) return resolve(override);
14576
- if (process.env.SCHEMAS_DIR) return resolve(process.env.SCHEMAS_DIR);
14577
- const bundled = resolve(dirname(fileURLToPath(import.meta.url)), "schemas");
14578
- if (existsSync(bundled)) return bundled;
14579
- const ref = require2.resolve("@studio/schemas/extended/casetype.schema.json");
14580
- return dirname(ref);
14581
- }
14582
- function loadSchemasFromDir(dir) {
14583
- const out = [];
14584
- const walk = (d) => {
14585
- for (const entry of readdirSync(d, { withFileTypes: true })) {
14586
- const p = join(d, entry.name);
14587
- if (entry.isDirectory()) walk(p);
14588
- else if (entry.name.endsWith(".schema.json")) out.push(JSON.parse(readFileSync(p, "utf8")));
14589
- }
14590
- };
14591
- walk(dir);
14592
- return out;
14593
- }
14594
- function loadCoverage(dir) {
14595
- const file = join(dir, ".coverage.json");
14596
- if (!existsSync(file) || !statSync(file).isFile()) return null;
14597
- try {
14598
- return buildCoverageIndex(JSON.parse(readFileSync(file, "utf8")));
14599
- } catch {
14600
- return null;
14601
- }
14602
- }
14603
- function createNodeContext(opts = {}) {
14604
- const schemasDir = resolveSchemasDir(opts.schemasDir);
14605
- const registry = createSchemaRegistry(loadSchemasFromDir(schemasDir));
14606
- const coverage = loadCoverage(schemasDir);
14607
- return { registry, coverage, schemasDir };
14608
- }
14609
-
14610
- // src/yaml.ts
14611
- var import_yaml = __toESM(require_dist(), 1);
14612
-
14613
14573
  // src/types.ts
14614
14574
  var DEFAULT_RULE_SEVERITY = {
14615
14575
  "required-field": "error",
@@ -14619,6 +14579,7 @@ var DEFAULT_RULE_SEVERITY = {
14619
14579
  "value-case": "warning",
14620
14580
  "invalid-type": "error",
14621
14581
  "invalid-format": "error",
14582
+ "invalid-reference": "error",
14622
14583
  "unknown-casetype": "warning",
14623
14584
  "parse-error": "error",
14624
14585
  schema: "error"
@@ -14640,6 +14601,7 @@ var LINT_RULE_DESCRIPTORS = [
14640
14601
  { 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
14602
  { id: "invalid-type", label: "Invalid type", description: "Value has the wrong JSON type.", defaultSeverity: "error" },
14642
14603
  { id: "invalid-format", label: "Invalid format", description: "Value fails a pattern / format constraint.", defaultSeverity: "error" },
14604
+ { id: "invalid-reference", label: "Invalid reference", description: "A case-reference field points at a file whose `_type` is not in the field's allowedCasetypes.", defaultSeverity: "error" },
14643
14605
  { id: "unknown-casetype", label: "Unknown casetype", description: "No schema found for the document `_type`.", defaultSeverity: "warning" },
14644
14606
  { id: "parse-error", label: "Parse error", description: "The YAML document could not be parsed.", defaultSeverity: "error" },
14645
14607
  { id: "schema", label: "Other schema error", description: "Any other constraint reported by the validator.", defaultSeverity: "error" }
@@ -14655,6 +14617,136 @@ function severityFor(rule, settings) {
14655
14617
  return settings.rules[rule] ?? DEFAULT_RULE_SEVERITY[rule];
14656
14618
  }
14657
14619
 
14620
+ // src/node.ts
14621
+ var require2 = createRequire(import.meta.url);
14622
+ function resolveSchemasDir(override) {
14623
+ if (override) return resolve(override);
14624
+ if (process.env.SCHEMAS_DIR) return resolve(process.env.SCHEMAS_DIR);
14625
+ const bundled = resolve(dirname(fileURLToPath(import.meta.url)), "schemas");
14626
+ if (existsSync(bundled)) return bundled;
14627
+ const ref = require2.resolve("@studio/schemas/extended/casetype.schema.json");
14628
+ return dirname(ref);
14629
+ }
14630
+ function loadSchemasFromDir(dir) {
14631
+ const out = [];
14632
+ const walk = (d) => {
14633
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
14634
+ const p = join(d, entry.name);
14635
+ if (entry.isDirectory()) walk(p);
14636
+ else if (entry.name.endsWith(".schema.json")) out.push(JSON.parse(readFileSync(p, "utf8")));
14637
+ }
14638
+ };
14639
+ walk(dir);
14640
+ return out;
14641
+ }
14642
+ function loadCoverage(dir) {
14643
+ const file = join(dir, ".coverage.json");
14644
+ if (!existsSync(file) || !statSync(file).isFile()) return null;
14645
+ try {
14646
+ return buildCoverageIndex(JSON.parse(readFileSync(file, "utf8")));
14647
+ } catch {
14648
+ return null;
14649
+ }
14650
+ }
14651
+ function loadCaseRefs(dir) {
14652
+ const file = join(dir, ".caserefs.json");
14653
+ if (!existsSync(file) || !statSync(file).isFile()) return {};
14654
+ try {
14655
+ return JSON.parse(readFileSync(file, "utf8"));
14656
+ } catch {
14657
+ return {};
14658
+ }
14659
+ }
14660
+ var docCache = /* @__PURE__ */ new Map();
14661
+ function targetDoc(absPath) {
14662
+ if (docCache.has(absPath)) return docCache.get(absPath);
14663
+ let doc = null;
14664
+ if (existsSync(absPath) && statSync(absPath).isFile()) {
14665
+ try {
14666
+ doc = (0, import_yaml.parse)(readFileSync(absPath, "utf8"));
14667
+ } catch {
14668
+ doc = null;
14669
+ }
14670
+ }
14671
+ docCache.set(absPath, doc);
14672
+ return doc;
14673
+ }
14674
+ function targetType(absPath, fragment) {
14675
+ const doc = targetDoc(absPath);
14676
+ if (doc == null || typeof doc !== "object") return "not-found";
14677
+ if (!fragment) {
14678
+ const t2 = doc._type;
14679
+ return typeof t2 === "string" ? t2 : null;
14680
+ }
14681
+ let found = null;
14682
+ const walk = (n) => {
14683
+ if (found || !n || typeof n !== "object") return;
14684
+ if (Array.isArray(n)) {
14685
+ n.forEach(walk);
14686
+ return;
14687
+ }
14688
+ const o = n;
14689
+ if (o._uid === fragment) {
14690
+ found = o;
14691
+ return;
14692
+ }
14693
+ for (const v of Object.values(o)) walk(v);
14694
+ };
14695
+ walk(doc);
14696
+ if (!found) return "not-found";
14697
+ const t = found._type;
14698
+ return typeof t === "string" ? t : null;
14699
+ }
14700
+ var looksLikePath = (s) => s.includes("/") || s.endsWith(".yaml") || s.endsWith(".yml");
14701
+ function referenceFindings(value, file, caseRefs, settings) {
14702
+ const sev = severityFor("invalid-reference", settings);
14703
+ if (sev === "off" || Object.keys(caseRefs).length === 0) return [];
14704
+ const baseDir = dirname(file);
14705
+ const out = [];
14706
+ const checkRef = (ref, allowed, casetype, field, path) => {
14707
+ if (!looksLikePath(ref)) return;
14708
+ const frag = /^(.*\.ya?ml)#(.+)$/.exec(ref);
14709
+ const pathPart = frag ? frag[1] : ref;
14710
+ const fragment = frag ? frag[2] : "";
14711
+ const t = targetType(resolve(baseDir, pathPart), fragment);
14712
+ if (t === "not-found") {
14713
+ out.push({ severity: sev, rule: "invalid-reference", casetype, path, field, message: `reference target not found: "${ref}"`, file });
14714
+ } else if (t !== null && !allowed.includes(t)) {
14715
+ out.push({ severity: sev, rule: "invalid-reference", casetype, path, field, message: `field "${field}" references a "${t}" but must reference one of: ${allowed.join(", ")}`, file });
14716
+ }
14717
+ };
14718
+ const walk = (node, path) => {
14719
+ if (Array.isArray(node)) {
14720
+ node.forEach((n, i) => walk(n, `${path}/${i}`));
14721
+ return;
14722
+ }
14723
+ if (!node || typeof node !== "object") return;
14724
+ const o = node;
14725
+ const refs = typeof o._type === "string" ? caseRefs[o._type] : void 0;
14726
+ if (refs) {
14727
+ for (const [field, allowed] of Object.entries(refs)) {
14728
+ const v = o[field];
14729
+ if (typeof v === "string") checkRef(v, allowed, o._type, field, `${path}/${field}`);
14730
+ else if (Array.isArray(v)) v.forEach((item, i) => {
14731
+ if (typeof item === "string") checkRef(item, allowed, o._type, field, `${path}/${field}/${i}`);
14732
+ });
14733
+ }
14734
+ }
14735
+ for (const [k, v] of Object.entries(o)) walk(v, `${path}/${k}`);
14736
+ };
14737
+ walk(value, "");
14738
+ return out;
14739
+ }
14740
+ function createNodeContext(opts = {}) {
14741
+ const schemasDir = resolveSchemasDir(opts.schemasDir);
14742
+ const registry = createSchemaRegistry(loadSchemasFromDir(schemasDir));
14743
+ const coverage = loadCoverage(schemasDir);
14744
+ return { registry, coverage, schemasDir };
14745
+ }
14746
+
14747
+ // src/yaml.ts
14748
+ var import_yaml2 = __toESM(require_dist(), 1);
14749
+
14658
14750
  // src/lint.ts
14659
14751
  function ruleForKeyword(keyword) {
14660
14752
  switch (keyword) {
@@ -14859,8 +14951,32 @@ function lintDocument(ctx, input) {
14859
14951
  const schema = validate.schema;
14860
14952
  const forbidden = /* @__PURE__ */ new Set();
14861
14953
  for (const e of errors) if (e.keyword === "false schema") forbidden.add(e.instancePath);
14954
+ const anyOfRe = /^(.*\/anyOf)\/\d+\/required$/;
14955
+ const reqAnyGroups = /* @__PURE__ */ new Map();
14956
+ for (const e of errors) {
14957
+ const m = e.keyword === "required" ? anyOfRe.exec(e.schemaPath) : null;
14958
+ if (!m) continue;
14959
+ const key2 = `${m[1]}@${e.instancePath}`;
14960
+ const g = reqAnyGroups.get(key2) ?? { instancePath: e.instancePath, fields: [], err: e };
14961
+ g.fields.push(String(e.params.missingProperty));
14962
+ reqAnyGroups.set(key2, g);
14963
+ }
14964
+ const emittedReqAny = /* @__PURE__ */ new Set();
14862
14965
  for (const e of errors) {
14863
14966
  if (META_KEYWORDS.has(e.keyword)) continue;
14967
+ if (e.keyword === "required") {
14968
+ const m = anyOfRe.exec(e.schemaPath);
14969
+ const key2 = m ? `${m[1]}@${e.instancePath}` : null;
14970
+ const g = key2 ? reqAnyGroups.get(key2) : null;
14971
+ if (g && g.fields.length >= 2) {
14972
+ if (!emittedReqAny.has(key2)) {
14973
+ emittedReqAny.add(key2);
14974
+ const state = describeState(schema, g.err, input.value, ctx.registry.resolveRef);
14975
+ push("required-field", g.instancePath, null, `missing at least one of: ${g.fields.join(", ")}${state ? ` ${state}` : ""}`);
14976
+ }
14977
+ continue;
14978
+ }
14979
+ }
14864
14980
  const { field, path } = locate(e);
14865
14981
  if ((e.keyword === "additionalProperties" || e.keyword === "unevaluatedProperties") && forbidden.has(path)) {
14866
14982
  continue;
@@ -14874,6 +14990,18 @@ function lintDocument(ctx, input) {
14874
14990
  push("value-case", path, field, `"${String(actual)}" should be "${want}" (case/format mismatch)`);
14875
14991
  continue;
14876
14992
  }
14993
+ const got = actual === void 0 ? "absent" : JSON.stringify(actual);
14994
+ push("invalid-value", path, field, `got ${got} \u2014 ${humanize(e)}`);
14995
+ continue;
14996
+ }
14997
+ if (e.keyword === "type") {
14998
+ const actual = valueAtPointer(input.value, e.instancePath);
14999
+ const recv = actual === null ? "null" : Array.isArray(actual) ? "array" : typeof actual;
15000
+ let val = JSON.stringify(actual) ?? "";
15001
+ if (val.length > 60) val = `${val.slice(0, 57)}\u2026"`;
15002
+ const expected = String(e.params.type);
15003
+ push("invalid-type", path, field, `expected ${expected} but received ${recv}${actual === void 0 ? "" : ` with value ${val}`}`);
15004
+ continue;
14877
15005
  }
14878
15006
  const rule = ruleForKeyword(e.keyword);
14879
15007
  let message = humanize(e);
@@ -14922,7 +15050,7 @@ function isLintableYaml(text) {
14922
15050
  // src/cli.ts
14923
15051
  var PARALLEL_THRESHOLD = 200;
14924
15052
  var MAX_WORKERS = 8;
14925
- var VERSION = true ? "0.2.348" : "0.0.0-dev";
15053
+ var VERSION = true ? "0.2.351" : "0.0.0-dev";
14926
15054
  function isNewer(latest, current) {
14927
15055
  const a = latest.split(".").map(Number);
14928
15056
  const b = current.split(".").map(Number);
@@ -14943,7 +15071,7 @@ function checkForUpdate(disabled) {
14943
15071
  }
14944
15072
  var VALID_RULES = new Set(LINT_RULE_DESCRIPTORS.map((d) => d.id));
14945
15073
  function parseArgs(argv) {
14946
- const args = { paths: [], off: [], format: "text", quiet: false, noUpdateCheck: false };
15074
+ const args = { paths: [], off: [], format: "text", quiet: false, errorsOnly: false, noUpdateCheck: false };
14947
15075
  for (let i = 0; i < argv.length; i++) {
14948
15076
  const a = argv[i];
14949
15077
  if (a === "--settings") args.settingsFile = argv[++i];
@@ -14958,6 +15086,7 @@ function parseArgs(argv) {
14958
15086
  else if (a === "--format") args.format = argv[++i];
14959
15087
  else if (a === "--report") args.report = argv[++i];
14960
15088
  else if (a === "--quiet") args.quiet = true;
15089
+ else if (a === "--errors-only") args.errorsOnly = true;
14961
15090
  else if (a === "--no-update-check") args.noUpdateCheck = true;
14962
15091
  else if (a.startsWith("--")) throw new Error(`unknown option: ${a}`);
14963
15092
  else args.paths.push(a);
@@ -14996,18 +15125,19 @@ var COLORS = { error: "\x1B[31m", warning: "\x1B[33m", info: "\x1B[36m" };
14996
15125
  var RESET = "\x1B[0m";
14997
15126
  var useColor = process.stdout.isTTY;
14998
15127
  var paint = (s, c) => useColor ? `${c}${s}${RESET}` : s;
14999
- function printText(results, settings, quiet) {
15128
+ function printText(results, settings, quiet, errorsOnly) {
15000
15129
  for (const r of results) {
15001
15130
  if (r.skipped) continue;
15002
- if (quiet && r.messages.length === 0) continue;
15131
+ const msgs = errorsOnly ? r.messages.filter((m) => m.severity === "error") : r.messages;
15132
+ if (quiet && msgs.length === 0) continue;
15003
15133
  const rel = r.file ? relative(process.cwd(), r.file) : "<input>";
15004
15134
  const head = `${rel}${r.casetype ? ` (${r.casetype})` : ""}`;
15005
- if (r.messages.length === 0) {
15135
+ if (msgs.length === 0) {
15006
15136
  if (!quiet) console.log(`${paint("ok", COLORS.info)} ${head}`);
15007
15137
  continue;
15008
15138
  }
15009
15139
  console.log(head);
15010
- for (const m of r.messages) {
15140
+ for (const m of msgs) {
15011
15141
  const loc = m.path || "/";
15012
15142
  console.log(` ${paint(m.severity.padEnd(7), COLORS[m.severity])} ${m.rule.padEnd(16)} ${loc} ${m.message}`);
15013
15143
  }
@@ -15019,8 +15149,9 @@ function printText(results, settings, quiet) {
15019
15149
  ${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}]`
15020
15150
  );
15021
15151
  }
15022
- function printReport(results, kind) {
15023
- const msgs = results.filter((r) => !r.skipped).flatMap((r) => r.messages);
15152
+ function printReport(results, kind, errorsOnly) {
15153
+ let msgs = results.filter((r) => !r.skipped).flatMap((r) => r.messages);
15154
+ if (errorsOnly) msgs = msgs.filter((m) => m.severity === "error");
15024
15155
  if (kind === "rules") {
15025
15156
  const by2 = /* @__PURE__ */ new Map();
15026
15157
  for (const m of msgs) {
@@ -15054,6 +15185,7 @@ function printReport(results, kind) {
15054
15185
  var WORKER_FILE = fileURLToPath2(import.meta.url);
15055
15186
  function lintFiles(files, ctx, settings) {
15056
15187
  const out = [];
15188
+ const caseRefs = loadCaseRefs(ctx.schemasDir);
15057
15189
  const parseErr = (file, message) => ({
15058
15190
  file,
15059
15191
  casetype: null,
@@ -15066,7 +15198,7 @@ function lintFiles(files, ctx, settings) {
15066
15198
  if (!isLintableYaml(text)) continue;
15067
15199
  let docs;
15068
15200
  try {
15069
- docs = (0, import_yaml2.parseAllDocuments)(text);
15201
+ docs = (0, import_yaml3.parseAllDocuments)(text);
15070
15202
  } catch (e) {
15071
15203
  out.push(parseErr(file, e.message));
15072
15204
  continue;
@@ -15077,7 +15209,13 @@ function lintFiles(files, ctx, settings) {
15077
15209
  continue;
15078
15210
  }
15079
15211
  const input = { value: doc.toJS(), file };
15080
- out.push(lintDocument({ registry: ctx.registry, coverage: ctx.coverage, settings }, input));
15212
+ const result = lintDocument({ registry: ctx.registry, coverage: ctx.coverage, settings }, input);
15213
+ const refs = referenceFindings(input.value, file, caseRefs, settings);
15214
+ if (refs.length) {
15215
+ result.messages.push(...refs);
15216
+ if (result.ok && refs.some((m) => meetsThreshold(m.severity, settings.failOn))) result.ok = false;
15217
+ }
15218
+ out.push(result);
15081
15219
  }
15082
15220
  }
15083
15221
  return out;
@@ -15132,12 +15270,12 @@ async function main() {
15132
15270
  results = lintFiles(files, ctx, settings);
15133
15271
  }
15134
15272
  if (args.report) {
15135
- printReport(results, args.report);
15273
+ printReport(results, args.report, args.errorsOnly);
15136
15274
  } else if (args.format === "json") {
15137
15275
  const active = results.filter((r) => !r.skipped);
15138
15276
  console.log(JSON.stringify({ summary: summarize(active), results }, null, 2));
15139
15277
  } else {
15140
- printText(results, settings, args.quiet);
15278
+ printText(results, settings, args.quiet, args.errorsOnly);
15141
15279
  }
15142
15280
  const latest = await updatePromise;
15143
15281
  if (latest) {