@sarj/eslint-plugin 15.6.6 → 15.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,
@@ -474,7 +475,7 @@ var duplicateTestBodyDocumentation = {
474
475
  outcome: "no-match",
475
476
  files: [{
476
477
  path: "src/user.test.ts",
477
- source: "test.each(['a', 'b'])('parses %s', (value) => { const x = parse(value); expect(x.ok).toBe(true); expect(x.value).toBe(value); });"
478
+ source: "test.each(['a', 'b'])('accepts %s', (value) => { const result = parse(value); expect(result.ok).toBe(true); expect(result.value).toBe(value); });"
478
479
  }],
479
480
  focusPath: "src/user.test.ts",
480
481
  expectedCount: 0,
@@ -486,7 +487,7 @@ var duplicateTestBodyDocumentation = {
486
487
  outcome: "match",
487
488
  files: [{
488
489
  path: "src/user.test.ts",
489
- source: "test('accepts a', () => { const result = parse('a'); expect(result.ok).toBe(true); expect(result.value).toBeDefined(); });\ntest('accepts b', () => { const result = parse('b'); expect(result.ok).toBe(true); expect(result.value).toBeDefined(); });"
490
+ source: "test('accepts a', () => { const value = 'a'; const result = parse(value); expect(result.ok).toBe(true); expect(result.value).toBe(value); });\ntest('accepts b', () => { const value = 'b'; const result = parse(value); expect(result.ok).toBe(true); expect(result.value).toBe(value); });"
490
491
  }],
491
492
  focusPath: "src/user.test.ts",
492
493
  expectedCount: 1,
@@ -14784,8 +14785,290 @@ var stepdown_default = createRule({
14784
14785
  }
14785
14786
  });
14786
14787
 
14787
- // src/rules/zod-naming-convention.ts
14788
+ // src/rules/source-coupled-test.ts
14788
14789
  var import_utils70 = require("@typescript-eslint/utils");
14790
+ var SOURCE_SUFFIX_RE = /\.(?:bash|hcl|sh|tf|tfvars|ya?ml|py|[cm]?[jt]s)$/iu;
14791
+ var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
14792
+ var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
14793
+ var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
14794
+ "slice",
14795
+ "substring",
14796
+ "substr",
14797
+ "toLowerCase",
14798
+ "toString",
14799
+ "toUpperCase",
14800
+ "trim",
14801
+ "trimEnd",
14802
+ "trimStart",
14803
+ "replace",
14804
+ "replaceAll"
14805
+ ]);
14806
+ var TEXT_PREDICATES = /* @__PURE__ */ new Set(["endsWith", "includes", "indexOf", "lastIndexOf", "match", "matchAll", "search", "startsWith"]);
14807
+ var REGEXP_PREDICATES = /* @__PURE__ */ new Set(["exec", "test"]);
14808
+ var EXPECT_MATCHERS = /* @__PURE__ */ new Set([
14809
+ "toBe",
14810
+ "toBeFalsy",
14811
+ "toBeGreaterThan",
14812
+ "toBeGreaterThanOrEqual",
14813
+ "toBeLessThan",
14814
+ "toBeLessThanOrEqual",
14815
+ "toBeNull",
14816
+ "toBeTruthy",
14817
+ "toContain",
14818
+ "toEqual",
14819
+ "toHaveLength",
14820
+ "toMatch",
14821
+ "toMatchSnapshot",
14822
+ "toStrictEqual"
14823
+ ]);
14824
+ var EXPECT_MODIFIERS = /* @__PURE__ */ new Set(["not", "rejects", "resolves"]);
14825
+ var ASSERT_MATCHERS = /* @__PURE__ */ new Set(["deepEqual", "doesNotMatch", "equal", "match", "notDeepEqual", "notEqual", "notStrictEqual", "ok", "strictEqual"]);
14826
+ var sourceCoupledTestDocumentation = {
14827
+ summary: "Disallow raw repository source text as a test oracle; parse or execute the artifact instead.",
14828
+ rationale: "Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
14829
+ remediation: "Parse the artifact, execute its validator, or assert on Terraform plan JSON or another runtime contract.",
14830
+ category: "testing",
14831
+ limitations: [
14832
+ "The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
14833
+ "When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."
14834
+ ],
14835
+ examples: [
14836
+ {
14837
+ id: "parsed-policy-contract",
14838
+ title: "Assert on parsed policy behavior",
14839
+ outcome: "no-match",
14840
+ 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([]); });" }],
14841
+ focusPath: "src/policy.test.ts",
14842
+ expectedCount: 0,
14843
+ public: true
14844
+ },
14845
+ {
14846
+ id: "terraform-substring-contract",
14847
+ title: "Do not prove Terraform behavior with a regex",
14848
+ outcome: "match",
14849
+ 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/); });" }],
14850
+ focusPath: "src/policy.test.ts",
14851
+ expectedCount: 1,
14852
+ public: true
14853
+ }
14854
+ ]
14855
+ };
14856
+ function staticMemberName5(node) {
14857
+ if (!node.computed && node.property.type === import_utils70.AST_NODE_TYPES.Identifier) return node.property.name;
14858
+ if (node.computed && node.property.type === import_utils70.AST_NODE_TYPES.Literal && typeof node.property.value === "string") return node.property.value;
14859
+ return null;
14860
+ }
14861
+ function unwrap5(node) {
14862
+ if (node.type === import_utils70.AST_NODE_TYPES.AwaitExpression) return unwrap5(node.argument);
14863
+ if (node.type === import_utils70.AST_NODE_TYPES.ChainExpression) return unwrap5(node.expression);
14864
+ 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);
14865
+ return node;
14866
+ }
14867
+ function stringValue(node) {
14868
+ const current = unwrap5(node);
14869
+ if (current.type === import_utils70.AST_NODE_TYPES.Literal && typeof current.value === "string") return current.value;
14870
+ if (current.type === import_utils70.AST_NODE_TYPES.TemplateLiteral && current.expressions.length === 0) return current.quasis[0]?.value.cooked ?? null;
14871
+ return null;
14872
+ }
14873
+ function importSource(node) {
14874
+ return typeof node.source.value === "string" ? node.source.value : null;
14875
+ }
14876
+ function requireSource(node) {
14877
+ const current = unwrap5(node);
14878
+ 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;
14879
+ return stringValue(current.arguments[0]);
14880
+ }
14881
+ function newScope() {
14882
+ 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() };
14883
+ }
14884
+ var source_coupled_test_default = createRule({
14885
+ name: "source-coupled-test",
14886
+ documentation: sourceCoupledTestDocumentation,
14887
+ meta: {
14888
+ type: "suggestion",
14889
+ docs: { description: sourceCoupledTestDocumentation.summary },
14890
+ schema: [],
14891
+ messages: { rawSourceOracle: "Raw repository source text is the oracle. Parse or execute the artifact so comments, formatting, and unreachable blocks cannot satisfy the contract." }
14892
+ },
14893
+ defaultOptions: [],
14894
+ create(context) {
14895
+ if (!isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
14896
+ const scopes = [newScope()];
14897
+ const reportedOrigins = /* @__PURE__ */ new Set();
14898
+ const currentScope = () => scopes.at(-1) ?? scopes[0];
14899
+ const visible = (kind, name) => {
14900
+ for (let index = scopes.length - 1; index >= 0; index--) {
14901
+ const scope = scopes[index];
14902
+ if (scope.declared.has(name)) return scope[kind].has(name);
14903
+ }
14904
+ return false;
14905
+ };
14906
+ const visibleRawOrigins = (name) => {
14907
+ for (let index = scopes.length - 1; index >= 0; index--) {
14908
+ const scope = scopes[index];
14909
+ if (scope.declared.has(name)) return scope.rawOrigins.get(name) ?? /* @__PURE__ */ new Set();
14910
+ }
14911
+ return /* @__PURE__ */ new Set();
14912
+ };
14913
+ const sourcePath = (node) => {
14914
+ const current = unwrap5(node);
14915
+ const value = stringValue(current);
14916
+ if (value !== null) return SOURCE_SUFFIX_RE.test(value);
14917
+ if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return visible("paths", current.name);
14918
+ if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression && current.operator === "+") {
14919
+ return sourcePath(current.left) || sourcePath(current.right);
14920
+ }
14921
+ if (current.type === import_utils70.AST_NODE_TYPES.TemplateLiteral) return current.expressions.some(sourcePath);
14922
+ if (current.type === import_utils70.AST_NODE_TYPES.CallExpression || current.type === import_utils70.AST_NODE_TYPES.NewExpression) {
14923
+ return current.arguments.some((argument) => argument.type !== import_utils70.AST_NODE_TYPES.SpreadElement && sourcePath(argument));
14924
+ }
14925
+ if (current.type === import_utils70.AST_NODE_TYPES.MemberExpression) return sourcePath(current.object);
14926
+ return false;
14927
+ };
14928
+ const rawRead = (node) => {
14929
+ const current = unwrap5(node);
14930
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression || current.arguments.length === 0) return false;
14931
+ const callee = unwrap5(current.callee);
14932
+ if (callee.type === import_utils70.AST_NODE_TYPES.Identifier) {
14933
+ return visible("fsReaders", callee.name) && sourcePath(current.arguments[0]);
14934
+ }
14935
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return false;
14936
+ const name = staticMemberName5(callee);
14937
+ const object = unwrap5(callee.object);
14938
+ return name !== null && FS_READERS.has(name) && object.type === import_utils70.AST_NODE_TYPES.Identifier && visible("fsObjects", object.name) && sourcePath(current.arguments[0]);
14939
+ };
14940
+ const rawOrigins = (node) => {
14941
+ const current = unwrap5(node);
14942
+ if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return visibleRawOrigins(current.name);
14943
+ if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
14944
+ if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
14945
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression) return /* @__PURE__ */ new Set();
14946
+ const callee = unwrap5(current.callee);
14947
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
14948
+ const name = staticMemberName5(callee);
14949
+ return name !== null && TEXT_TRANSFORMS.has(name) ? rawOrigins(callee.object) : /* @__PURE__ */ new Set();
14950
+ };
14951
+ const evidenceOrigins = (node) => {
14952
+ const current = unwrap5(node);
14953
+ const direct = rawOrigins(current);
14954
+ if (direct.size > 0) return direct;
14955
+ 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)]);
14956
+ if (current.type === import_utils70.AST_NODE_TYPES.UnaryExpression) return evidenceOrigins(current.argument);
14957
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression) return /* @__PURE__ */ new Set();
14958
+ const callee = unwrap5(current.callee);
14959
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
14960
+ const name = staticMemberName5(callee);
14961
+ if (name !== null && TEXT_PREDICATES.has(name)) return rawOrigins(callee.object);
14962
+ if (name !== null && REGEXP_PREDICATES.has(name)) return new Set(current.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...rawOrigins(argument)]));
14963
+ return /* @__PURE__ */ new Set();
14964
+ };
14965
+ const rawAssertionOrigins = (node) => {
14966
+ const callee = unwrap5(node.callee);
14967
+ if (callee.type === import_utils70.AST_NODE_TYPES.Identifier && callee.name === "assert") {
14968
+ return new Set(node.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
14969
+ }
14970
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
14971
+ const matcher = staticMemberName5(callee);
14972
+ if (matcher === null) return /* @__PURE__ */ new Set();
14973
+ let receiver = unwrap5(callee.object);
14974
+ while (receiver.type === import_utils70.AST_NODE_TYPES.MemberExpression && EXPECT_MODIFIERS.has(staticMemberName5(receiver) ?? "")) receiver = unwrap5(receiver.object);
14975
+ if (receiver.type === import_utils70.AST_NODE_TYPES.CallExpression && receiver.callee.type === import_utils70.AST_NODE_TYPES.Identifier && receiver.callee.name === "expect") {
14976
+ if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
14977
+ return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
14978
+ }
14979
+ if (receiver.type !== import_utils70.AST_NODE_TYPES.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
14980
+ return new Set(node.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
14981
+ };
14982
+ const declare = (name, state) => {
14983
+ const scope = currentScope();
14984
+ scope.declared.add(name);
14985
+ scope.collections.delete(name);
14986
+ scope.fsObjects.delete(name);
14987
+ scope.fsReaders.delete(name);
14988
+ scope.paths.delete(name);
14989
+ scope.rawOrigins.delete(name);
14990
+ if (state.collection === true) scope.collections.add(name);
14991
+ if (state.fsObject === true) scope.fsObjects.add(name);
14992
+ if (state.fsReader === true) scope.fsReaders.add(name);
14993
+ if (state.path === true) scope.paths.add(name);
14994
+ if (state.rawOrigins !== void 0 && state.rawOrigins.size > 0) {
14995
+ scope.rawOrigins.set(name, state.rawOrigins);
14996
+ }
14997
+ };
14998
+ const sourceCollection = (node) => {
14999
+ const current = unwrap5(node);
15000
+ 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));
15001
+ };
15002
+ const declaredNames2 = (node) => {
15003
+ const current = unwrap5(node);
15004
+ if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return [current.name];
15005
+ if (current.type === import_utils70.AST_NODE_TYPES.AssignmentPattern) return declaredNames2(current.left);
15006
+ if (current.type === import_utils70.AST_NODE_TYPES.RestElement) return declaredNames2(current.argument);
15007
+ if (current.type === import_utils70.AST_NODE_TYPES.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
15008
+ 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));
15009
+ return [];
15010
+ };
15011
+ const enterFunction = (node) => {
15012
+ scopes.push(newScope());
15013
+ for (const parameter of node.params) for (const name of declaredNames2(parameter)) declare(name, {});
15014
+ };
15015
+ const exitFunction = () => {
15016
+ scopes.pop();
15017
+ };
15018
+ return {
15019
+ ImportDeclaration(node) {
15020
+ const source = importSource(node);
15021
+ if (source === null || !FS_MODULES.has(source)) return;
15022
+ for (const specifier of node.specifiers) {
15023
+ if (specifier.type === import_utils70.AST_NODE_TYPES.ImportSpecifier) {
15024
+ const imported = specifier.imported.type === import_utils70.AST_NODE_TYPES.Identifier ? specifier.imported.name : String(specifier.imported.value);
15025
+ if (FS_READERS.has(imported)) declare(specifier.local.name, { fsReader: true });
15026
+ } else {
15027
+ declare(specifier.local.name, { fsObject: true });
15028
+ }
15029
+ }
15030
+ },
15031
+ ":function": enterFunction,
15032
+ ":function:exit": exitFunction,
15033
+ VariableDeclarator(node) {
15034
+ if (node.init === null) return;
15035
+ const required = requireSource(node.init);
15036
+ if (required !== null && FS_MODULES.has(required) && node.id.type === import_utils70.AST_NODE_TYPES.Identifier) {
15037
+ declare(node.id.name, { fsObject: true });
15038
+ return;
15039
+ }
15040
+ if (node.id.type === import_utils70.AST_NODE_TYPES.ObjectPattern && required !== null && FS_MODULES.has(required)) {
15041
+ for (const property of node.id.properties) {
15042
+ if (property.type !== import_utils70.AST_NODE_TYPES.Property || property.value.type !== import_utils70.AST_NODE_TYPES.Identifier) continue;
15043
+ 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) : "";
15044
+ if (FS_READERS.has(key)) declare(property.value.name, { fsReader: true });
15045
+ }
15046
+ return;
15047
+ }
15048
+ if (node.id.type !== import_utils70.AST_NODE_TYPES.Identifier) return;
15049
+ declare(node.id.name, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
15050
+ },
15051
+ AssignmentExpression(node) {
15052
+ if (node.left.type === import_utils70.AST_NODE_TYPES.Identifier) declare(node.left.name, { path: sourcePath(node.right), rawOrigins: rawOrigins(node.right) });
15053
+ },
15054
+ ForOfStatement(node) {
15055
+ const right = unwrap5(node.right);
15056
+ const collection = right.type === import_utils70.AST_NODE_TYPES.Identifier && visible("collections", right.name);
15057
+ const left = node.left.type === import_utils70.AST_NODE_TYPES.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
15058
+ if (collection && left?.type === import_utils70.AST_NODE_TYPES.Identifier) declare(left.name, { path: true });
15059
+ },
15060
+ CallExpression(node) {
15061
+ const origins = rawAssertionOrigins(node);
15062
+ if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
15063
+ for (const origin of origins) reportedOrigins.add(origin);
15064
+ context.report({ node, messageId: "rawSourceOracle" });
15065
+ }
15066
+ };
15067
+ }
15068
+ });
15069
+
15070
+ // src/rules/zod-naming-convention.ts
15071
+ var import_utils71 = require("@typescript-eslint/utils");
14789
15072
  var zodNamingConventionDocumentation = {
14790
15073
  summary: "Enforce a consistent Zod schema naming convention \u2014 a `Z` prefix (`ZUser`) or a `Schema` suffix (`userSchema`); both are accepted by default.",
14791
15074
  rationale: "A recognizable schema name distinguishes runtime validators from ordinary values at each use site.",
@@ -14826,18 +15109,18 @@ var NON_SCHEMA_TERMINALS = /* @__PURE__ */ new Set([
14826
15109
  "prettifyError",
14827
15110
  "treeifyError"
14828
15111
  ]);
14829
- var terminalMethodName = (callee) => !callee.computed && callee.property.type === import_utils70.AST_NODE_TYPES.Identifier ? callee.property.name : null;
15112
+ var terminalMethodName = (callee) => !callee.computed && callee.property.type === import_utils71.AST_NODE_TYPES.Identifier ? callee.property.name : null;
14830
15113
  var calleeChainRoot = (node) => {
14831
15114
  let current = node;
14832
15115
  for (; ; ) {
14833
- if (current.type === import_utils70.AST_NODE_TYPES.Identifier) {
15116
+ if (current.type === import_utils71.AST_NODE_TYPES.Identifier) {
14834
15117
  return current;
14835
15118
  }
14836
- if (current.type === import_utils70.AST_NODE_TYPES.MemberExpression) {
15119
+ if (current.type === import_utils71.AST_NODE_TYPES.MemberExpression) {
14837
15120
  current = current.object;
14838
15121
  continue;
14839
15122
  }
14840
- if (current.type === import_utils70.AST_NODE_TYPES.CallExpression) {
15123
+ if (current.type === import_utils71.AST_NODE_TYPES.CallExpression) {
14841
15124
  current = current.callee;
14842
15125
  continue;
14843
15126
  }
@@ -14877,7 +15160,7 @@ var zod_naming_convention_default = createRule({
14877
15160
  const acceptsSchemaWord = convention !== "prefix";
14878
15161
  const zodBindings = /* @__PURE__ */ new Set();
14879
15162
  function resolvedBinding(identifier) {
14880
- return import_utils70.ASTUtils.findVariable(
15163
+ return import_utils71.ASTUtils.findVariable(
14881
15164
  context.sourceCode.getScope(identifier),
14882
15165
  identifier.name
14883
15166
  );
@@ -14899,7 +15182,7 @@ var zod_naming_convention_default = createRule({
14899
15182
  ImportDeclaration(node) {
14900
15183
  if (!isZodModule(node.source.value)) return;
14901
15184
  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")) {
15185
+ 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
15186
  recordZodBinding(specifier.local);
14904
15187
  }
14905
15188
  }
@@ -14907,13 +15190,13 @@ var zod_naming_convention_default = createRule({
14907
15190
  VariableDeclarator(node) {
14908
15191
  const init = node.init;
14909
15192
  if (init === null || init === void 0) return;
14910
- if (init.type !== import_utils70.AST_NODE_TYPES.CallExpression) return;
15193
+ if (init.type !== import_utils71.AST_NODE_TYPES.CallExpression) return;
14911
15194
  const callee = init.callee;
14912
- if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return;
15195
+ if (callee.type !== import_utils71.AST_NODE_TYPES.MemberExpression) return;
14913
15196
  if (!isZodChain(callee)) return;
14914
15197
  const terminal = terminalMethodName(callee);
14915
15198
  if (terminal !== null && NON_SCHEMA_TERMINALS.has(terminal)) return;
14916
- if (node.id.type !== import_utils70.AST_NODE_TYPES.Identifier) return;
15199
+ if (node.id.type !== import_utils71.AST_NODE_TYPES.Identifier) return;
14917
15200
  if (test.test(node.id.name)) return;
14918
15201
  if (acceptsSchemaWord && CONTAINS_SCHEMA_RE.test(node.id.name)) return;
14919
15202
  context.report({
@@ -15070,17 +15353,19 @@ var rules = {
15070
15353
  "require-zod-form-validation": require_zod_form_validation_default,
15071
15354
  "store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
15072
15355
  "stepdown": stepdown_default,
15356
+ "source-coupled-test": source_coupled_test_default,
15073
15357
  "zod-naming-convention": zod_naming_convention_default
15074
15358
  };
15075
15359
  var meta = {
15076
15360
  name: "@sarj/eslint-plugin",
15077
- version: "15.6.6"
15361
+ version: "15.6.8"
15078
15362
  };
15079
15363
  var applicationOnlyRules = [
15080
15364
  "no-restricted-library-load",
15081
15365
  "prefer-native-random-uuid",
15082
15366
  "prefer-shadcn-primitives"
15083
15367
  ];
15368
+ var advisoryRules = ["source-coupled-test"];
15084
15369
  var recommendedRules = {
15085
15370
  "@sarj/duplicate-test-body": "error",
15086
15371
  "@sarj/enforce-file-structure": "error",
@@ -15138,6 +15423,7 @@ var recommendedRules = {
15138
15423
  "@sarj/require-zod-form-validation": "error",
15139
15424
  "@sarj/store-insert-requires-on-conflict": "error",
15140
15425
  "@sarj/stepdown": "error",
15426
+ "@sarj/source-coupled-test": "warn",
15141
15427
  "@sarj/zod-naming-convention": "error"
15142
15428
  };
15143
15429
  var strictRules = {
@@ -15201,6 +15487,7 @@ var strictRules = {
15201
15487
  "@sarj/require-zod-form-validation": "error",
15202
15488
  "@sarj/store-insert-requires-on-conflict": "error",
15203
15489
  "@sarj/stepdown": "error",
15490
+ "@sarj/source-coupled-test": "warn",
15204
15491
  "@sarj/zod-naming-convention": "error"
15205
15492
  };
15206
15493
  var plugin = {
@@ -15225,6 +15512,7 @@ var plugin = {
15225
15512
  var index_default = plugin;
15226
15513
  // Annotate the CommonJS export names for ESM import in node:
15227
15514
  0 && (module.exports = {
15515
+ advisoryRules,
15228
15516
  applicationOnlyRules,
15229
15517
  publicDocumentation,
15230
15518
  recommendedRules,
package/dist/index.d.cts CHANGED
@@ -274,12 +274,15 @@ declare const rules: {
274
274
  readonly "require-zod-form-validation": DocumentedRule<readonly [], "missingZodValidation">;
275
275
  readonly "store-insert-requires-on-conflict": DocumentedRule<readonly [], "storeInsertRequiresOnConflict">;
276
276
  readonly stepdown: DocumentedRule<[], "helperAboveOnlyCaller">;
277
+ readonly "source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
277
278
  readonly "zod-naming-convention": DocumentedRule<readonly [{
278
279
  convention?: "prefix" | "suffix" | "either";
279
280
  }?], "zPrefix" | "schemaSuffix" | "zodSchemaName">;
280
281
  };
281
282
  /** Rules registered for application-profile configs but intentionally absent from general presets. */
282
283
  declare const applicationOnlyRules: readonly ["no-restricted-library-load", "prefer-native-random-uuid", "prefer-shadcn-primitives"];
284
+ /** Calibrated rules that intentionally remain warnings until consumer-corpus precision is proven. */
285
+ declare const advisoryRules: readonly ["source-coupled-test"];
283
286
  declare const recommendedRules: {
284
287
  readonly "@sarj/duplicate-test-body": "error";
285
288
  readonly "@sarj/enforce-file-structure": "error";
@@ -341,6 +344,7 @@ declare const recommendedRules: {
341
344
  readonly "@sarj/require-zod-form-validation": "error";
342
345
  readonly "@sarj/store-insert-requires-on-conflict": "error";
343
346
  readonly "@sarj/stepdown": "error";
347
+ readonly "@sarj/source-coupled-test": "warn";
344
348
  readonly "@sarj/zod-naming-convention": "error";
345
349
  };
346
350
  declare const strictRules: {
@@ -408,6 +412,7 @@ declare const strictRules: {
408
412
  readonly "@sarj/require-zod-form-validation": "error";
409
413
  readonly "@sarj/store-insert-requires-on-conflict": "error";
410
414
  readonly "@sarj/stepdown": "error";
415
+ readonly "@sarj/source-coupled-test": "warn";
411
416
  readonly "@sarj/zod-naming-convention": "error";
412
417
  };
413
418
  type FlatPreset = {
@@ -418,7 +423,7 @@ type FlatPreset = {
418
423
  declare const plugin: {
419
424
  readonly meta: {
420
425
  readonly name: "@sarj/eslint-plugin";
421
- readonly version: "15.6.6";
426
+ readonly version: "15.6.8";
422
427
  };
423
428
  readonly rules: {
424
429
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
@@ -507,6 +512,7 @@ declare const plugin: {
507
512
  readonly "require-zod-form-validation": DocumentedRule<readonly [], "missingZodValidation">;
508
513
  readonly "store-insert-requires-on-conflict": DocumentedRule<readonly [], "storeInsertRequiresOnConflict">;
509
514
  readonly stepdown: DocumentedRule<[], "helperAboveOnlyCaller">;
515
+ readonly "source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
510
516
  readonly "zod-naming-convention": DocumentedRule<readonly [{
511
517
  convention?: "prefix" | "suffix" | "either";
512
518
  }?], "zPrefix" | "schemaSuffix" | "zodSchemaName">;
@@ -518,4 +524,4 @@ declare const plugin: {
518
524
  };
519
525
  };
520
526
 
521
- export { type RetiredRule, applicationOnlyRules, plugin as default, publicDocumentation, recommendedRules, renamedRules, retiredRules, rules, strictRules };
527
+ export { type RetiredRule, advisoryRules, applicationOnlyRules, plugin as default, publicDocumentation, recommendedRules, renamedRules, retiredRules, rules, strictRules };
package/dist/index.d.ts CHANGED
@@ -274,12 +274,15 @@ declare const rules: {
274
274
  readonly "require-zod-form-validation": DocumentedRule<readonly [], "missingZodValidation">;
275
275
  readonly "store-insert-requires-on-conflict": DocumentedRule<readonly [], "storeInsertRequiresOnConflict">;
276
276
  readonly stepdown: DocumentedRule<[], "helperAboveOnlyCaller">;
277
+ readonly "source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
277
278
  readonly "zod-naming-convention": DocumentedRule<readonly [{
278
279
  convention?: "prefix" | "suffix" | "either";
279
280
  }?], "zPrefix" | "schemaSuffix" | "zodSchemaName">;
280
281
  };
281
282
  /** Rules registered for application-profile configs but intentionally absent from general presets. */
282
283
  declare const applicationOnlyRules: readonly ["no-restricted-library-load", "prefer-native-random-uuid", "prefer-shadcn-primitives"];
284
+ /** Calibrated rules that intentionally remain warnings until consumer-corpus precision is proven. */
285
+ declare const advisoryRules: readonly ["source-coupled-test"];
283
286
  declare const recommendedRules: {
284
287
  readonly "@sarj/duplicate-test-body": "error";
285
288
  readonly "@sarj/enforce-file-structure": "error";
@@ -341,6 +344,7 @@ declare const recommendedRules: {
341
344
  readonly "@sarj/require-zod-form-validation": "error";
342
345
  readonly "@sarj/store-insert-requires-on-conflict": "error";
343
346
  readonly "@sarj/stepdown": "error";
347
+ readonly "@sarj/source-coupled-test": "warn";
344
348
  readonly "@sarj/zod-naming-convention": "error";
345
349
  };
346
350
  declare const strictRules: {
@@ -408,6 +412,7 @@ declare const strictRules: {
408
412
  readonly "@sarj/require-zod-form-validation": "error";
409
413
  readonly "@sarj/store-insert-requires-on-conflict": "error";
410
414
  readonly "@sarj/stepdown": "error";
415
+ readonly "@sarj/source-coupled-test": "warn";
411
416
  readonly "@sarj/zod-naming-convention": "error";
412
417
  };
413
418
  type FlatPreset = {
@@ -418,7 +423,7 @@ type FlatPreset = {
418
423
  declare const plugin: {
419
424
  readonly meta: {
420
425
  readonly name: "@sarj/eslint-plugin";
421
- readonly version: "15.6.6";
426
+ readonly version: "15.6.8";
422
427
  };
423
428
  readonly rules: {
424
429
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
@@ -507,6 +512,7 @@ declare const plugin: {
507
512
  readonly "require-zod-form-validation": DocumentedRule<readonly [], "missingZodValidation">;
508
513
  readonly "store-insert-requires-on-conflict": DocumentedRule<readonly [], "storeInsertRequiresOnConflict">;
509
514
  readonly stepdown: DocumentedRule<[], "helperAboveOnlyCaller">;
515
+ readonly "source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
510
516
  readonly "zod-naming-convention": DocumentedRule<readonly [{
511
517
  convention?: "prefix" | "suffix" | "either";
512
518
  }?], "zPrefix" | "schemaSuffix" | "zodSchemaName">;
@@ -518,4 +524,4 @@ declare const plugin: {
518
524
  };
519
525
  };
520
526
 
521
- export { type RetiredRule, applicationOnlyRules, plugin as default, publicDocumentation, recommendedRules, renamedRules, retiredRules, rules, strictRules };
527
+ export { type RetiredRule, advisoryRules, applicationOnlyRules, plugin as default, publicDocumentation, recommendedRules, renamedRules, retiredRules, rules, strictRules };
package/dist/index.js CHANGED
@@ -431,7 +431,7 @@ var duplicateTestBodyDocumentation = {
431
431
  outcome: "no-match",
432
432
  files: [{
433
433
  path: "src/user.test.ts",
434
- source: "test.each(['a', 'b'])('parses %s', (value) => { const x = parse(value); expect(x.ok).toBe(true); expect(x.value).toBe(value); });"
434
+ source: "test.each(['a', 'b'])('accepts %s', (value) => { const result = parse(value); expect(result.ok).toBe(true); expect(result.value).toBe(value); });"
435
435
  }],
436
436
  focusPath: "src/user.test.ts",
437
437
  expectedCount: 0,
@@ -443,7 +443,7 @@ var duplicateTestBodyDocumentation = {
443
443
  outcome: "match",
444
444
  files: [{
445
445
  path: "src/user.test.ts",
446
- source: "test('accepts a', () => { const result = parse('a'); expect(result.ok).toBe(true); expect(result.value).toBeDefined(); });\ntest('accepts b', () => { const result = parse('b'); expect(result.ok).toBe(true); expect(result.value).toBeDefined(); });"
446
+ source: "test('accepts a', () => { const value = 'a'; const result = parse(value); expect(result.ok).toBe(true); expect(result.value).toBe(value); });\ntest('accepts b', () => { const value = 'b'; const result = parse(value); expect(result.ok).toBe(true); expect(result.value).toBe(value); });"
447
447
  }],
448
448
  focusPath: "src/user.test.ts",
449
449
  expectedCount: 1,
@@ -14762,9 +14762,291 @@ var stepdown_default = createRule({
14762
14762
  }
14763
14763
  });
14764
14764
 
14765
+ // src/rules/source-coupled-test.ts
14766
+ import { AST_NODE_TYPES as AST_NODE_TYPES56 } from "@typescript-eslint/utils";
14767
+ var SOURCE_SUFFIX_RE = /\.(?:bash|hcl|sh|tf|tfvars|ya?ml|py|[cm]?[jt]s)$/iu;
14768
+ var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
14769
+ var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
14770
+ var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
14771
+ "slice",
14772
+ "substring",
14773
+ "substr",
14774
+ "toLowerCase",
14775
+ "toString",
14776
+ "toUpperCase",
14777
+ "trim",
14778
+ "trimEnd",
14779
+ "trimStart",
14780
+ "replace",
14781
+ "replaceAll"
14782
+ ]);
14783
+ var TEXT_PREDICATES = /* @__PURE__ */ new Set(["endsWith", "includes", "indexOf", "lastIndexOf", "match", "matchAll", "search", "startsWith"]);
14784
+ var REGEXP_PREDICATES = /* @__PURE__ */ new Set(["exec", "test"]);
14785
+ var EXPECT_MATCHERS = /* @__PURE__ */ new Set([
14786
+ "toBe",
14787
+ "toBeFalsy",
14788
+ "toBeGreaterThan",
14789
+ "toBeGreaterThanOrEqual",
14790
+ "toBeLessThan",
14791
+ "toBeLessThanOrEqual",
14792
+ "toBeNull",
14793
+ "toBeTruthy",
14794
+ "toContain",
14795
+ "toEqual",
14796
+ "toHaveLength",
14797
+ "toMatch",
14798
+ "toMatchSnapshot",
14799
+ "toStrictEqual"
14800
+ ]);
14801
+ var EXPECT_MODIFIERS = /* @__PURE__ */ new Set(["not", "rejects", "resolves"]);
14802
+ var ASSERT_MATCHERS = /* @__PURE__ */ new Set(["deepEqual", "doesNotMatch", "equal", "match", "notDeepEqual", "notEqual", "notStrictEqual", "ok", "strictEqual"]);
14803
+ var sourceCoupledTestDocumentation = {
14804
+ summary: "Disallow raw repository source text as a test oracle; parse or execute the artifact instead.",
14805
+ rationale: "Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
14806
+ remediation: "Parse the artifact, execute its validator, or assert on Terraform plan JSON or another runtime contract.",
14807
+ category: "testing",
14808
+ limitations: [
14809
+ "The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
14810
+ "When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."
14811
+ ],
14812
+ examples: [
14813
+ {
14814
+ id: "parsed-policy-contract",
14815
+ title: "Assert on parsed policy behavior",
14816
+ outcome: "no-match",
14817
+ 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([]); });" }],
14818
+ focusPath: "src/policy.test.ts",
14819
+ expectedCount: 0,
14820
+ public: true
14821
+ },
14822
+ {
14823
+ id: "terraform-substring-contract",
14824
+ title: "Do not prove Terraform behavior with a regex",
14825
+ outcome: "match",
14826
+ 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/); });" }],
14827
+ focusPath: "src/policy.test.ts",
14828
+ expectedCount: 1,
14829
+ public: true
14830
+ }
14831
+ ]
14832
+ };
14833
+ function staticMemberName5(node) {
14834
+ if (!node.computed && node.property.type === AST_NODE_TYPES56.Identifier) return node.property.name;
14835
+ if (node.computed && node.property.type === AST_NODE_TYPES56.Literal && typeof node.property.value === "string") return node.property.value;
14836
+ return null;
14837
+ }
14838
+ function unwrap5(node) {
14839
+ if (node.type === AST_NODE_TYPES56.AwaitExpression) return unwrap5(node.argument);
14840
+ if (node.type === AST_NODE_TYPES56.ChainExpression) return unwrap5(node.expression);
14841
+ if (node.type === AST_NODE_TYPES56.TSAsExpression || node.type === AST_NODE_TYPES56.TSNonNullExpression || node.type === AST_NODE_TYPES56.TSTypeAssertion) return unwrap5(node.expression);
14842
+ return node;
14843
+ }
14844
+ function stringValue(node) {
14845
+ const current = unwrap5(node);
14846
+ if (current.type === AST_NODE_TYPES56.Literal && typeof current.value === "string") return current.value;
14847
+ if (current.type === AST_NODE_TYPES56.TemplateLiteral && current.expressions.length === 0) return current.quasis[0]?.value.cooked ?? null;
14848
+ return null;
14849
+ }
14850
+ function importSource(node) {
14851
+ return typeof node.source.value === "string" ? node.source.value : null;
14852
+ }
14853
+ function requireSource(node) {
14854
+ const current = unwrap5(node);
14855
+ 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;
14856
+ return stringValue(current.arguments[0]);
14857
+ }
14858
+ function newScope() {
14859
+ 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() };
14860
+ }
14861
+ var source_coupled_test_default = createRule({
14862
+ name: "source-coupled-test",
14863
+ documentation: sourceCoupledTestDocumentation,
14864
+ meta: {
14865
+ type: "suggestion",
14866
+ docs: { description: sourceCoupledTestDocumentation.summary },
14867
+ schema: [],
14868
+ messages: { rawSourceOracle: "Raw repository source text is the oracle. Parse or execute the artifact so comments, formatting, and unreachable blocks cannot satisfy the contract." }
14869
+ },
14870
+ defaultOptions: [],
14871
+ create(context) {
14872
+ if (!isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
14873
+ const scopes = [newScope()];
14874
+ const reportedOrigins = /* @__PURE__ */ new Set();
14875
+ const currentScope = () => scopes.at(-1) ?? scopes[0];
14876
+ const visible = (kind, name) => {
14877
+ for (let index = scopes.length - 1; index >= 0; index--) {
14878
+ const scope = scopes[index];
14879
+ if (scope.declared.has(name)) return scope[kind].has(name);
14880
+ }
14881
+ return false;
14882
+ };
14883
+ const visibleRawOrigins = (name) => {
14884
+ for (let index = scopes.length - 1; index >= 0; index--) {
14885
+ const scope = scopes[index];
14886
+ if (scope.declared.has(name)) return scope.rawOrigins.get(name) ?? /* @__PURE__ */ new Set();
14887
+ }
14888
+ return /* @__PURE__ */ new Set();
14889
+ };
14890
+ const sourcePath = (node) => {
14891
+ const current = unwrap5(node);
14892
+ const value = stringValue(current);
14893
+ if (value !== null) return SOURCE_SUFFIX_RE.test(value);
14894
+ if (current.type === AST_NODE_TYPES56.Identifier) return visible("paths", current.name);
14895
+ if (current.type === AST_NODE_TYPES56.BinaryExpression && current.operator === "+") {
14896
+ return sourcePath(current.left) || sourcePath(current.right);
14897
+ }
14898
+ if (current.type === AST_NODE_TYPES56.TemplateLiteral) return current.expressions.some(sourcePath);
14899
+ if (current.type === AST_NODE_TYPES56.CallExpression || current.type === AST_NODE_TYPES56.NewExpression) {
14900
+ return current.arguments.some((argument) => argument.type !== AST_NODE_TYPES56.SpreadElement && sourcePath(argument));
14901
+ }
14902
+ if (current.type === AST_NODE_TYPES56.MemberExpression) return sourcePath(current.object);
14903
+ return false;
14904
+ };
14905
+ const rawRead = (node) => {
14906
+ const current = unwrap5(node);
14907
+ if (current.type !== AST_NODE_TYPES56.CallExpression || current.arguments.length === 0) return false;
14908
+ const callee = unwrap5(current.callee);
14909
+ if (callee.type === AST_NODE_TYPES56.Identifier) {
14910
+ return visible("fsReaders", callee.name) && sourcePath(current.arguments[0]);
14911
+ }
14912
+ if (callee.type !== AST_NODE_TYPES56.MemberExpression) return false;
14913
+ const name = staticMemberName5(callee);
14914
+ const object = unwrap5(callee.object);
14915
+ return name !== null && FS_READERS.has(name) && object.type === AST_NODE_TYPES56.Identifier && visible("fsObjects", object.name) && sourcePath(current.arguments[0]);
14916
+ };
14917
+ const rawOrigins = (node) => {
14918
+ const current = unwrap5(node);
14919
+ if (current.type === AST_NODE_TYPES56.Identifier) return visibleRawOrigins(current.name);
14920
+ if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
14921
+ if (current.type === AST_NODE_TYPES56.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
14922
+ if (current.type !== AST_NODE_TYPES56.CallExpression) return /* @__PURE__ */ new Set();
14923
+ const callee = unwrap5(current.callee);
14924
+ if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
14925
+ const name = staticMemberName5(callee);
14926
+ return name !== null && TEXT_TRANSFORMS.has(name) ? rawOrigins(callee.object) : /* @__PURE__ */ new Set();
14927
+ };
14928
+ const evidenceOrigins = (node) => {
14929
+ const current = unwrap5(node);
14930
+ const direct = rawOrigins(current);
14931
+ if (direct.size > 0) return direct;
14932
+ if (current.type === AST_NODE_TYPES56.BinaryExpression || current.type === AST_NODE_TYPES56.LogicalExpression) return /* @__PURE__ */ new Set([...evidenceOrigins(current.left), ...evidenceOrigins(current.right)]);
14933
+ if (current.type === AST_NODE_TYPES56.UnaryExpression) return evidenceOrigins(current.argument);
14934
+ if (current.type !== AST_NODE_TYPES56.CallExpression) return /* @__PURE__ */ new Set();
14935
+ const callee = unwrap5(current.callee);
14936
+ if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
14937
+ const name = staticMemberName5(callee);
14938
+ if (name !== null && TEXT_PREDICATES.has(name)) return rawOrigins(callee.object);
14939
+ if (name !== null && REGEXP_PREDICATES.has(name)) return new Set(current.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...rawOrigins(argument)]));
14940
+ return /* @__PURE__ */ new Set();
14941
+ };
14942
+ const rawAssertionOrigins = (node) => {
14943
+ const callee = unwrap5(node.callee);
14944
+ if (callee.type === AST_NODE_TYPES56.Identifier && callee.name === "assert") {
14945
+ return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
14946
+ }
14947
+ if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
14948
+ const matcher = staticMemberName5(callee);
14949
+ if (matcher === null) return /* @__PURE__ */ new Set();
14950
+ let receiver = unwrap5(callee.object);
14951
+ while (receiver.type === AST_NODE_TYPES56.MemberExpression && EXPECT_MODIFIERS.has(staticMemberName5(receiver) ?? "")) receiver = unwrap5(receiver.object);
14952
+ if (receiver.type === AST_NODE_TYPES56.CallExpression && receiver.callee.type === AST_NODE_TYPES56.Identifier && receiver.callee.name === "expect") {
14953
+ if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
14954
+ return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
14955
+ }
14956
+ if (receiver.type !== AST_NODE_TYPES56.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
14957
+ return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
14958
+ };
14959
+ const declare = (name, state) => {
14960
+ const scope = currentScope();
14961
+ scope.declared.add(name);
14962
+ scope.collections.delete(name);
14963
+ scope.fsObjects.delete(name);
14964
+ scope.fsReaders.delete(name);
14965
+ scope.paths.delete(name);
14966
+ scope.rawOrigins.delete(name);
14967
+ if (state.collection === true) scope.collections.add(name);
14968
+ if (state.fsObject === true) scope.fsObjects.add(name);
14969
+ if (state.fsReader === true) scope.fsReaders.add(name);
14970
+ if (state.path === true) scope.paths.add(name);
14971
+ if (state.rawOrigins !== void 0 && state.rawOrigins.size > 0) {
14972
+ scope.rawOrigins.set(name, state.rawOrigins);
14973
+ }
14974
+ };
14975
+ const sourceCollection = (node) => {
14976
+ const current = unwrap5(node);
14977
+ 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));
14978
+ };
14979
+ const declaredNames2 = (node) => {
14980
+ const current = unwrap5(node);
14981
+ if (current.type === AST_NODE_TYPES56.Identifier) return [current.name];
14982
+ if (current.type === AST_NODE_TYPES56.AssignmentPattern) return declaredNames2(current.left);
14983
+ if (current.type === AST_NODE_TYPES56.RestElement) return declaredNames2(current.argument);
14984
+ if (current.type === AST_NODE_TYPES56.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
14985
+ if (current.type === AST_NODE_TYPES56.ObjectPattern) return current.properties.flatMap((property) => property.type === AST_NODE_TYPES56.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
14986
+ return [];
14987
+ };
14988
+ const enterFunction = (node) => {
14989
+ scopes.push(newScope());
14990
+ for (const parameter of node.params) for (const name of declaredNames2(parameter)) declare(name, {});
14991
+ };
14992
+ const exitFunction = () => {
14993
+ scopes.pop();
14994
+ };
14995
+ return {
14996
+ ImportDeclaration(node) {
14997
+ const source = importSource(node);
14998
+ if (source === null || !FS_MODULES.has(source)) return;
14999
+ for (const specifier of node.specifiers) {
15000
+ if (specifier.type === AST_NODE_TYPES56.ImportSpecifier) {
15001
+ const imported = specifier.imported.type === AST_NODE_TYPES56.Identifier ? specifier.imported.name : String(specifier.imported.value);
15002
+ if (FS_READERS.has(imported)) declare(specifier.local.name, { fsReader: true });
15003
+ } else {
15004
+ declare(specifier.local.name, { fsObject: true });
15005
+ }
15006
+ }
15007
+ },
15008
+ ":function": enterFunction,
15009
+ ":function:exit": exitFunction,
15010
+ VariableDeclarator(node) {
15011
+ if (node.init === null) return;
15012
+ const required = requireSource(node.init);
15013
+ if (required !== null && FS_MODULES.has(required) && node.id.type === AST_NODE_TYPES56.Identifier) {
15014
+ declare(node.id.name, { fsObject: true });
15015
+ return;
15016
+ }
15017
+ if (node.id.type === AST_NODE_TYPES56.ObjectPattern && required !== null && FS_MODULES.has(required)) {
15018
+ for (const property of node.id.properties) {
15019
+ if (property.type !== AST_NODE_TYPES56.Property || property.value.type !== AST_NODE_TYPES56.Identifier) continue;
15020
+ const key = property.key.type === AST_NODE_TYPES56.Identifier ? property.key.name : property.key.type === AST_NODE_TYPES56.Literal ? String(property.key.value) : "";
15021
+ if (FS_READERS.has(key)) declare(property.value.name, { fsReader: true });
15022
+ }
15023
+ return;
15024
+ }
15025
+ if (node.id.type !== AST_NODE_TYPES56.Identifier) return;
15026
+ declare(node.id.name, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
15027
+ },
15028
+ AssignmentExpression(node) {
15029
+ if (node.left.type === AST_NODE_TYPES56.Identifier) declare(node.left.name, { path: sourcePath(node.right), rawOrigins: rawOrigins(node.right) });
15030
+ },
15031
+ ForOfStatement(node) {
15032
+ const right = unwrap5(node.right);
15033
+ const collection = right.type === AST_NODE_TYPES56.Identifier && visible("collections", right.name);
15034
+ const left = node.left.type === AST_NODE_TYPES56.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
15035
+ if (collection && left?.type === AST_NODE_TYPES56.Identifier) declare(left.name, { path: true });
15036
+ },
15037
+ CallExpression(node) {
15038
+ const origins = rawAssertionOrigins(node);
15039
+ if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
15040
+ for (const origin of origins) reportedOrigins.add(origin);
15041
+ context.report({ node, messageId: "rawSourceOracle" });
15042
+ }
15043
+ };
15044
+ }
15045
+ });
15046
+
14765
15047
  // src/rules/zod-naming-convention.ts
14766
15048
  import {
14767
- AST_NODE_TYPES as AST_NODE_TYPES56,
15049
+ AST_NODE_TYPES as AST_NODE_TYPES57,
14768
15050
  ASTUtils as ASTUtils16
14769
15051
  } from "@typescript-eslint/utils";
14770
15052
  var zodNamingConventionDocumentation = {
@@ -14807,18 +15089,18 @@ var NON_SCHEMA_TERMINALS = /* @__PURE__ */ new Set([
14807
15089
  "prettifyError",
14808
15090
  "treeifyError"
14809
15091
  ]);
14810
- var terminalMethodName = (callee) => !callee.computed && callee.property.type === AST_NODE_TYPES56.Identifier ? callee.property.name : null;
15092
+ var terminalMethodName = (callee) => !callee.computed && callee.property.type === AST_NODE_TYPES57.Identifier ? callee.property.name : null;
14811
15093
  var calleeChainRoot = (node) => {
14812
15094
  let current = node;
14813
15095
  for (; ; ) {
14814
- if (current.type === AST_NODE_TYPES56.Identifier) {
15096
+ if (current.type === AST_NODE_TYPES57.Identifier) {
14815
15097
  return current;
14816
15098
  }
14817
- if (current.type === AST_NODE_TYPES56.MemberExpression) {
15099
+ if (current.type === AST_NODE_TYPES57.MemberExpression) {
14818
15100
  current = current.object;
14819
15101
  continue;
14820
15102
  }
14821
- if (current.type === AST_NODE_TYPES56.CallExpression) {
15103
+ if (current.type === AST_NODE_TYPES57.CallExpression) {
14822
15104
  current = current.callee;
14823
15105
  continue;
14824
15106
  }
@@ -14880,7 +15162,7 @@ var zod_naming_convention_default = createRule({
14880
15162
  ImportDeclaration(node) {
14881
15163
  if (!isZodModule(node.source.value)) return;
14882
15164
  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")) {
15165
+ 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
15166
  recordZodBinding(specifier.local);
14885
15167
  }
14886
15168
  }
@@ -14888,13 +15170,13 @@ var zod_naming_convention_default = createRule({
14888
15170
  VariableDeclarator(node) {
14889
15171
  const init = node.init;
14890
15172
  if (init === null || init === void 0) return;
14891
- if (init.type !== AST_NODE_TYPES56.CallExpression) return;
15173
+ if (init.type !== AST_NODE_TYPES57.CallExpression) return;
14892
15174
  const callee = init.callee;
14893
- if (callee.type !== AST_NODE_TYPES56.MemberExpression) return;
15175
+ if (callee.type !== AST_NODE_TYPES57.MemberExpression) return;
14894
15176
  if (!isZodChain(callee)) return;
14895
15177
  const terminal = terminalMethodName(callee);
14896
15178
  if (terminal !== null && NON_SCHEMA_TERMINALS.has(terminal)) return;
14897
- if (node.id.type !== AST_NODE_TYPES56.Identifier) return;
15179
+ if (node.id.type !== AST_NODE_TYPES57.Identifier) return;
14898
15180
  if (test.test(node.id.name)) return;
14899
15181
  if (acceptsSchemaWord && CONTAINS_SCHEMA_RE.test(node.id.name)) return;
14900
15182
  context.report({
@@ -15051,17 +15333,19 @@ var rules = {
15051
15333
  "require-zod-form-validation": require_zod_form_validation_default,
15052
15334
  "store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
15053
15335
  "stepdown": stepdown_default,
15336
+ "source-coupled-test": source_coupled_test_default,
15054
15337
  "zod-naming-convention": zod_naming_convention_default
15055
15338
  };
15056
15339
  var meta = {
15057
15340
  name: "@sarj/eslint-plugin",
15058
- version: "15.6.6"
15341
+ version: "15.6.8"
15059
15342
  };
15060
15343
  var applicationOnlyRules = [
15061
15344
  "no-restricted-library-load",
15062
15345
  "prefer-native-random-uuid",
15063
15346
  "prefer-shadcn-primitives"
15064
15347
  ];
15348
+ var advisoryRules = ["source-coupled-test"];
15065
15349
  var recommendedRules = {
15066
15350
  "@sarj/duplicate-test-body": "error",
15067
15351
  "@sarj/enforce-file-structure": "error",
@@ -15119,6 +15403,7 @@ var recommendedRules = {
15119
15403
  "@sarj/require-zod-form-validation": "error",
15120
15404
  "@sarj/store-insert-requires-on-conflict": "error",
15121
15405
  "@sarj/stepdown": "error",
15406
+ "@sarj/source-coupled-test": "warn",
15122
15407
  "@sarj/zod-naming-convention": "error"
15123
15408
  };
15124
15409
  var strictRules = {
@@ -15182,6 +15467,7 @@ var strictRules = {
15182
15467
  "@sarj/require-zod-form-validation": "error",
15183
15468
  "@sarj/store-insert-requires-on-conflict": "error",
15184
15469
  "@sarj/stepdown": "error",
15470
+ "@sarj/source-coupled-test": "warn",
15185
15471
  "@sarj/zod-naming-convention": "error"
15186
15472
  };
15187
15473
  var plugin = {
@@ -15205,6 +15491,7 @@ var plugin = {
15205
15491
  };
15206
15492
  var index_default = plugin;
15207
15493
  export {
15494
+ advisoryRules,
15208
15495
  applicationOnlyRules,
15209
15496
  index_default as default,
15210
15497
  publicDocumentation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "15.6.6",
3
+ "version": "15.6.8",
4
4
  "packageManager": "npm@12.0.2",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",