@sarj/eslint-plugin 15.6.7 → 15.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ advisoryRules: () => advisoryRules,
33
34
  applicationOnlyRules: () => applicationOnlyRules,
34
35
  default: () => index_default,
35
36
  publicDocumentation: () => publicDocumentation,
@@ -4291,6 +4292,87 @@ var no_long_comment_default = createRule({
4291
4292
  }
4292
4293
  });
4293
4294
 
4295
+ // src/rules/no-vague-suppression-description.ts
4296
+ var DIRECTIVE_RE3 = /(?:eslint-(?:disable|disable-next-line|disable-line)|@ts-(?:expect-error|ignore))\b/iu;
4297
+ var DESCRIPTION_RE = /(?::|--)\s*(.+?)\s*$/u;
4298
+ var VAGUE_RE = /^(?:needed|required|intentional(?:ly)?|ignore(?:d)?|false positive|type error|typescript|to satisfy (?:the )?(?:linter|typescript|type checker))\.?$/iu;
4299
+ var noVagueSuppressionDescriptionDocumentation = {
4300
+ summary: "Require suppression descriptions to name the concrete mismatch or invariant instead of a generic non-reason.",
4301
+ rationale: "Generic phrases satisfy require-description mechanically while leaving reviewers unable to audit the risk or remove stale debt.",
4302
+ remediation: "Name the exact type/runtime mismatch, external contract, or safety invariant that makes this suppression acceptable.",
4303
+ category: "maintainability",
4304
+ limitations: [
4305
+ "Only ESLint disable comments and TypeScript expect-error/ignore directives are checked.",
4306
+ "The rule uses a small anchored vocabulary and does not score prose quality generally.",
4307
+ "Generated files and descriptions containing any concrete context are excluded."
4308
+ ],
4309
+ examples: [
4310
+ {
4311
+ id: "concrete-runtime-mismatch",
4312
+ title: "Explain the concrete runtime contract",
4313
+ outcome: "no-match",
4314
+ files: [
4315
+ {
4316
+ path: "src/adapter.ts",
4317
+ source: "// @ts-expect-error -- vendor types omit the runtime requestId field\nreturn response.requestId;"
4318
+ }
4319
+ ],
4320
+ focusPath: "src/adapter.ts",
4321
+ expectedCount: 0,
4322
+ public: true
4323
+ },
4324
+ {
4325
+ id: "generic-suppression-reason",
4326
+ title: "Reject a suppression with no auditable reason",
4327
+ outcome: "match",
4328
+ files: [
4329
+ {
4330
+ path: "src/adapter.ts",
4331
+ source: "// @ts-expect-error -- false positive\nreturn response.requestId;"
4332
+ }
4333
+ ],
4334
+ focusPath: "src/adapter.ts",
4335
+ expectedCount: 1,
4336
+ public: true
4337
+ }
4338
+ ]
4339
+ };
4340
+ var no_vague_suppression_description_default = createRule({
4341
+ name: "no-vague-suppression-description",
4342
+ documentation: noVagueSuppressionDescriptionDocumentation,
4343
+ meta: {
4344
+ type: "suggestion",
4345
+ docs: {
4346
+ description: "Require suppression descriptions to name the concrete mismatch or invariant instead of a generic non-reason."
4347
+ },
4348
+ schema: [],
4349
+ messages: {
4350
+ vagueDescription: "Suppression description `{{description}}` does not explain why the suppressed diagnostic is safe here. Name the concrete type/runtime mismatch, external contract, or invariant."
4351
+ }
4352
+ },
4353
+ defaultOptions: [],
4354
+ create(context) {
4355
+ if (isGeneratedFile(context.filename, context.sourceCode.text)) {
4356
+ return {};
4357
+ }
4358
+ return {
4359
+ Program() {
4360
+ for (const comment of context.sourceCode.getAllComments()) {
4361
+ const text = comment.value.trim();
4362
+ if (!DIRECTIVE_RE3.test(text)) continue;
4363
+ const description = DESCRIPTION_RE.exec(text)?.[1]?.trim();
4364
+ if (description === void 0 || !VAGUE_RE.test(description)) continue;
4365
+ context.report({
4366
+ loc: comment.loc,
4367
+ messageId: "vagueDescription",
4368
+ data: { description }
4369
+ });
4370
+ }
4371
+ }
4372
+ };
4373
+ }
4374
+ });
4375
+
4294
4376
  // src/rules/no-generic-single-export-module.ts
4295
4377
  var import_utils21 = require("@typescript-eslint/utils");
4296
4378
  var noGenericSingleExportModuleDocumentation = {
@@ -5430,7 +5512,7 @@ var no_repeated_string_literal_default = createRule({
5430
5512
  var import_utils28 = require("@typescript-eslint/utils");
5431
5513
  var MAX_WORDS = 8;
5432
5514
  var MIN_CONTENT_TOKENS = 2;
5433
- var DIRECTIVE_RE3 = /^(eslint\b|eslint-|sarj-noqa\b|@ts-|prettier-ignore|prettier\b|biome-|c8\b|v8\b|istanbul\b|@type\b|@vite|webpack|<reference|<amd|global\b|noinspection|todo\b|fixme\b|hack\b|xxx\b)/i;
5515
+ var DIRECTIVE_RE4 = /^(eslint\b|eslint-|sarj-noqa\b|@ts-|prettier-ignore|prettier\b|biome-|c8\b|v8\b|istanbul\b|@type\b|@vite|webpack|<reference|<amd|global\b|noinspection|todo\b|fixme\b|hack\b|xxx\b)/i;
5434
5516
  var CODEY_RE = /^[\w.$[\]'"]+\s*[:=]\s*\S|^[\w.$]+\s*\(|^(?:return|throw|await|import|export|const|let|var)\b.*[=()[\]{}]/;
5435
5517
  var BANNERISH_RE = /[=\-─-╿*#~_.]{3,}|^[A-Z0-9 _:-]+$/;
5436
5518
  var MODALITY_RE = /\b(?:can|could|should|shall|may|might|must|will|would|cannot)\b/i;
@@ -5529,7 +5611,7 @@ var no_restated_comment_default = createRule({
5529
5611
  }
5530
5612
  const body2 = comment.value.replace(/^\/*/, "").trim();
5531
5613
  if (body2.length === 0 || body2.endsWith("?")) continue;
5532
- if (DIRECTIVE_RE3.test(body2) || CODEY_RE.test(body2) || BANNERISH_RE.test(body2)) continue;
5614
+ if (DIRECTIVE_RE4.test(body2) || CODEY_RE.test(body2) || BANNERISH_RE.test(body2)) continue;
5533
5615
  if (NON_ASCII_LETTER_RE.test(body2) || isProtected(body2)) continue;
5534
5616
  if (MODALITY_RE.test(body2) || LEAD_IN_RE.test(body2) || EMPHASIS_RE.test(body2)) continue;
5535
5617
  if (NEGATION_WORD_RE.test(body2)) continue;
@@ -5577,7 +5659,7 @@ var MODELLED_TAGS = /* @__PURE__ */ new Set([
5577
5659
  ]);
5578
5660
  var PARAM_TAGS = /* @__PURE__ */ new Set(["arg", "argument", "param"]);
5579
5661
  var RETURN_TAGS = /* @__PURE__ */ new Set(["return", "returns"]);
5580
- var DIRECTIVE_RE4 = /^\s*(?:eslint\b|eslint-|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|@vite|webpack|@jsx|@jest-environment|@vitest-environment|#__)/i;
5662
+ var DIRECTIVE_RE5 = /^\s*(?:eslint\b|eslint-|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|@vite|webpack|@jsx|@jest-environment|@vitest-environment|#__)/i;
5581
5663
  var STOPWORDS2 = new Set(
5582
5664
  `the a an of to for in on with and or as at by is are was be been being
5583
5665
  this that it its if whether when where which what will would can could should
@@ -5693,7 +5775,7 @@ var no_restated_jsdoc_default = createRule({
5693
5775
  description,
5694
5776
  ...tags.filter((tag) => tag.name === "description").map((tag) => tag.text)
5695
5777
  ].filter((text) => text.length > 0).join("\n");
5696
- if (DIRECTIVE_RE4.test(describedText)) continue;
5778
+ if (DIRECTIVE_RE5.test(describedText)) continue;
5697
5779
  const tagNames = new Set(tags.map((tag) => tag.name));
5698
5780
  if ([...tagNames].some((name) => !MODELLED_TAGS.has(name))) continue;
5699
5781
  if (isProtected(describedText)) continue;
@@ -7655,10 +7737,10 @@ var STOPWORDS3 = /* @__PURE__ */ new Set([
7655
7737
  "we",
7656
7738
  "with"
7657
7739
  ]);
7658
- var DIRECTIVE_RE5 = /^\s*(?:eslint\b|eslint-|sarj-noqa\b|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|todo\b|fixme\b|hack\b|xxx\b)/i;
7740
+ var DIRECTIVE_RE6 = /^\s*(?:eslint\b|eslint-|sarj-noqa\b|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|todo\b|fixme\b|hack\b|xxx\b)/i;
7659
7741
  var UNIT_NAME_SUFFIX_RE = /(?:_(?:NS|US|MS|S|SEC|SECS|SECOND|SECONDS|MIN|MINS|MINUTE|MINUTES|HOUR|HOURS|DAY|DAYS|BYTE|BYTES|KB|MB|GB|HZ|KHZ|MHZ|PX)|(?:Ns|Us|Ms|Sec|Secs|Second|Seconds|Min|Mins|Minute|Minutes|Hour|Hours|Day|Days|Byte|Bytes|Kb|Mb|Gb|Hz|Khz|Mhz|Px))$/u;
7660
7742
  function narratesValue(body2, code) {
7661
- if (body2.length === 0 || DIRECTIVE_RE5.test(body2) || hasExternalReference(body2)) return false;
7743
+ if (body2.length === 0 || DIRECTIVE_RE6.test(body2) || hasExternalReference(body2)) return false;
7662
7744
  const codeNumbers = numbersIn(code);
7663
7745
  if (codeNumbers.size === 0) return false;
7664
7746
  const words2 = (body2.match(WORD_RE3) ?? []).map((word) => word.toLowerCase());
@@ -14784,8 +14866,290 @@ var stepdown_default = createRule({
14784
14866
  }
14785
14867
  });
14786
14868
 
14787
- // src/rules/zod-naming-convention.ts
14869
+ // src/rules/source-coupled-test.ts
14788
14870
  var import_utils70 = require("@typescript-eslint/utils");
14871
+ var SOURCE_SUFFIX_RE = /\.(?:bash|hcl|sh|tf|tfvars|ya?ml|py|[cm]?[jt]s)$/iu;
14872
+ var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
14873
+ var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
14874
+ var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
14875
+ "slice",
14876
+ "substring",
14877
+ "substr",
14878
+ "toLowerCase",
14879
+ "toString",
14880
+ "toUpperCase",
14881
+ "trim",
14882
+ "trimEnd",
14883
+ "trimStart",
14884
+ "replace",
14885
+ "replaceAll"
14886
+ ]);
14887
+ var TEXT_PREDICATES = /* @__PURE__ */ new Set(["endsWith", "includes", "indexOf", "lastIndexOf", "match", "matchAll", "search", "startsWith"]);
14888
+ var REGEXP_PREDICATES = /* @__PURE__ */ new Set(["exec", "test"]);
14889
+ var EXPECT_MATCHERS = /* @__PURE__ */ new Set([
14890
+ "toBe",
14891
+ "toBeFalsy",
14892
+ "toBeGreaterThan",
14893
+ "toBeGreaterThanOrEqual",
14894
+ "toBeLessThan",
14895
+ "toBeLessThanOrEqual",
14896
+ "toBeNull",
14897
+ "toBeTruthy",
14898
+ "toContain",
14899
+ "toEqual",
14900
+ "toHaveLength",
14901
+ "toMatch",
14902
+ "toMatchSnapshot",
14903
+ "toStrictEqual"
14904
+ ]);
14905
+ var EXPECT_MODIFIERS = /* @__PURE__ */ new Set(["not", "rejects", "resolves"]);
14906
+ var ASSERT_MATCHERS = /* @__PURE__ */ new Set(["deepEqual", "doesNotMatch", "equal", "match", "notDeepEqual", "notEqual", "notStrictEqual", "ok", "strictEqual"]);
14907
+ var sourceCoupledTestDocumentation = {
14908
+ summary: "Disallow raw repository source text as a test oracle; parse or execute the artifact instead.",
14909
+ rationale: "Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
14910
+ remediation: "Parse the artifact, execute its validator, or assert on Terraform plan JSON or another runtime contract.",
14911
+ category: "testing",
14912
+ limitations: [
14913
+ "The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
14914
+ "When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."
14915
+ ],
14916
+ examples: [
14917
+ {
14918
+ id: "parsed-policy-contract",
14919
+ title: "Assert on parsed policy behavior",
14920
+ outcome: "no-match",
14921
+ files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const policy = JSON.parse(readFileSync('policy.json', 'utf8')); expect(validate(policy)).toEqual([]); });" }],
14922
+ focusPath: "src/policy.test.ts",
14923
+ expectedCount: 0,
14924
+ public: true
14925
+ },
14926
+ {
14927
+ id: "terraform-substring-contract",
14928
+ title: "Do not prove Terraform behavior with a regex",
14929
+ outcome: "match",
14930
+ files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const source = readFileSync('main.tf', 'utf8'); expect(source).toMatch(/prevent_destroy/); });" }],
14931
+ focusPath: "src/policy.test.ts",
14932
+ expectedCount: 1,
14933
+ public: true
14934
+ }
14935
+ ]
14936
+ };
14937
+ function staticMemberName5(node) {
14938
+ if (!node.computed && node.property.type === import_utils70.AST_NODE_TYPES.Identifier) return node.property.name;
14939
+ if (node.computed && node.property.type === import_utils70.AST_NODE_TYPES.Literal && typeof node.property.value === "string") return node.property.value;
14940
+ return null;
14941
+ }
14942
+ function unwrap5(node) {
14943
+ if (node.type === import_utils70.AST_NODE_TYPES.AwaitExpression) return unwrap5(node.argument);
14944
+ if (node.type === import_utils70.AST_NODE_TYPES.ChainExpression) return unwrap5(node.expression);
14945
+ if (node.type === import_utils70.AST_NODE_TYPES.TSAsExpression || node.type === import_utils70.AST_NODE_TYPES.TSNonNullExpression || node.type === import_utils70.AST_NODE_TYPES.TSTypeAssertion) return unwrap5(node.expression);
14946
+ return node;
14947
+ }
14948
+ function stringValue(node) {
14949
+ const current = unwrap5(node);
14950
+ if (current.type === import_utils70.AST_NODE_TYPES.Literal && typeof current.value === "string") return current.value;
14951
+ if (current.type === import_utils70.AST_NODE_TYPES.TemplateLiteral && current.expressions.length === 0) return current.quasis[0]?.value.cooked ?? null;
14952
+ return null;
14953
+ }
14954
+ function importSource(node) {
14955
+ return typeof node.source.value === "string" ? node.source.value : null;
14956
+ }
14957
+ function requireSource(node) {
14958
+ const current = unwrap5(node);
14959
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression || current.callee.type !== import_utils70.AST_NODE_TYPES.Identifier || current.callee.name !== "require" || current.arguments.length !== 1 || current.arguments[0]?.type === import_utils70.AST_NODE_TYPES.SpreadElement) return null;
14960
+ return stringValue(current.arguments[0]);
14961
+ }
14962
+ function newScope() {
14963
+ return { collections: /* @__PURE__ */ new Set(), declared: /* @__PURE__ */ new Set(), fsObjects: /* @__PURE__ */ new Set(), fsReaders: /* @__PURE__ */ new Set(), paths: /* @__PURE__ */ new Set(), rawOrigins: /* @__PURE__ */ new Map() };
14964
+ }
14965
+ var source_coupled_test_default = createRule({
14966
+ name: "source-coupled-test",
14967
+ documentation: sourceCoupledTestDocumentation,
14968
+ meta: {
14969
+ type: "suggestion",
14970
+ docs: { description: sourceCoupledTestDocumentation.summary },
14971
+ schema: [],
14972
+ messages: { rawSourceOracle: "Raw repository source text is the oracle. Parse or execute the artifact so comments, formatting, and unreachable blocks cannot satisfy the contract." }
14973
+ },
14974
+ defaultOptions: [],
14975
+ create(context) {
14976
+ if (!isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
14977
+ const scopes = [newScope()];
14978
+ const reportedOrigins = /* @__PURE__ */ new Set();
14979
+ const currentScope = () => scopes.at(-1) ?? scopes[0];
14980
+ const visible = (kind, name) => {
14981
+ for (let index = scopes.length - 1; index >= 0; index--) {
14982
+ const scope = scopes[index];
14983
+ if (scope.declared.has(name)) return scope[kind].has(name);
14984
+ }
14985
+ return false;
14986
+ };
14987
+ const visibleRawOrigins = (name) => {
14988
+ for (let index = scopes.length - 1; index >= 0; index--) {
14989
+ const scope = scopes[index];
14990
+ if (scope.declared.has(name)) return scope.rawOrigins.get(name) ?? /* @__PURE__ */ new Set();
14991
+ }
14992
+ return /* @__PURE__ */ new Set();
14993
+ };
14994
+ const sourcePath = (node) => {
14995
+ const current = unwrap5(node);
14996
+ const value = stringValue(current);
14997
+ if (value !== null) return SOURCE_SUFFIX_RE.test(value);
14998
+ if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return visible("paths", current.name);
14999
+ if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression && current.operator === "+") {
15000
+ return sourcePath(current.left) || sourcePath(current.right);
15001
+ }
15002
+ if (current.type === import_utils70.AST_NODE_TYPES.TemplateLiteral) return current.expressions.some(sourcePath);
15003
+ if (current.type === import_utils70.AST_NODE_TYPES.CallExpression || current.type === import_utils70.AST_NODE_TYPES.NewExpression) {
15004
+ return current.arguments.some((argument) => argument.type !== import_utils70.AST_NODE_TYPES.SpreadElement && sourcePath(argument));
15005
+ }
15006
+ if (current.type === import_utils70.AST_NODE_TYPES.MemberExpression) return sourcePath(current.object);
15007
+ return false;
15008
+ };
15009
+ const rawRead = (node) => {
15010
+ const current = unwrap5(node);
15011
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression || current.arguments.length === 0) return false;
15012
+ const callee = unwrap5(current.callee);
15013
+ if (callee.type === import_utils70.AST_NODE_TYPES.Identifier) {
15014
+ return visible("fsReaders", callee.name) && sourcePath(current.arguments[0]);
15015
+ }
15016
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return false;
15017
+ const name = staticMemberName5(callee);
15018
+ const object = unwrap5(callee.object);
15019
+ return name !== null && FS_READERS.has(name) && object.type === import_utils70.AST_NODE_TYPES.Identifier && visible("fsObjects", object.name) && sourcePath(current.arguments[0]);
15020
+ };
15021
+ const rawOrigins = (node) => {
15022
+ const current = unwrap5(node);
15023
+ if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return visibleRawOrigins(current.name);
15024
+ if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
15025
+ if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
15026
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression) return /* @__PURE__ */ new Set();
15027
+ const callee = unwrap5(current.callee);
15028
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
15029
+ const name = staticMemberName5(callee);
15030
+ return name !== null && TEXT_TRANSFORMS.has(name) ? rawOrigins(callee.object) : /* @__PURE__ */ new Set();
15031
+ };
15032
+ const evidenceOrigins = (node) => {
15033
+ const current = unwrap5(node);
15034
+ const direct = rawOrigins(current);
15035
+ if (direct.size > 0) return direct;
15036
+ if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression || current.type === import_utils70.AST_NODE_TYPES.LogicalExpression) return /* @__PURE__ */ new Set([...evidenceOrigins(current.left), ...evidenceOrigins(current.right)]);
15037
+ if (current.type === import_utils70.AST_NODE_TYPES.UnaryExpression) return evidenceOrigins(current.argument);
15038
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression) return /* @__PURE__ */ new Set();
15039
+ const callee = unwrap5(current.callee);
15040
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
15041
+ const name = staticMemberName5(callee);
15042
+ if (name !== null && TEXT_PREDICATES.has(name)) return rawOrigins(callee.object);
15043
+ if (name !== null && REGEXP_PREDICATES.has(name)) return new Set(current.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...rawOrigins(argument)]));
15044
+ return /* @__PURE__ */ new Set();
15045
+ };
15046
+ const rawAssertionOrigins = (node) => {
15047
+ const callee = unwrap5(node.callee);
15048
+ if (callee.type === import_utils70.AST_NODE_TYPES.Identifier && callee.name === "assert") {
15049
+ return new Set(node.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15050
+ }
15051
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
15052
+ const matcher = staticMemberName5(callee);
15053
+ if (matcher === null) return /* @__PURE__ */ new Set();
15054
+ let receiver = unwrap5(callee.object);
15055
+ while (receiver.type === import_utils70.AST_NODE_TYPES.MemberExpression && EXPECT_MODIFIERS.has(staticMemberName5(receiver) ?? "")) receiver = unwrap5(receiver.object);
15056
+ if (receiver.type === import_utils70.AST_NODE_TYPES.CallExpression && receiver.callee.type === import_utils70.AST_NODE_TYPES.Identifier && receiver.callee.name === "expect") {
15057
+ if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
15058
+ return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15059
+ }
15060
+ if (receiver.type !== import_utils70.AST_NODE_TYPES.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
15061
+ return new Set(node.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15062
+ };
15063
+ const declare = (name, state) => {
15064
+ const scope = currentScope();
15065
+ scope.declared.add(name);
15066
+ scope.collections.delete(name);
15067
+ scope.fsObjects.delete(name);
15068
+ scope.fsReaders.delete(name);
15069
+ scope.paths.delete(name);
15070
+ scope.rawOrigins.delete(name);
15071
+ if (state.collection === true) scope.collections.add(name);
15072
+ if (state.fsObject === true) scope.fsObjects.add(name);
15073
+ if (state.fsReader === true) scope.fsReaders.add(name);
15074
+ if (state.path === true) scope.paths.add(name);
15075
+ if (state.rawOrigins !== void 0 && state.rawOrigins.size > 0) {
15076
+ scope.rawOrigins.set(name, state.rawOrigins);
15077
+ }
15078
+ };
15079
+ const sourceCollection = (node) => {
15080
+ const current = unwrap5(node);
15081
+ return current.type === import_utils70.AST_NODE_TYPES.ArrayExpression && current.elements.length > 0 && current.elements.every((element) => element !== null && element.type !== import_utils70.AST_NODE_TYPES.SpreadElement && sourcePath(element));
15082
+ };
15083
+ const declaredNames2 = (node) => {
15084
+ const current = unwrap5(node);
15085
+ if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return [current.name];
15086
+ if (current.type === import_utils70.AST_NODE_TYPES.AssignmentPattern) return declaredNames2(current.left);
15087
+ if (current.type === import_utils70.AST_NODE_TYPES.RestElement) return declaredNames2(current.argument);
15088
+ if (current.type === import_utils70.AST_NODE_TYPES.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
15089
+ if (current.type === import_utils70.AST_NODE_TYPES.ObjectPattern) return current.properties.flatMap((property) => property.type === import_utils70.AST_NODE_TYPES.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
15090
+ return [];
15091
+ };
15092
+ const enterFunction = (node) => {
15093
+ scopes.push(newScope());
15094
+ for (const parameter of node.params) for (const name of declaredNames2(parameter)) declare(name, {});
15095
+ };
15096
+ const exitFunction = () => {
15097
+ scopes.pop();
15098
+ };
15099
+ return {
15100
+ ImportDeclaration(node) {
15101
+ const source = importSource(node);
15102
+ if (source === null || !FS_MODULES.has(source)) return;
15103
+ for (const specifier of node.specifiers) {
15104
+ if (specifier.type === import_utils70.AST_NODE_TYPES.ImportSpecifier) {
15105
+ const imported = specifier.imported.type === import_utils70.AST_NODE_TYPES.Identifier ? specifier.imported.name : String(specifier.imported.value);
15106
+ if (FS_READERS.has(imported)) declare(specifier.local.name, { fsReader: true });
15107
+ } else {
15108
+ declare(specifier.local.name, { fsObject: true });
15109
+ }
15110
+ }
15111
+ },
15112
+ ":function": enterFunction,
15113
+ ":function:exit": exitFunction,
15114
+ VariableDeclarator(node) {
15115
+ if (node.init === null) return;
15116
+ const required = requireSource(node.init);
15117
+ if (required !== null && FS_MODULES.has(required) && node.id.type === import_utils70.AST_NODE_TYPES.Identifier) {
15118
+ declare(node.id.name, { fsObject: true });
15119
+ return;
15120
+ }
15121
+ if (node.id.type === import_utils70.AST_NODE_TYPES.ObjectPattern && required !== null && FS_MODULES.has(required)) {
15122
+ for (const property of node.id.properties) {
15123
+ if (property.type !== import_utils70.AST_NODE_TYPES.Property || property.value.type !== import_utils70.AST_NODE_TYPES.Identifier) continue;
15124
+ const key = property.key.type === import_utils70.AST_NODE_TYPES.Identifier ? property.key.name : property.key.type === import_utils70.AST_NODE_TYPES.Literal ? String(property.key.value) : "";
15125
+ if (FS_READERS.has(key)) declare(property.value.name, { fsReader: true });
15126
+ }
15127
+ return;
15128
+ }
15129
+ if (node.id.type !== import_utils70.AST_NODE_TYPES.Identifier) return;
15130
+ declare(node.id.name, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
15131
+ },
15132
+ AssignmentExpression(node) {
15133
+ if (node.left.type === import_utils70.AST_NODE_TYPES.Identifier) declare(node.left.name, { path: sourcePath(node.right), rawOrigins: rawOrigins(node.right) });
15134
+ },
15135
+ ForOfStatement(node) {
15136
+ const right = unwrap5(node.right);
15137
+ const collection = right.type === import_utils70.AST_NODE_TYPES.Identifier && visible("collections", right.name);
15138
+ const left = node.left.type === import_utils70.AST_NODE_TYPES.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
15139
+ if (collection && left?.type === import_utils70.AST_NODE_TYPES.Identifier) declare(left.name, { path: true });
15140
+ },
15141
+ CallExpression(node) {
15142
+ const origins = rawAssertionOrigins(node);
15143
+ if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
15144
+ for (const origin of origins) reportedOrigins.add(origin);
15145
+ context.report({ node, messageId: "rawSourceOracle" });
15146
+ }
15147
+ };
15148
+ }
15149
+ });
15150
+
15151
+ // src/rules/zod-naming-convention.ts
15152
+ var import_utils71 = require("@typescript-eslint/utils");
14789
15153
  var zodNamingConventionDocumentation = {
14790
15154
  summary: "Enforce a consistent Zod schema naming convention \u2014 a `Z` prefix (`ZUser`) or a `Schema` suffix (`userSchema`); both are accepted by default.",
14791
15155
  rationale: "A recognizable schema name distinguishes runtime validators from ordinary values at each use site.",
@@ -14826,18 +15190,18 @@ var NON_SCHEMA_TERMINALS = /* @__PURE__ */ new Set([
14826
15190
  "prettifyError",
14827
15191
  "treeifyError"
14828
15192
  ]);
14829
- var terminalMethodName = (callee) => !callee.computed && callee.property.type === import_utils70.AST_NODE_TYPES.Identifier ? callee.property.name : null;
15193
+ var terminalMethodName = (callee) => !callee.computed && callee.property.type === import_utils71.AST_NODE_TYPES.Identifier ? callee.property.name : null;
14830
15194
  var calleeChainRoot = (node) => {
14831
15195
  let current = node;
14832
15196
  for (; ; ) {
14833
- if (current.type === import_utils70.AST_NODE_TYPES.Identifier) {
15197
+ if (current.type === import_utils71.AST_NODE_TYPES.Identifier) {
14834
15198
  return current;
14835
15199
  }
14836
- if (current.type === import_utils70.AST_NODE_TYPES.MemberExpression) {
15200
+ if (current.type === import_utils71.AST_NODE_TYPES.MemberExpression) {
14837
15201
  current = current.object;
14838
15202
  continue;
14839
15203
  }
14840
- if (current.type === import_utils70.AST_NODE_TYPES.CallExpression) {
15204
+ if (current.type === import_utils71.AST_NODE_TYPES.CallExpression) {
14841
15205
  current = current.callee;
14842
15206
  continue;
14843
15207
  }
@@ -14877,7 +15241,7 @@ var zod_naming_convention_default = createRule({
14877
15241
  const acceptsSchemaWord = convention !== "prefix";
14878
15242
  const zodBindings = /* @__PURE__ */ new Set();
14879
15243
  function resolvedBinding(identifier) {
14880
- return import_utils70.ASTUtils.findVariable(
15244
+ return import_utils71.ASTUtils.findVariable(
14881
15245
  context.sourceCode.getScope(identifier),
14882
15246
  identifier.name
14883
15247
  );
@@ -14899,7 +15263,7 @@ var zod_naming_convention_default = createRule({
14899
15263
  ImportDeclaration(node) {
14900
15264
  if (!isZodModule(node.source.value)) return;
14901
15265
  for (const specifier of node.specifiers) {
14902
- if (specifier.type === import_utils70.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils70.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils70.AST_NODE_TYPES.ImportSpecifier && (specifier.imported.type === import_utils70.AST_NODE_TYPES.Identifier ? specifier.imported.name === "z" : specifier.imported.value === "z")) {
15266
+ if (specifier.type === import_utils71.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils71.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils71.AST_NODE_TYPES.ImportSpecifier && (specifier.imported.type === import_utils71.AST_NODE_TYPES.Identifier ? specifier.imported.name === "z" : specifier.imported.value === "z")) {
14903
15267
  recordZodBinding(specifier.local);
14904
15268
  }
14905
15269
  }
@@ -14907,13 +15271,13 @@ var zod_naming_convention_default = createRule({
14907
15271
  VariableDeclarator(node) {
14908
15272
  const init = node.init;
14909
15273
  if (init === null || init === void 0) return;
14910
- if (init.type !== import_utils70.AST_NODE_TYPES.CallExpression) return;
15274
+ if (init.type !== import_utils71.AST_NODE_TYPES.CallExpression) return;
14911
15275
  const callee = init.callee;
14912
- if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return;
15276
+ if (callee.type !== import_utils71.AST_NODE_TYPES.MemberExpression) return;
14913
15277
  if (!isZodChain(callee)) return;
14914
15278
  const terminal = terminalMethodName(callee);
14915
15279
  if (terminal !== null && NON_SCHEMA_TERMINALS.has(terminal)) return;
14916
- if (node.id.type !== import_utils70.AST_NODE_TYPES.Identifier) return;
15280
+ if (node.id.type !== import_utils71.AST_NODE_TYPES.Identifier) return;
14917
15281
  if (test.test(node.id.name)) return;
14918
15282
  if (acceptsSchemaWord && CONTAINS_SCHEMA_RE.test(node.id.name)) return;
14919
15283
  context.report({
@@ -15022,6 +15386,7 @@ var rules = {
15022
15386
  "no-impossible-zod-literal-bounds": no_impossible_zod_literal_bounds_default,
15023
15387
  "no-log-only-catch": no_log_only_catch_default,
15024
15388
  "no-long-comment": no_long_comment_default,
15389
+ "no-vague-suppression-description": no_vague_suppression_description_default,
15025
15390
  "no-generic-single-export-module": no_generic_single_export_module_default,
15026
15391
  "no-offset-pagination": no_offset_pagination_default,
15027
15392
  "no-positional-tuple-return": no_positional_tuple_return_default,
@@ -15070,17 +15435,19 @@ var rules = {
15070
15435
  "require-zod-form-validation": require_zod_form_validation_default,
15071
15436
  "store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
15072
15437
  "stepdown": stepdown_default,
15438
+ "source-coupled-test": source_coupled_test_default,
15073
15439
  "zod-naming-convention": zod_naming_convention_default
15074
15440
  };
15075
15441
  var meta = {
15076
15442
  name: "@sarj/eslint-plugin",
15077
- version: "15.6.7"
15443
+ version: "15.6.9"
15078
15444
  };
15079
15445
  var applicationOnlyRules = [
15080
15446
  "no-restricted-library-load",
15081
15447
  "prefer-native-random-uuid",
15082
15448
  "prefer-shadcn-primitives"
15083
15449
  ];
15450
+ var advisoryRules = ["no-vague-suppression-description", "source-coupled-test"];
15084
15451
  var recommendedRules = {
15085
15452
  "@sarj/duplicate-test-body": "error",
15086
15453
  "@sarj/enforce-file-structure": "error",
@@ -15096,6 +15463,7 @@ var recommendedRules = {
15096
15463
  "@sarj/no-impossible-zod-literal-bounds": "error",
15097
15464
  "@sarj/no-log-only-catch": "error",
15098
15465
  "@sarj/no-long-comment": "error",
15466
+ "@sarj/no-vague-suppression-description": "warn",
15099
15467
  "@sarj/no-generic-single-export-module": "error",
15100
15468
  "@sarj/no-offset-pagination": "error",
15101
15469
  "@sarj/no-positional-tuple-return": "error",
@@ -15138,6 +15506,7 @@ var recommendedRules = {
15138
15506
  "@sarj/require-zod-form-validation": "error",
15139
15507
  "@sarj/store-insert-requires-on-conflict": "error",
15140
15508
  "@sarj/stepdown": "error",
15509
+ "@sarj/source-coupled-test": "warn",
15141
15510
  "@sarj/zod-naming-convention": "error"
15142
15511
  };
15143
15512
  var strictRules = {
@@ -15156,6 +15525,7 @@ var strictRules = {
15156
15525
  "@sarj/no-impossible-zod-literal-bounds": "error",
15157
15526
  "@sarj/no-log-only-catch": "error",
15158
15527
  "@sarj/no-long-comment": "error",
15528
+ "@sarj/no-vague-suppression-description": "warn",
15159
15529
  "@sarj/no-generic-single-export-module": "error",
15160
15530
  "@sarj/no-offset-pagination": "error",
15161
15531
  "@sarj/no-positional-tuple-return": "error",
@@ -15201,6 +15571,7 @@ var strictRules = {
15201
15571
  "@sarj/require-zod-form-validation": "error",
15202
15572
  "@sarj/store-insert-requires-on-conflict": "error",
15203
15573
  "@sarj/stepdown": "error",
15574
+ "@sarj/source-coupled-test": "warn",
15204
15575
  "@sarj/zod-naming-convention": "error"
15205
15576
  };
15206
15577
  var plugin = {
@@ -15225,6 +15596,7 @@ var plugin = {
15225
15596
  var index_default = plugin;
15226
15597
  // Annotate the CommonJS export names for ESM import in node:
15227
15598
  0 && (module.exports = {
15599
+ advisoryRules,
15228
15600
  applicationOnlyRules,
15229
15601
  publicDocumentation,
15230
15602
  recommendedRules,
package/dist/index.d.cts CHANGED
@@ -208,6 +208,7 @@ declare const rules: {
208
208
  readonly "no-impossible-zod-literal-bounds": DocumentedRule<readonly [], "impossibleBounds">;
209
209
  readonly "no-log-only-catch": DocumentedRule<readonly [LoggingOptions?], "noLogOnlyCatch" | "emptyCatch">;
210
210
  readonly "no-long-comment": DocumentedRule<readonly [], "tooLong">;
211
+ readonly "no-vague-suppression-description": DocumentedRule<readonly [], "vagueDescription">;
211
212
  readonly "no-generic-single-export-module": DocumentedRule<[], "genericSingleExport">;
212
213
  readonly "no-offset-pagination": DocumentedRule<readonly [], "noOffsetPagination">;
213
214
  readonly "no-positional-tuple-return": DocumentedRule<readonly [], "noPositionalTupleReturn">;
@@ -274,12 +275,15 @@ declare const rules: {
274
275
  readonly "require-zod-form-validation": DocumentedRule<readonly [], "missingZodValidation">;
275
276
  readonly "store-insert-requires-on-conflict": DocumentedRule<readonly [], "storeInsertRequiresOnConflict">;
276
277
  readonly stepdown: DocumentedRule<[], "helperAboveOnlyCaller">;
278
+ readonly "source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
277
279
  readonly "zod-naming-convention": DocumentedRule<readonly [{
278
280
  convention?: "prefix" | "suffix" | "either";
279
281
  }?], "zPrefix" | "schemaSuffix" | "zodSchemaName">;
280
282
  };
281
283
  /** Rules registered for application-profile configs but intentionally absent from general presets. */
282
284
  declare const applicationOnlyRules: readonly ["no-restricted-library-load", "prefer-native-random-uuid", "prefer-shadcn-primitives"];
285
+ /** Calibrated rules that intentionally remain warnings until consumer-corpus precision is proven. */
286
+ declare const advisoryRules: readonly ["no-vague-suppression-description", "source-coupled-test"];
283
287
  declare const recommendedRules: {
284
288
  readonly "@sarj/duplicate-test-body": "error";
285
289
  readonly "@sarj/enforce-file-structure": "error";
@@ -297,6 +301,7 @@ declare const recommendedRules: {
297
301
  readonly "@sarj/no-impossible-zod-literal-bounds": "error";
298
302
  readonly "@sarj/no-log-only-catch": "error";
299
303
  readonly "@sarj/no-long-comment": "error";
304
+ readonly "@sarj/no-vague-suppression-description": "warn";
300
305
  readonly "@sarj/no-generic-single-export-module": "error";
301
306
  readonly "@sarj/no-offset-pagination": "error";
302
307
  readonly "@sarj/no-positional-tuple-return": "error";
@@ -341,6 +346,7 @@ declare const recommendedRules: {
341
346
  readonly "@sarj/require-zod-form-validation": "error";
342
347
  readonly "@sarj/store-insert-requires-on-conflict": "error";
343
348
  readonly "@sarj/stepdown": "error";
349
+ readonly "@sarj/source-coupled-test": "warn";
344
350
  readonly "@sarj/zod-naming-convention": "error";
345
351
  };
346
352
  declare const strictRules: {
@@ -361,6 +367,7 @@ declare const strictRules: {
361
367
  readonly "@sarj/no-impossible-zod-literal-bounds": "error";
362
368
  readonly "@sarj/no-log-only-catch": "error";
363
369
  readonly "@sarj/no-long-comment": "error";
370
+ readonly "@sarj/no-vague-suppression-description": "warn";
364
371
  readonly "@sarj/no-generic-single-export-module": "error";
365
372
  readonly "@sarj/no-offset-pagination": "error";
366
373
  readonly "@sarj/no-positional-tuple-return": "error";
@@ -408,6 +415,7 @@ declare const strictRules: {
408
415
  readonly "@sarj/require-zod-form-validation": "error";
409
416
  readonly "@sarj/store-insert-requires-on-conflict": "error";
410
417
  readonly "@sarj/stepdown": "error";
418
+ readonly "@sarj/source-coupled-test": "warn";
411
419
  readonly "@sarj/zod-naming-convention": "error";
412
420
  };
413
421
  type FlatPreset = {
@@ -418,7 +426,7 @@ type FlatPreset = {
418
426
  declare const plugin: {
419
427
  readonly meta: {
420
428
  readonly name: "@sarj/eslint-plugin";
421
- readonly version: "15.6.7";
429
+ readonly version: "15.6.9";
422
430
  };
423
431
  readonly rules: {
424
432
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
@@ -441,6 +449,7 @@ declare const plugin: {
441
449
  readonly "no-impossible-zod-literal-bounds": DocumentedRule<readonly [], "impossibleBounds">;
442
450
  readonly "no-log-only-catch": DocumentedRule<readonly [LoggingOptions?], "noLogOnlyCatch" | "emptyCatch">;
443
451
  readonly "no-long-comment": DocumentedRule<readonly [], "tooLong">;
452
+ readonly "no-vague-suppression-description": DocumentedRule<readonly [], "vagueDescription">;
444
453
  readonly "no-generic-single-export-module": DocumentedRule<[], "genericSingleExport">;
445
454
  readonly "no-offset-pagination": DocumentedRule<readonly [], "noOffsetPagination">;
446
455
  readonly "no-positional-tuple-return": DocumentedRule<readonly [], "noPositionalTupleReturn">;
@@ -507,6 +516,7 @@ declare const plugin: {
507
516
  readonly "require-zod-form-validation": DocumentedRule<readonly [], "missingZodValidation">;
508
517
  readonly "store-insert-requires-on-conflict": DocumentedRule<readonly [], "storeInsertRequiresOnConflict">;
509
518
  readonly stepdown: DocumentedRule<[], "helperAboveOnlyCaller">;
519
+ readonly "source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
510
520
  readonly "zod-naming-convention": DocumentedRule<readonly [{
511
521
  convention?: "prefix" | "suffix" | "either";
512
522
  }?], "zPrefix" | "schemaSuffix" | "zodSchemaName">;
@@ -518,4 +528,4 @@ declare const plugin: {
518
528
  };
519
529
  };
520
530
 
521
- export { type RetiredRule, applicationOnlyRules, plugin as default, publicDocumentation, recommendedRules, renamedRules, retiredRules, rules, strictRules };
531
+ export { type RetiredRule, advisoryRules, applicationOnlyRules, plugin as default, publicDocumentation, recommendedRules, renamedRules, retiredRules, rules, strictRules };
package/dist/index.d.ts CHANGED
@@ -208,6 +208,7 @@ declare const rules: {
208
208
  readonly "no-impossible-zod-literal-bounds": DocumentedRule<readonly [], "impossibleBounds">;
209
209
  readonly "no-log-only-catch": DocumentedRule<readonly [LoggingOptions?], "noLogOnlyCatch" | "emptyCatch">;
210
210
  readonly "no-long-comment": DocumentedRule<readonly [], "tooLong">;
211
+ readonly "no-vague-suppression-description": DocumentedRule<readonly [], "vagueDescription">;
211
212
  readonly "no-generic-single-export-module": DocumentedRule<[], "genericSingleExport">;
212
213
  readonly "no-offset-pagination": DocumentedRule<readonly [], "noOffsetPagination">;
213
214
  readonly "no-positional-tuple-return": DocumentedRule<readonly [], "noPositionalTupleReturn">;
@@ -274,12 +275,15 @@ declare const rules: {
274
275
  readonly "require-zod-form-validation": DocumentedRule<readonly [], "missingZodValidation">;
275
276
  readonly "store-insert-requires-on-conflict": DocumentedRule<readonly [], "storeInsertRequiresOnConflict">;
276
277
  readonly stepdown: DocumentedRule<[], "helperAboveOnlyCaller">;
278
+ readonly "source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
277
279
  readonly "zod-naming-convention": DocumentedRule<readonly [{
278
280
  convention?: "prefix" | "suffix" | "either";
279
281
  }?], "zPrefix" | "schemaSuffix" | "zodSchemaName">;
280
282
  };
281
283
  /** Rules registered for application-profile configs but intentionally absent from general presets. */
282
284
  declare const applicationOnlyRules: readonly ["no-restricted-library-load", "prefer-native-random-uuid", "prefer-shadcn-primitives"];
285
+ /** Calibrated rules that intentionally remain warnings until consumer-corpus precision is proven. */
286
+ declare const advisoryRules: readonly ["no-vague-suppression-description", "source-coupled-test"];
283
287
  declare const recommendedRules: {
284
288
  readonly "@sarj/duplicate-test-body": "error";
285
289
  readonly "@sarj/enforce-file-structure": "error";
@@ -297,6 +301,7 @@ declare const recommendedRules: {
297
301
  readonly "@sarj/no-impossible-zod-literal-bounds": "error";
298
302
  readonly "@sarj/no-log-only-catch": "error";
299
303
  readonly "@sarj/no-long-comment": "error";
304
+ readonly "@sarj/no-vague-suppression-description": "warn";
300
305
  readonly "@sarj/no-generic-single-export-module": "error";
301
306
  readonly "@sarj/no-offset-pagination": "error";
302
307
  readonly "@sarj/no-positional-tuple-return": "error";
@@ -341,6 +346,7 @@ declare const recommendedRules: {
341
346
  readonly "@sarj/require-zod-form-validation": "error";
342
347
  readonly "@sarj/store-insert-requires-on-conflict": "error";
343
348
  readonly "@sarj/stepdown": "error";
349
+ readonly "@sarj/source-coupled-test": "warn";
344
350
  readonly "@sarj/zod-naming-convention": "error";
345
351
  };
346
352
  declare const strictRules: {
@@ -361,6 +367,7 @@ declare const strictRules: {
361
367
  readonly "@sarj/no-impossible-zod-literal-bounds": "error";
362
368
  readonly "@sarj/no-log-only-catch": "error";
363
369
  readonly "@sarj/no-long-comment": "error";
370
+ readonly "@sarj/no-vague-suppression-description": "warn";
364
371
  readonly "@sarj/no-generic-single-export-module": "error";
365
372
  readonly "@sarj/no-offset-pagination": "error";
366
373
  readonly "@sarj/no-positional-tuple-return": "error";
@@ -408,6 +415,7 @@ declare const strictRules: {
408
415
  readonly "@sarj/require-zod-form-validation": "error";
409
416
  readonly "@sarj/store-insert-requires-on-conflict": "error";
410
417
  readonly "@sarj/stepdown": "error";
418
+ readonly "@sarj/source-coupled-test": "warn";
411
419
  readonly "@sarj/zod-naming-convention": "error";
412
420
  };
413
421
  type FlatPreset = {
@@ -418,7 +426,7 @@ type FlatPreset = {
418
426
  declare const plugin: {
419
427
  readonly meta: {
420
428
  readonly name: "@sarj/eslint-plugin";
421
- readonly version: "15.6.7";
429
+ readonly version: "15.6.9";
422
430
  };
423
431
  readonly rules: {
424
432
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
@@ -441,6 +449,7 @@ declare const plugin: {
441
449
  readonly "no-impossible-zod-literal-bounds": DocumentedRule<readonly [], "impossibleBounds">;
442
450
  readonly "no-log-only-catch": DocumentedRule<readonly [LoggingOptions?], "noLogOnlyCatch" | "emptyCatch">;
443
451
  readonly "no-long-comment": DocumentedRule<readonly [], "tooLong">;
452
+ readonly "no-vague-suppression-description": DocumentedRule<readonly [], "vagueDescription">;
444
453
  readonly "no-generic-single-export-module": DocumentedRule<[], "genericSingleExport">;
445
454
  readonly "no-offset-pagination": DocumentedRule<readonly [], "noOffsetPagination">;
446
455
  readonly "no-positional-tuple-return": DocumentedRule<readonly [], "noPositionalTupleReturn">;
@@ -507,6 +516,7 @@ declare const plugin: {
507
516
  readonly "require-zod-form-validation": DocumentedRule<readonly [], "missingZodValidation">;
508
517
  readonly "store-insert-requires-on-conflict": DocumentedRule<readonly [], "storeInsertRequiresOnConflict">;
509
518
  readonly stepdown: DocumentedRule<[], "helperAboveOnlyCaller">;
519
+ readonly "source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
510
520
  readonly "zod-naming-convention": DocumentedRule<readonly [{
511
521
  convention?: "prefix" | "suffix" | "either";
512
522
  }?], "zPrefix" | "schemaSuffix" | "zodSchemaName">;
@@ -518,4 +528,4 @@ declare const plugin: {
518
528
  };
519
529
  };
520
530
 
521
- export { type RetiredRule, applicationOnlyRules, plugin as default, publicDocumentation, recommendedRules, renamedRules, retiredRules, rules, strictRules };
531
+ export { type RetiredRule, advisoryRules, applicationOnlyRules, plugin as default, publicDocumentation, recommendedRules, renamedRules, retiredRules, rules, strictRules };
package/dist/index.js CHANGED
@@ -4253,6 +4253,87 @@ var no_long_comment_default = createRule({
4253
4253
  }
4254
4254
  });
4255
4255
 
4256
+ // src/rules/no-vague-suppression-description.ts
4257
+ var DIRECTIVE_RE3 = /(?:eslint-(?:disable|disable-next-line|disable-line)|@ts-(?:expect-error|ignore))\b/iu;
4258
+ var DESCRIPTION_RE = /(?::|--)\s*(.+?)\s*$/u;
4259
+ var VAGUE_RE = /^(?:needed|required|intentional(?:ly)?|ignore(?:d)?|false positive|type error|typescript|to satisfy (?:the )?(?:linter|typescript|type checker))\.?$/iu;
4260
+ var noVagueSuppressionDescriptionDocumentation = {
4261
+ summary: "Require suppression descriptions to name the concrete mismatch or invariant instead of a generic non-reason.",
4262
+ rationale: "Generic phrases satisfy require-description mechanically while leaving reviewers unable to audit the risk or remove stale debt.",
4263
+ remediation: "Name the exact type/runtime mismatch, external contract, or safety invariant that makes this suppression acceptable.",
4264
+ category: "maintainability",
4265
+ limitations: [
4266
+ "Only ESLint disable comments and TypeScript expect-error/ignore directives are checked.",
4267
+ "The rule uses a small anchored vocabulary and does not score prose quality generally.",
4268
+ "Generated files and descriptions containing any concrete context are excluded."
4269
+ ],
4270
+ examples: [
4271
+ {
4272
+ id: "concrete-runtime-mismatch",
4273
+ title: "Explain the concrete runtime contract",
4274
+ outcome: "no-match",
4275
+ files: [
4276
+ {
4277
+ path: "src/adapter.ts",
4278
+ source: "// @ts-expect-error -- vendor types omit the runtime requestId field\nreturn response.requestId;"
4279
+ }
4280
+ ],
4281
+ focusPath: "src/adapter.ts",
4282
+ expectedCount: 0,
4283
+ public: true
4284
+ },
4285
+ {
4286
+ id: "generic-suppression-reason",
4287
+ title: "Reject a suppression with no auditable reason",
4288
+ outcome: "match",
4289
+ files: [
4290
+ {
4291
+ path: "src/adapter.ts",
4292
+ source: "// @ts-expect-error -- false positive\nreturn response.requestId;"
4293
+ }
4294
+ ],
4295
+ focusPath: "src/adapter.ts",
4296
+ expectedCount: 1,
4297
+ public: true
4298
+ }
4299
+ ]
4300
+ };
4301
+ var no_vague_suppression_description_default = createRule({
4302
+ name: "no-vague-suppression-description",
4303
+ documentation: noVagueSuppressionDescriptionDocumentation,
4304
+ meta: {
4305
+ type: "suggestion",
4306
+ docs: {
4307
+ description: "Require suppression descriptions to name the concrete mismatch or invariant instead of a generic non-reason."
4308
+ },
4309
+ schema: [],
4310
+ messages: {
4311
+ vagueDescription: "Suppression description `{{description}}` does not explain why the suppressed diagnostic is safe here. Name the concrete type/runtime mismatch, external contract, or invariant."
4312
+ }
4313
+ },
4314
+ defaultOptions: [],
4315
+ create(context) {
4316
+ if (isGeneratedFile(context.filename, context.sourceCode.text)) {
4317
+ return {};
4318
+ }
4319
+ return {
4320
+ Program() {
4321
+ for (const comment of context.sourceCode.getAllComments()) {
4322
+ const text = comment.value.trim();
4323
+ if (!DIRECTIVE_RE3.test(text)) continue;
4324
+ const description = DESCRIPTION_RE.exec(text)?.[1]?.trim();
4325
+ if (description === void 0 || !VAGUE_RE.test(description)) continue;
4326
+ context.report({
4327
+ loc: comment.loc,
4328
+ messageId: "vagueDescription",
4329
+ data: { description }
4330
+ });
4331
+ }
4332
+ }
4333
+ };
4334
+ }
4335
+ });
4336
+
4256
4337
  // src/rules/no-generic-single-export-module.ts
4257
4338
  import { AST_NODE_TYPES as AST_NODE_TYPES16, ASTUtils as ASTUtils4 } from "@typescript-eslint/utils";
4258
4339
  var noGenericSingleExportModuleDocumentation = {
@@ -5392,7 +5473,7 @@ var no_repeated_string_literal_default = createRule({
5392
5473
  import { AST_NODE_TYPES as AST_NODE_TYPES21 } from "@typescript-eslint/utils";
5393
5474
  var MAX_WORDS = 8;
5394
5475
  var MIN_CONTENT_TOKENS = 2;
5395
- var DIRECTIVE_RE3 = /^(eslint\b|eslint-|sarj-noqa\b|@ts-|prettier-ignore|prettier\b|biome-|c8\b|v8\b|istanbul\b|@type\b|@vite|webpack|<reference|<amd|global\b|noinspection|todo\b|fixme\b|hack\b|xxx\b)/i;
5476
+ var DIRECTIVE_RE4 = /^(eslint\b|eslint-|sarj-noqa\b|@ts-|prettier-ignore|prettier\b|biome-|c8\b|v8\b|istanbul\b|@type\b|@vite|webpack|<reference|<amd|global\b|noinspection|todo\b|fixme\b|hack\b|xxx\b)/i;
5396
5477
  var CODEY_RE = /^[\w.$[\]'"]+\s*[:=]\s*\S|^[\w.$]+\s*\(|^(?:return|throw|await|import|export|const|let|var)\b.*[=()[\]{}]/;
5397
5478
  var BANNERISH_RE = /[=\-─-╿*#~_.]{3,}|^[A-Z0-9 _:-]+$/;
5398
5479
  var MODALITY_RE = /\b(?:can|could|should|shall|may|might|must|will|would|cannot)\b/i;
@@ -5491,7 +5572,7 @@ var no_restated_comment_default = createRule({
5491
5572
  }
5492
5573
  const body2 = comment.value.replace(/^\/*/, "").trim();
5493
5574
  if (body2.length === 0 || body2.endsWith("?")) continue;
5494
- if (DIRECTIVE_RE3.test(body2) || CODEY_RE.test(body2) || BANNERISH_RE.test(body2)) continue;
5575
+ if (DIRECTIVE_RE4.test(body2) || CODEY_RE.test(body2) || BANNERISH_RE.test(body2)) continue;
5495
5576
  if (NON_ASCII_LETTER_RE.test(body2) || isProtected(body2)) continue;
5496
5577
  if (MODALITY_RE.test(body2) || LEAD_IN_RE.test(body2) || EMPHASIS_RE.test(body2)) continue;
5497
5578
  if (NEGATION_WORD_RE.test(body2)) continue;
@@ -5539,7 +5620,7 @@ var MODELLED_TAGS = /* @__PURE__ */ new Set([
5539
5620
  ]);
5540
5621
  var PARAM_TAGS = /* @__PURE__ */ new Set(["arg", "argument", "param"]);
5541
5622
  var RETURN_TAGS = /* @__PURE__ */ new Set(["return", "returns"]);
5542
- var DIRECTIVE_RE4 = /^\s*(?:eslint\b|eslint-|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|@vite|webpack|@jsx|@jest-environment|@vitest-environment|#__)/i;
5623
+ var DIRECTIVE_RE5 = /^\s*(?:eslint\b|eslint-|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|@vite|webpack|@jsx|@jest-environment|@vitest-environment|#__)/i;
5543
5624
  var STOPWORDS2 = new Set(
5544
5625
  `the a an of to for in on with and or as at by is are was be been being
5545
5626
  this that it its if whether when where which what will would can could should
@@ -5655,7 +5736,7 @@ var no_restated_jsdoc_default = createRule({
5655
5736
  description,
5656
5737
  ...tags.filter((tag) => tag.name === "description").map((tag) => tag.text)
5657
5738
  ].filter((text) => text.length > 0).join("\n");
5658
- if (DIRECTIVE_RE4.test(describedText)) continue;
5739
+ if (DIRECTIVE_RE5.test(describedText)) continue;
5659
5740
  const tagNames = new Set(tags.map((tag) => tag.name));
5660
5741
  if ([...tagNames].some((name) => !MODELLED_TAGS.has(name))) continue;
5661
5742
  if (isProtected(describedText)) continue;
@@ -7617,10 +7698,10 @@ var STOPWORDS3 = /* @__PURE__ */ new Set([
7617
7698
  "we",
7618
7699
  "with"
7619
7700
  ]);
7620
- var DIRECTIVE_RE5 = /^\s*(?:eslint\b|eslint-|sarj-noqa\b|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|todo\b|fixme\b|hack\b|xxx\b)/i;
7701
+ var DIRECTIVE_RE6 = /^\s*(?:eslint\b|eslint-|sarj-noqa\b|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|todo\b|fixme\b|hack\b|xxx\b)/i;
7621
7702
  var UNIT_NAME_SUFFIX_RE = /(?:_(?:NS|US|MS|S|SEC|SECS|SECOND|SECONDS|MIN|MINS|MINUTE|MINUTES|HOUR|HOURS|DAY|DAYS|BYTE|BYTES|KB|MB|GB|HZ|KHZ|MHZ|PX)|(?:Ns|Us|Ms|Sec|Secs|Second|Seconds|Min|Mins|Minute|Minutes|Hour|Hours|Day|Days|Byte|Bytes|Kb|Mb|Gb|Hz|Khz|Mhz|Px))$/u;
7622
7703
  function narratesValue(body2, code) {
7623
- if (body2.length === 0 || DIRECTIVE_RE5.test(body2) || hasExternalReference(body2)) return false;
7704
+ if (body2.length === 0 || DIRECTIVE_RE6.test(body2) || hasExternalReference(body2)) return false;
7624
7705
  const codeNumbers = numbersIn(code);
7625
7706
  if (codeNumbers.size === 0) return false;
7626
7707
  const words2 = (body2.match(WORD_RE3) ?? []).map((word) => word.toLowerCase());
@@ -14762,9 +14843,291 @@ var stepdown_default = createRule({
14762
14843
  }
14763
14844
  });
14764
14845
 
14846
+ // src/rules/source-coupled-test.ts
14847
+ import { AST_NODE_TYPES as AST_NODE_TYPES56 } from "@typescript-eslint/utils";
14848
+ var SOURCE_SUFFIX_RE = /\.(?:bash|hcl|sh|tf|tfvars|ya?ml|py|[cm]?[jt]s)$/iu;
14849
+ var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
14850
+ var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
14851
+ var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
14852
+ "slice",
14853
+ "substring",
14854
+ "substr",
14855
+ "toLowerCase",
14856
+ "toString",
14857
+ "toUpperCase",
14858
+ "trim",
14859
+ "trimEnd",
14860
+ "trimStart",
14861
+ "replace",
14862
+ "replaceAll"
14863
+ ]);
14864
+ var TEXT_PREDICATES = /* @__PURE__ */ new Set(["endsWith", "includes", "indexOf", "lastIndexOf", "match", "matchAll", "search", "startsWith"]);
14865
+ var REGEXP_PREDICATES = /* @__PURE__ */ new Set(["exec", "test"]);
14866
+ var EXPECT_MATCHERS = /* @__PURE__ */ new Set([
14867
+ "toBe",
14868
+ "toBeFalsy",
14869
+ "toBeGreaterThan",
14870
+ "toBeGreaterThanOrEqual",
14871
+ "toBeLessThan",
14872
+ "toBeLessThanOrEqual",
14873
+ "toBeNull",
14874
+ "toBeTruthy",
14875
+ "toContain",
14876
+ "toEqual",
14877
+ "toHaveLength",
14878
+ "toMatch",
14879
+ "toMatchSnapshot",
14880
+ "toStrictEqual"
14881
+ ]);
14882
+ var EXPECT_MODIFIERS = /* @__PURE__ */ new Set(["not", "rejects", "resolves"]);
14883
+ var ASSERT_MATCHERS = /* @__PURE__ */ new Set(["deepEqual", "doesNotMatch", "equal", "match", "notDeepEqual", "notEqual", "notStrictEqual", "ok", "strictEqual"]);
14884
+ var sourceCoupledTestDocumentation = {
14885
+ summary: "Disallow raw repository source text as a test oracle; parse or execute the artifact instead.",
14886
+ rationale: "Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
14887
+ remediation: "Parse the artifact, execute its validator, or assert on Terraform plan JSON or another runtime contract.",
14888
+ category: "testing",
14889
+ limitations: [
14890
+ "The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
14891
+ "When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."
14892
+ ],
14893
+ examples: [
14894
+ {
14895
+ id: "parsed-policy-contract",
14896
+ title: "Assert on parsed policy behavior",
14897
+ outcome: "no-match",
14898
+ files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const policy = JSON.parse(readFileSync('policy.json', 'utf8')); expect(validate(policy)).toEqual([]); });" }],
14899
+ focusPath: "src/policy.test.ts",
14900
+ expectedCount: 0,
14901
+ public: true
14902
+ },
14903
+ {
14904
+ id: "terraform-substring-contract",
14905
+ title: "Do not prove Terraform behavior with a regex",
14906
+ outcome: "match",
14907
+ files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const source = readFileSync('main.tf', 'utf8'); expect(source).toMatch(/prevent_destroy/); });" }],
14908
+ focusPath: "src/policy.test.ts",
14909
+ expectedCount: 1,
14910
+ public: true
14911
+ }
14912
+ ]
14913
+ };
14914
+ function staticMemberName5(node) {
14915
+ if (!node.computed && node.property.type === AST_NODE_TYPES56.Identifier) return node.property.name;
14916
+ if (node.computed && node.property.type === AST_NODE_TYPES56.Literal && typeof node.property.value === "string") return node.property.value;
14917
+ return null;
14918
+ }
14919
+ function unwrap5(node) {
14920
+ if (node.type === AST_NODE_TYPES56.AwaitExpression) return unwrap5(node.argument);
14921
+ if (node.type === AST_NODE_TYPES56.ChainExpression) return unwrap5(node.expression);
14922
+ if (node.type === AST_NODE_TYPES56.TSAsExpression || node.type === AST_NODE_TYPES56.TSNonNullExpression || node.type === AST_NODE_TYPES56.TSTypeAssertion) return unwrap5(node.expression);
14923
+ return node;
14924
+ }
14925
+ function stringValue(node) {
14926
+ const current = unwrap5(node);
14927
+ if (current.type === AST_NODE_TYPES56.Literal && typeof current.value === "string") return current.value;
14928
+ if (current.type === AST_NODE_TYPES56.TemplateLiteral && current.expressions.length === 0) return current.quasis[0]?.value.cooked ?? null;
14929
+ return null;
14930
+ }
14931
+ function importSource(node) {
14932
+ return typeof node.source.value === "string" ? node.source.value : null;
14933
+ }
14934
+ function requireSource(node) {
14935
+ const current = unwrap5(node);
14936
+ if (current.type !== AST_NODE_TYPES56.CallExpression || current.callee.type !== AST_NODE_TYPES56.Identifier || current.callee.name !== "require" || current.arguments.length !== 1 || current.arguments[0]?.type === AST_NODE_TYPES56.SpreadElement) return null;
14937
+ return stringValue(current.arguments[0]);
14938
+ }
14939
+ function newScope() {
14940
+ return { collections: /* @__PURE__ */ new Set(), declared: /* @__PURE__ */ new Set(), fsObjects: /* @__PURE__ */ new Set(), fsReaders: /* @__PURE__ */ new Set(), paths: /* @__PURE__ */ new Set(), rawOrigins: /* @__PURE__ */ new Map() };
14941
+ }
14942
+ var source_coupled_test_default = createRule({
14943
+ name: "source-coupled-test",
14944
+ documentation: sourceCoupledTestDocumentation,
14945
+ meta: {
14946
+ type: "suggestion",
14947
+ docs: { description: sourceCoupledTestDocumentation.summary },
14948
+ schema: [],
14949
+ messages: { rawSourceOracle: "Raw repository source text is the oracle. Parse or execute the artifact so comments, formatting, and unreachable blocks cannot satisfy the contract." }
14950
+ },
14951
+ defaultOptions: [],
14952
+ create(context) {
14953
+ if (!isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
14954
+ const scopes = [newScope()];
14955
+ const reportedOrigins = /* @__PURE__ */ new Set();
14956
+ const currentScope = () => scopes.at(-1) ?? scopes[0];
14957
+ const visible = (kind, name) => {
14958
+ for (let index = scopes.length - 1; index >= 0; index--) {
14959
+ const scope = scopes[index];
14960
+ if (scope.declared.has(name)) return scope[kind].has(name);
14961
+ }
14962
+ return false;
14963
+ };
14964
+ const visibleRawOrigins = (name) => {
14965
+ for (let index = scopes.length - 1; index >= 0; index--) {
14966
+ const scope = scopes[index];
14967
+ if (scope.declared.has(name)) return scope.rawOrigins.get(name) ?? /* @__PURE__ */ new Set();
14968
+ }
14969
+ return /* @__PURE__ */ new Set();
14970
+ };
14971
+ const sourcePath = (node) => {
14972
+ const current = unwrap5(node);
14973
+ const value = stringValue(current);
14974
+ if (value !== null) return SOURCE_SUFFIX_RE.test(value);
14975
+ if (current.type === AST_NODE_TYPES56.Identifier) return visible("paths", current.name);
14976
+ if (current.type === AST_NODE_TYPES56.BinaryExpression && current.operator === "+") {
14977
+ return sourcePath(current.left) || sourcePath(current.right);
14978
+ }
14979
+ if (current.type === AST_NODE_TYPES56.TemplateLiteral) return current.expressions.some(sourcePath);
14980
+ if (current.type === AST_NODE_TYPES56.CallExpression || current.type === AST_NODE_TYPES56.NewExpression) {
14981
+ return current.arguments.some((argument) => argument.type !== AST_NODE_TYPES56.SpreadElement && sourcePath(argument));
14982
+ }
14983
+ if (current.type === AST_NODE_TYPES56.MemberExpression) return sourcePath(current.object);
14984
+ return false;
14985
+ };
14986
+ const rawRead = (node) => {
14987
+ const current = unwrap5(node);
14988
+ if (current.type !== AST_NODE_TYPES56.CallExpression || current.arguments.length === 0) return false;
14989
+ const callee = unwrap5(current.callee);
14990
+ if (callee.type === AST_NODE_TYPES56.Identifier) {
14991
+ return visible("fsReaders", callee.name) && sourcePath(current.arguments[0]);
14992
+ }
14993
+ if (callee.type !== AST_NODE_TYPES56.MemberExpression) return false;
14994
+ const name = staticMemberName5(callee);
14995
+ const object = unwrap5(callee.object);
14996
+ return name !== null && FS_READERS.has(name) && object.type === AST_NODE_TYPES56.Identifier && visible("fsObjects", object.name) && sourcePath(current.arguments[0]);
14997
+ };
14998
+ const rawOrigins = (node) => {
14999
+ const current = unwrap5(node);
15000
+ if (current.type === AST_NODE_TYPES56.Identifier) return visibleRawOrigins(current.name);
15001
+ if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
15002
+ if (current.type === AST_NODE_TYPES56.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
15003
+ if (current.type !== AST_NODE_TYPES56.CallExpression) return /* @__PURE__ */ new Set();
15004
+ const callee = unwrap5(current.callee);
15005
+ if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
15006
+ const name = staticMemberName5(callee);
15007
+ return name !== null && TEXT_TRANSFORMS.has(name) ? rawOrigins(callee.object) : /* @__PURE__ */ new Set();
15008
+ };
15009
+ const evidenceOrigins = (node) => {
15010
+ const current = unwrap5(node);
15011
+ const direct = rawOrigins(current);
15012
+ if (direct.size > 0) return direct;
15013
+ if (current.type === AST_NODE_TYPES56.BinaryExpression || current.type === AST_NODE_TYPES56.LogicalExpression) return /* @__PURE__ */ new Set([...evidenceOrigins(current.left), ...evidenceOrigins(current.right)]);
15014
+ if (current.type === AST_NODE_TYPES56.UnaryExpression) return evidenceOrigins(current.argument);
15015
+ if (current.type !== AST_NODE_TYPES56.CallExpression) return /* @__PURE__ */ new Set();
15016
+ const callee = unwrap5(current.callee);
15017
+ if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
15018
+ const name = staticMemberName5(callee);
15019
+ if (name !== null && TEXT_PREDICATES.has(name)) return rawOrigins(callee.object);
15020
+ if (name !== null && REGEXP_PREDICATES.has(name)) return new Set(current.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...rawOrigins(argument)]));
15021
+ return /* @__PURE__ */ new Set();
15022
+ };
15023
+ const rawAssertionOrigins = (node) => {
15024
+ const callee = unwrap5(node.callee);
15025
+ if (callee.type === AST_NODE_TYPES56.Identifier && callee.name === "assert") {
15026
+ return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15027
+ }
15028
+ if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
15029
+ const matcher = staticMemberName5(callee);
15030
+ if (matcher === null) return /* @__PURE__ */ new Set();
15031
+ let receiver = unwrap5(callee.object);
15032
+ while (receiver.type === AST_NODE_TYPES56.MemberExpression && EXPECT_MODIFIERS.has(staticMemberName5(receiver) ?? "")) receiver = unwrap5(receiver.object);
15033
+ if (receiver.type === AST_NODE_TYPES56.CallExpression && receiver.callee.type === AST_NODE_TYPES56.Identifier && receiver.callee.name === "expect") {
15034
+ if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
15035
+ return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15036
+ }
15037
+ if (receiver.type !== AST_NODE_TYPES56.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
15038
+ return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15039
+ };
15040
+ const declare = (name, state) => {
15041
+ const scope = currentScope();
15042
+ scope.declared.add(name);
15043
+ scope.collections.delete(name);
15044
+ scope.fsObjects.delete(name);
15045
+ scope.fsReaders.delete(name);
15046
+ scope.paths.delete(name);
15047
+ scope.rawOrigins.delete(name);
15048
+ if (state.collection === true) scope.collections.add(name);
15049
+ if (state.fsObject === true) scope.fsObjects.add(name);
15050
+ if (state.fsReader === true) scope.fsReaders.add(name);
15051
+ if (state.path === true) scope.paths.add(name);
15052
+ if (state.rawOrigins !== void 0 && state.rawOrigins.size > 0) {
15053
+ scope.rawOrigins.set(name, state.rawOrigins);
15054
+ }
15055
+ };
15056
+ const sourceCollection = (node) => {
15057
+ const current = unwrap5(node);
15058
+ return current.type === AST_NODE_TYPES56.ArrayExpression && current.elements.length > 0 && current.elements.every((element) => element !== null && element.type !== AST_NODE_TYPES56.SpreadElement && sourcePath(element));
15059
+ };
15060
+ const declaredNames2 = (node) => {
15061
+ const current = unwrap5(node);
15062
+ if (current.type === AST_NODE_TYPES56.Identifier) return [current.name];
15063
+ if (current.type === AST_NODE_TYPES56.AssignmentPattern) return declaredNames2(current.left);
15064
+ if (current.type === AST_NODE_TYPES56.RestElement) return declaredNames2(current.argument);
15065
+ if (current.type === AST_NODE_TYPES56.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
15066
+ if (current.type === AST_NODE_TYPES56.ObjectPattern) return current.properties.flatMap((property) => property.type === AST_NODE_TYPES56.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
15067
+ return [];
15068
+ };
15069
+ const enterFunction = (node) => {
15070
+ scopes.push(newScope());
15071
+ for (const parameter of node.params) for (const name of declaredNames2(parameter)) declare(name, {});
15072
+ };
15073
+ const exitFunction = () => {
15074
+ scopes.pop();
15075
+ };
15076
+ return {
15077
+ ImportDeclaration(node) {
15078
+ const source = importSource(node);
15079
+ if (source === null || !FS_MODULES.has(source)) return;
15080
+ for (const specifier of node.specifiers) {
15081
+ if (specifier.type === AST_NODE_TYPES56.ImportSpecifier) {
15082
+ const imported = specifier.imported.type === AST_NODE_TYPES56.Identifier ? specifier.imported.name : String(specifier.imported.value);
15083
+ if (FS_READERS.has(imported)) declare(specifier.local.name, { fsReader: true });
15084
+ } else {
15085
+ declare(specifier.local.name, { fsObject: true });
15086
+ }
15087
+ }
15088
+ },
15089
+ ":function": enterFunction,
15090
+ ":function:exit": exitFunction,
15091
+ VariableDeclarator(node) {
15092
+ if (node.init === null) return;
15093
+ const required = requireSource(node.init);
15094
+ if (required !== null && FS_MODULES.has(required) && node.id.type === AST_NODE_TYPES56.Identifier) {
15095
+ declare(node.id.name, { fsObject: true });
15096
+ return;
15097
+ }
15098
+ if (node.id.type === AST_NODE_TYPES56.ObjectPattern && required !== null && FS_MODULES.has(required)) {
15099
+ for (const property of node.id.properties) {
15100
+ if (property.type !== AST_NODE_TYPES56.Property || property.value.type !== AST_NODE_TYPES56.Identifier) continue;
15101
+ const key = property.key.type === AST_NODE_TYPES56.Identifier ? property.key.name : property.key.type === AST_NODE_TYPES56.Literal ? String(property.key.value) : "";
15102
+ if (FS_READERS.has(key)) declare(property.value.name, { fsReader: true });
15103
+ }
15104
+ return;
15105
+ }
15106
+ if (node.id.type !== AST_NODE_TYPES56.Identifier) return;
15107
+ declare(node.id.name, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
15108
+ },
15109
+ AssignmentExpression(node) {
15110
+ if (node.left.type === AST_NODE_TYPES56.Identifier) declare(node.left.name, { path: sourcePath(node.right), rawOrigins: rawOrigins(node.right) });
15111
+ },
15112
+ ForOfStatement(node) {
15113
+ const right = unwrap5(node.right);
15114
+ const collection = right.type === AST_NODE_TYPES56.Identifier && visible("collections", right.name);
15115
+ const left = node.left.type === AST_NODE_TYPES56.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
15116
+ if (collection && left?.type === AST_NODE_TYPES56.Identifier) declare(left.name, { path: true });
15117
+ },
15118
+ CallExpression(node) {
15119
+ const origins = rawAssertionOrigins(node);
15120
+ if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
15121
+ for (const origin of origins) reportedOrigins.add(origin);
15122
+ context.report({ node, messageId: "rawSourceOracle" });
15123
+ }
15124
+ };
15125
+ }
15126
+ });
15127
+
14765
15128
  // src/rules/zod-naming-convention.ts
14766
15129
  import {
14767
- AST_NODE_TYPES as AST_NODE_TYPES56,
15130
+ AST_NODE_TYPES as AST_NODE_TYPES57,
14768
15131
  ASTUtils as ASTUtils16
14769
15132
  } from "@typescript-eslint/utils";
14770
15133
  var zodNamingConventionDocumentation = {
@@ -14807,18 +15170,18 @@ var NON_SCHEMA_TERMINALS = /* @__PURE__ */ new Set([
14807
15170
  "prettifyError",
14808
15171
  "treeifyError"
14809
15172
  ]);
14810
- var terminalMethodName = (callee) => !callee.computed && callee.property.type === AST_NODE_TYPES56.Identifier ? callee.property.name : null;
15173
+ var terminalMethodName = (callee) => !callee.computed && callee.property.type === AST_NODE_TYPES57.Identifier ? callee.property.name : null;
14811
15174
  var calleeChainRoot = (node) => {
14812
15175
  let current = node;
14813
15176
  for (; ; ) {
14814
- if (current.type === AST_NODE_TYPES56.Identifier) {
15177
+ if (current.type === AST_NODE_TYPES57.Identifier) {
14815
15178
  return current;
14816
15179
  }
14817
- if (current.type === AST_NODE_TYPES56.MemberExpression) {
15180
+ if (current.type === AST_NODE_TYPES57.MemberExpression) {
14818
15181
  current = current.object;
14819
15182
  continue;
14820
15183
  }
14821
- if (current.type === AST_NODE_TYPES56.CallExpression) {
15184
+ if (current.type === AST_NODE_TYPES57.CallExpression) {
14822
15185
  current = current.callee;
14823
15186
  continue;
14824
15187
  }
@@ -14880,7 +15243,7 @@ var zod_naming_convention_default = createRule({
14880
15243
  ImportDeclaration(node) {
14881
15244
  if (!isZodModule(node.source.value)) return;
14882
15245
  for (const specifier of node.specifiers) {
14883
- if (specifier.type === AST_NODE_TYPES56.ImportNamespaceSpecifier || specifier.type === AST_NODE_TYPES56.ImportDefaultSpecifier || specifier.type === AST_NODE_TYPES56.ImportSpecifier && (specifier.imported.type === AST_NODE_TYPES56.Identifier ? specifier.imported.name === "z" : specifier.imported.value === "z")) {
15246
+ if (specifier.type === AST_NODE_TYPES57.ImportNamespaceSpecifier || specifier.type === AST_NODE_TYPES57.ImportDefaultSpecifier || specifier.type === AST_NODE_TYPES57.ImportSpecifier && (specifier.imported.type === AST_NODE_TYPES57.Identifier ? specifier.imported.name === "z" : specifier.imported.value === "z")) {
14884
15247
  recordZodBinding(specifier.local);
14885
15248
  }
14886
15249
  }
@@ -14888,13 +15251,13 @@ var zod_naming_convention_default = createRule({
14888
15251
  VariableDeclarator(node) {
14889
15252
  const init = node.init;
14890
15253
  if (init === null || init === void 0) return;
14891
- if (init.type !== AST_NODE_TYPES56.CallExpression) return;
15254
+ if (init.type !== AST_NODE_TYPES57.CallExpression) return;
14892
15255
  const callee = init.callee;
14893
- if (callee.type !== AST_NODE_TYPES56.MemberExpression) return;
15256
+ if (callee.type !== AST_NODE_TYPES57.MemberExpression) return;
14894
15257
  if (!isZodChain(callee)) return;
14895
15258
  const terminal = terminalMethodName(callee);
14896
15259
  if (terminal !== null && NON_SCHEMA_TERMINALS.has(terminal)) return;
14897
- if (node.id.type !== AST_NODE_TYPES56.Identifier) return;
15260
+ if (node.id.type !== AST_NODE_TYPES57.Identifier) return;
14898
15261
  if (test.test(node.id.name)) return;
14899
15262
  if (acceptsSchemaWord && CONTAINS_SCHEMA_RE.test(node.id.name)) return;
14900
15263
  context.report({
@@ -15003,6 +15366,7 @@ var rules = {
15003
15366
  "no-impossible-zod-literal-bounds": no_impossible_zod_literal_bounds_default,
15004
15367
  "no-log-only-catch": no_log_only_catch_default,
15005
15368
  "no-long-comment": no_long_comment_default,
15369
+ "no-vague-suppression-description": no_vague_suppression_description_default,
15006
15370
  "no-generic-single-export-module": no_generic_single_export_module_default,
15007
15371
  "no-offset-pagination": no_offset_pagination_default,
15008
15372
  "no-positional-tuple-return": no_positional_tuple_return_default,
@@ -15051,17 +15415,19 @@ var rules = {
15051
15415
  "require-zod-form-validation": require_zod_form_validation_default,
15052
15416
  "store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
15053
15417
  "stepdown": stepdown_default,
15418
+ "source-coupled-test": source_coupled_test_default,
15054
15419
  "zod-naming-convention": zod_naming_convention_default
15055
15420
  };
15056
15421
  var meta = {
15057
15422
  name: "@sarj/eslint-plugin",
15058
- version: "15.6.7"
15423
+ version: "15.6.9"
15059
15424
  };
15060
15425
  var applicationOnlyRules = [
15061
15426
  "no-restricted-library-load",
15062
15427
  "prefer-native-random-uuid",
15063
15428
  "prefer-shadcn-primitives"
15064
15429
  ];
15430
+ var advisoryRules = ["no-vague-suppression-description", "source-coupled-test"];
15065
15431
  var recommendedRules = {
15066
15432
  "@sarj/duplicate-test-body": "error",
15067
15433
  "@sarj/enforce-file-structure": "error",
@@ -15077,6 +15443,7 @@ var recommendedRules = {
15077
15443
  "@sarj/no-impossible-zod-literal-bounds": "error",
15078
15444
  "@sarj/no-log-only-catch": "error",
15079
15445
  "@sarj/no-long-comment": "error",
15446
+ "@sarj/no-vague-suppression-description": "warn",
15080
15447
  "@sarj/no-generic-single-export-module": "error",
15081
15448
  "@sarj/no-offset-pagination": "error",
15082
15449
  "@sarj/no-positional-tuple-return": "error",
@@ -15119,6 +15486,7 @@ var recommendedRules = {
15119
15486
  "@sarj/require-zod-form-validation": "error",
15120
15487
  "@sarj/store-insert-requires-on-conflict": "error",
15121
15488
  "@sarj/stepdown": "error",
15489
+ "@sarj/source-coupled-test": "warn",
15122
15490
  "@sarj/zod-naming-convention": "error"
15123
15491
  };
15124
15492
  var strictRules = {
@@ -15137,6 +15505,7 @@ var strictRules = {
15137
15505
  "@sarj/no-impossible-zod-literal-bounds": "error",
15138
15506
  "@sarj/no-log-only-catch": "error",
15139
15507
  "@sarj/no-long-comment": "error",
15508
+ "@sarj/no-vague-suppression-description": "warn",
15140
15509
  "@sarj/no-generic-single-export-module": "error",
15141
15510
  "@sarj/no-offset-pagination": "error",
15142
15511
  "@sarj/no-positional-tuple-return": "error",
@@ -15182,6 +15551,7 @@ var strictRules = {
15182
15551
  "@sarj/require-zod-form-validation": "error",
15183
15552
  "@sarj/store-insert-requires-on-conflict": "error",
15184
15553
  "@sarj/stepdown": "error",
15554
+ "@sarj/source-coupled-test": "warn",
15185
15555
  "@sarj/zod-naming-convention": "error"
15186
15556
  };
15187
15557
  var plugin = {
@@ -15205,6 +15575,7 @@ var plugin = {
15205
15575
  };
15206
15576
  var index_default = plugin;
15207
15577
  export {
15578
+ advisoryRules,
15208
15579
  applicationOnlyRules,
15209
15580
  index_default as default,
15210
15581
  publicDocumentation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "15.6.7",
3
+ "version": "15.6.9",
4
4
  "packageManager": "npm@12.0.2",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",