@sarj/eslint-plugin 15.6.9 → 15.7.0

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
@@ -14868,7 +14868,7 @@ var stepdown_default = createRule({
14868
14868
 
14869
14869
  // src/rules/source-coupled-test.ts
14870
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;
14871
+ var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|py|[cm]?[jt]s)$/iu;
14872
14872
  var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
14873
14873
  var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
14874
14874
  var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
@@ -14882,7 +14882,8 @@ var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
14882
14882
  "trimEnd",
14883
14883
  "trimStart",
14884
14884
  "replace",
14885
- "replaceAll"
14885
+ "replaceAll",
14886
+ "split"
14886
14887
  ]);
14887
14888
  var TEXT_PREDICATES = /* @__PURE__ */ new Set(["endsWith", "includes", "indexOf", "lastIndexOf", "match", "matchAll", "search", "startsWith"]);
14888
14889
  var REGEXP_PREDICATES = /* @__PURE__ */ new Set(["exec", "test"]);
@@ -14907,7 +14908,7 @@ var ASSERT_MATCHERS = /* @__PURE__ */ new Set(["deepEqual", "doesNotMatch", "equ
14907
14908
  var sourceCoupledTestDocumentation = {
14908
14909
  summary: "Disallow raw repository source text as a test oracle; parse or execute the artifact instead.",
14909
14910
  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
+ remediation: "Parse the artifact, execute its validator, or assert on another runtime contract.",
14911
14912
  category: "testing",
14912
14913
  limitations: [
14913
14914
  "The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
@@ -14924,10 +14925,10 @@ var sourceCoupledTestDocumentation = {
14924
14925
  public: true
14925
14926
  },
14926
14927
  {
14927
- id: "terraform-substring-contract",
14928
- title: "Do not prove Terraform behavior with a regex",
14928
+ id: "workflow-substring-contract",
14929
+ title: "Do not prove workflow behavior with a regex",
14929
14930
  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
+ files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const source = readFileSync('workflow.yml', 'utf8'); expect(source).toMatch(/permissions/); });" }],
14931
14932
  focusPath: "src/policy.test.ts",
14932
14933
  expectedCount: 1,
14933
14934
  public: true
@@ -14962,191 +14963,237 @@ function requireSource(node) {
14962
14963
  function newScope() {
14963
14964
  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
  }
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
- }
14966
+ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
14967
+ return createRule({
14968
+ name,
14969
+ documentation,
14970
+ meta: {
14971
+ type: "suggestion",
14972
+ docs: { description: documentation.summary },
14973
+ schema: [],
14974
+ messages: { rawSourceOracle: "Raw repository source text is the oracle. Parse or execute the artifact so comments, formatting, and unreachable blocks cannot satisfy the contract." }
14975
+ },
14976
+ defaultOptions: [],
14977
+ create(context) {
14978
+ if (!isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
14979
+ const scopes = [newScope()];
14980
+ const reportedOrigins = /* @__PURE__ */ new Set();
14981
+ const currentScope = () => scopes.at(-1) ?? scopes[0];
14982
+ const visible = (kind, name2) => {
14983
+ for (let index = scopes.length - 1; index >= 0; index--) {
14984
+ const scope = scopes[index];
14985
+ if (scope.declared.has(name2)) return scope[kind].has(name2);
15110
14986
  }
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;
14987
+ return false;
14988
+ };
14989
+ const visibleRawOrigins = (name2) => {
14990
+ for (let index = scopes.length - 1; index >= 0; index--) {
14991
+ const scope = scopes[index];
14992
+ if (scope.declared.has(name2)) return scope.rawOrigins.get(name2) ?? /* @__PURE__ */ new Set();
15120
14993
  }
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 });
14994
+ return /* @__PURE__ */ new Set();
14995
+ };
14996
+ const sourcePath = (node) => {
14997
+ const current = unwrap5(node);
14998
+ const value = stringValue(current);
14999
+ if (value !== null) return sourceSuffixRe.test(value);
15000
+ if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return visible("paths", current.name);
15001
+ if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression && current.operator === "+") {
15002
+ return sourcePath(current.left) || sourcePath(current.right);
15003
+ }
15004
+ if (current.type === import_utils70.AST_NODE_TYPES.TemplateLiteral) return current.expressions.some(sourcePath);
15005
+ if (current.type === import_utils70.AST_NODE_TYPES.CallExpression || current.type === import_utils70.AST_NODE_TYPES.NewExpression) {
15006
+ return current.arguments.some((argument) => argument.type !== import_utils70.AST_NODE_TYPES.SpreadElement && sourcePath(argument));
15007
+ }
15008
+ if (current.type === import_utils70.AST_NODE_TYPES.MemberExpression) return sourcePath(current.object);
15009
+ return false;
15010
+ };
15011
+ const rawRead = (node) => {
15012
+ const current = unwrap5(node);
15013
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression || current.arguments.length === 0) return false;
15014
+ const callee = unwrap5(current.callee);
15015
+ if (callee.type === import_utils70.AST_NODE_TYPES.Identifier) {
15016
+ return visible("fsReaders", callee.name) && sourcePath(current.arguments[0]);
15017
+ }
15018
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return false;
15019
+ const name2 = staticMemberName5(callee);
15020
+ const object = unwrap5(callee.object);
15021
+ return name2 !== null && FS_READERS.has(name2) && object.type === import_utils70.AST_NODE_TYPES.Identifier && visible("fsObjects", object.name) && sourcePath(current.arguments[0]);
15022
+ };
15023
+ const rawOrigins = (node) => {
15024
+ const current = unwrap5(node);
15025
+ if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return visibleRawOrigins(current.name);
15026
+ if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
15027
+ if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
15028
+ if (current.type === import_utils70.AST_NODE_TYPES.MemberExpression && staticMemberName5(current) === "length") return rawOrigins(current.object);
15029
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression) return /* @__PURE__ */ new Set();
15030
+ const callee = unwrap5(current.callee);
15031
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
15032
+ const name2 = staticMemberName5(callee);
15033
+ return name2 !== null && TEXT_TRANSFORMS.has(name2) ? rawOrigins(callee.object) : /* @__PURE__ */ new Set();
15034
+ };
15035
+ const evidenceOrigins = (node) => {
15036
+ const current = unwrap5(node);
15037
+ const direct = rawOrigins(current);
15038
+ if (direct.size > 0) return direct;
15039
+ 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)]);
15040
+ if (current.type === import_utils70.AST_NODE_TYPES.UnaryExpression) return evidenceOrigins(current.argument);
15041
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression) return /* @__PURE__ */ new Set();
15042
+ const callee = unwrap5(current.callee);
15043
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
15044
+ const name2 = staticMemberName5(callee);
15045
+ if (name2 !== null && TEXT_PREDICATES.has(name2)) return rawOrigins(callee.object);
15046
+ if (name2 !== null && REGEXP_PREDICATES.has(name2)) return new Set(current.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...rawOrigins(argument)]));
15047
+ return /* @__PURE__ */ new Set();
15048
+ };
15049
+ const rawAssertionOrigins = (node) => {
15050
+ const callee = unwrap5(node.callee);
15051
+ if (callee.type === import_utils70.AST_NODE_TYPES.Identifier && callee.name === "assert") {
15052
+ return new Set(node.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15053
+ }
15054
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
15055
+ const matcher = staticMemberName5(callee);
15056
+ if (matcher === null) return /* @__PURE__ */ new Set();
15057
+ let receiver = unwrap5(callee.object);
15058
+ while (receiver.type === import_utils70.AST_NODE_TYPES.MemberExpression && EXPECT_MODIFIERS.has(staticMemberName5(receiver) ?? "")) receiver = unwrap5(receiver.object);
15059
+ if (receiver.type === import_utils70.AST_NODE_TYPES.CallExpression && receiver.callee.type === import_utils70.AST_NODE_TYPES.Identifier && receiver.callee.name === "expect") {
15060
+ if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
15061
+ return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15062
+ }
15063
+ if (receiver.type !== import_utils70.AST_NODE_TYPES.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
15064
+ return new Set(node.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15065
+ };
15066
+ const declare = (name2, state) => {
15067
+ const scope = currentScope();
15068
+ scope.declared.add(name2);
15069
+ scope.collections.delete(name2);
15070
+ scope.fsObjects.delete(name2);
15071
+ scope.fsReaders.delete(name2);
15072
+ scope.paths.delete(name2);
15073
+ scope.rawOrigins.delete(name2);
15074
+ if (state.collection === true) scope.collections.add(name2);
15075
+ if (state.fsObject === true) scope.fsObjects.add(name2);
15076
+ if (state.fsReader === true) scope.fsReaders.add(name2);
15077
+ if (state.path === true) scope.paths.add(name2);
15078
+ if (state.rawOrigins !== void 0 && state.rawOrigins.size > 0) {
15079
+ scope.rawOrigins.set(name2, state.rawOrigins);
15080
+ }
15081
+ };
15082
+ const sourceCollection = (node) => {
15083
+ const current = unwrap5(node);
15084
+ 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));
15085
+ };
15086
+ const declaredNames2 = (node) => {
15087
+ const current = unwrap5(node);
15088
+ if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return [current.name];
15089
+ if (current.type === import_utils70.AST_NODE_TYPES.AssignmentPattern) return declaredNames2(current.left);
15090
+ if (current.type === import_utils70.AST_NODE_TYPES.RestElement) return declaredNames2(current.argument);
15091
+ if (current.type === import_utils70.AST_NODE_TYPES.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
15092
+ 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));
15093
+ return [];
15094
+ };
15095
+ const enterFunction = (node) => {
15096
+ scopes.push(newScope());
15097
+ for (const parameter of node.params) for (const name2 of declaredNames2(parameter)) declare(name2, {});
15098
+ };
15099
+ const exitFunction = () => {
15100
+ scopes.pop();
15101
+ };
15102
+ return {
15103
+ ImportDeclaration(node) {
15104
+ const source = importSource(node);
15105
+ if (source === null || !FS_MODULES.has(source)) return;
15106
+ for (const specifier of node.specifiers) {
15107
+ if (specifier.type === import_utils70.AST_NODE_TYPES.ImportSpecifier) {
15108
+ const imported = specifier.imported.type === import_utils70.AST_NODE_TYPES.Identifier ? specifier.imported.name : String(specifier.imported.value);
15109
+ if (FS_READERS.has(imported)) declare(specifier.local.name, { fsReader: true });
15110
+ } else {
15111
+ declare(specifier.local.name, { fsObject: true });
15112
+ }
15126
15113
  }
15127
- return;
15114
+ },
15115
+ ":function": enterFunction,
15116
+ ":function:exit": exitFunction,
15117
+ VariableDeclarator(node) {
15118
+ if (node.init === null) return;
15119
+ const required = requireSource(node.init);
15120
+ if (required !== null && FS_MODULES.has(required) && node.id.type === import_utils70.AST_NODE_TYPES.Identifier) {
15121
+ declare(node.id.name, { fsObject: true });
15122
+ return;
15123
+ }
15124
+ if (node.id.type === import_utils70.AST_NODE_TYPES.ObjectPattern && required !== null && FS_MODULES.has(required)) {
15125
+ for (const property of node.id.properties) {
15126
+ if (property.type !== import_utils70.AST_NODE_TYPES.Property || property.value.type !== import_utils70.AST_NODE_TYPES.Identifier) continue;
15127
+ 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) : "";
15128
+ if (FS_READERS.has(key)) declare(property.value.name, { fsReader: true });
15129
+ }
15130
+ return;
15131
+ }
15132
+ if (node.id.type !== import_utils70.AST_NODE_TYPES.Identifier) return;
15133
+ declare(node.id.name, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
15134
+ },
15135
+ AssignmentExpression(node) {
15136
+ if (node.left.type === import_utils70.AST_NODE_TYPES.Identifier) declare(node.left.name, { path: sourcePath(node.right), rawOrigins: rawOrigins(node.right) });
15137
+ },
15138
+ ForOfStatement(node) {
15139
+ const right = unwrap5(node.right);
15140
+ const collection = right.type === import_utils70.AST_NODE_TYPES.Identifier && visible("collections", right.name);
15141
+ const left = node.left.type === import_utils70.AST_NODE_TYPES.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
15142
+ if (collection && left?.type === import_utils70.AST_NODE_TYPES.Identifier) declare(left.name, { path: true });
15143
+ },
15144
+ CallExpression(node) {
15145
+ const origins = rawAssertionOrigins(node);
15146
+ if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
15147
+ for (const origin of origins) reportedOrigins.add(origin);
15148
+ context.report({ node, messageId: "rawSourceOracle" });
15128
15149
  }
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
+ }
15152
+ });
15153
+ }
15154
+ var source_coupled_test_default = createSourceCoupledRule(
15155
+ "source-coupled-test",
15156
+ sourceCoupledTestDocumentation,
15157
+ GENERAL_SOURCE_SUFFIX_RE
15158
+ );
15159
+
15160
+ // src/rules/iac-source-coupled-test.ts
15161
+ var IAC_SOURCE_SUFFIX_RE = /(?:\.tf\.json|\.tftest\.(?:hcl|json)|\.(?:hcl|tf|tfvars))$/iu;
15162
+ var iacSourceCoupledTestDocumentation = {
15163
+ summary: "Disallow raw IaC source text as a test oracle; inspect a rendered plan, provider state, or runtime behavior.",
15164
+ rationale: "Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration while clients fail silently.",
15165
+ remediation: "Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
15166
+ category: "testing",
15167
+ limitations: [
15168
+ "The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
15169
+ "The warning-stage rule remains suppressible for calibration; promotion may make the locked policy non-suppressible."
15170
+ ],
15171
+ examples: [
15172
+ {
15173
+ id: "rendered-plan-contract",
15174
+ title: "Assert on rendered plan behavior",
15175
+ outcome: "no-match",
15176
+ files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const plan = JSON.parse(readFileSync('plan.json', 'utf8')); expect(validate(plan)).toEqual([]); });" }],
15177
+ focusPath: "src/policy.test.ts",
15178
+ expectedCount: 0,
15179
+ public: true
15180
+ },
15181
+ {
15182
+ id: "terraform-substring-contract",
15183
+ title: "Do not prove Terraform behavior with a regex",
15184
+ outcome: "match",
15185
+ 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/); });" }],
15186
+ focusPath: "src/policy.test.ts",
15187
+ expectedCount: 1,
15188
+ public: true
15189
+ }
15190
+ ]
15191
+ };
15192
+ var iac_source_coupled_test_default = createSourceCoupledRule(
15193
+ "iac-source-coupled-test",
15194
+ iacSourceCoupledTestDocumentation,
15195
+ IAC_SOURCE_SUFFIX_RE
15196
+ );
15150
15197
 
15151
15198
  // src/rules/zod-naming-convention.ts
15152
15199
  var import_utils71 = require("@typescript-eslint/utils");
@@ -15371,6 +15418,7 @@ var retiredRules = {
15371
15418
 
15372
15419
  // src/index.ts
15373
15420
  var rules = {
15421
+ "iac-source-coupled-test": iac_source_coupled_test_default,
15374
15422
  "duplicate-test-body": duplicate_test_body_default,
15375
15423
  "enforce-file-structure": enforce_file_structure_default,
15376
15424
  "no-client-side-data-fetching": no_client_side_data_fetching_default,
@@ -15440,15 +15488,16 @@ var rules = {
15440
15488
  };
15441
15489
  var meta = {
15442
15490
  name: "@sarj/eslint-plugin",
15443
- version: "15.6.9"
15491
+ version: "15.7.0"
15444
15492
  };
15445
15493
  var applicationOnlyRules = [
15446
15494
  "no-restricted-library-load",
15447
15495
  "prefer-native-random-uuid",
15448
15496
  "prefer-shadcn-primitives"
15449
15497
  ];
15450
- var advisoryRules = ["no-vague-suppression-description", "source-coupled-test"];
15498
+ var advisoryRules = ["no-vague-suppression-description", "iac-source-coupled-test", "source-coupled-test"];
15451
15499
  var recommendedRules = {
15500
+ "@sarj/iac-source-coupled-test": "warn",
15452
15501
  "@sarj/duplicate-test-body": "error",
15453
15502
  "@sarj/enforce-file-structure": "error",
15454
15503
  "@sarj/no-client-side-data-fetching": "error",
@@ -15510,6 +15559,7 @@ var recommendedRules = {
15510
15559
  "@sarj/zod-naming-convention": "error"
15511
15560
  };
15512
15561
  var strictRules = {
15562
+ "@sarj/iac-source-coupled-test": "warn",
15513
15563
  "@sarj/duplicate-test-body": "error",
15514
15564
  "@sarj/enforce-file-structure": "error",
15515
15565
  "@sarj/no-client-side-data-fetching": "error",
package/dist/index.d.cts CHANGED
@@ -188,6 +188,7 @@ declare const renamedRules: {
188
188
  };
189
189
 
190
190
  declare const rules: {
191
+ readonly "iac-source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
191
192
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
192
193
  readonly "enforce-file-structure": DocumentedRule<readonly [], "importsFirst" | "useServerDirective">;
193
194
  readonly "no-client-side-data-fetching": DocumentedRule<readonly [], "noClientFetch">;
@@ -283,8 +284,9 @@ declare const rules: {
283
284
  /** Rules registered for application-profile configs but intentionally absent from general presets. */
284
285
  declare const applicationOnlyRules: readonly ["no-restricted-library-load", "prefer-native-random-uuid", "prefer-shadcn-primitives"];
285
286
  /** Calibrated rules that intentionally remain warnings until consumer-corpus precision is proven. */
286
- declare const advisoryRules: readonly ["no-vague-suppression-description", "source-coupled-test"];
287
+ declare const advisoryRules: readonly ["no-vague-suppression-description", "iac-source-coupled-test", "source-coupled-test"];
287
288
  declare const recommendedRules: {
289
+ readonly "@sarj/iac-source-coupled-test": "warn";
288
290
  readonly "@sarj/duplicate-test-body": "error";
289
291
  readonly "@sarj/enforce-file-structure": "error";
290
292
  readonly "@sarj/no-client-side-data-fetching": "error";
@@ -350,6 +352,7 @@ declare const recommendedRules: {
350
352
  readonly "@sarj/zod-naming-convention": "error";
351
353
  };
352
354
  declare const strictRules: {
355
+ readonly "@sarj/iac-source-coupled-test": "warn";
353
356
  readonly "@sarj/duplicate-test-body": "error";
354
357
  readonly "@sarj/enforce-file-structure": "error";
355
358
  readonly "@sarj/no-client-side-data-fetching": "error";
@@ -426,9 +429,10 @@ type FlatPreset = {
426
429
  declare const plugin: {
427
430
  readonly meta: {
428
431
  readonly name: "@sarj/eslint-plugin";
429
- readonly version: "15.6.9";
432
+ readonly version: "15.7.0";
430
433
  };
431
434
  readonly rules: {
435
+ readonly "iac-source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
432
436
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
433
437
  readonly "enforce-file-structure": DocumentedRule<readonly [], "importsFirst" | "useServerDirective">;
434
438
  readonly "no-client-side-data-fetching": DocumentedRule<readonly [], "noClientFetch">;
package/dist/index.d.ts CHANGED
@@ -188,6 +188,7 @@ declare const renamedRules: {
188
188
  };
189
189
 
190
190
  declare const rules: {
191
+ readonly "iac-source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
191
192
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
192
193
  readonly "enforce-file-structure": DocumentedRule<readonly [], "importsFirst" | "useServerDirective">;
193
194
  readonly "no-client-side-data-fetching": DocumentedRule<readonly [], "noClientFetch">;
@@ -283,8 +284,9 @@ declare const rules: {
283
284
  /** Rules registered for application-profile configs but intentionally absent from general presets. */
284
285
  declare const applicationOnlyRules: readonly ["no-restricted-library-load", "prefer-native-random-uuid", "prefer-shadcn-primitives"];
285
286
  /** Calibrated rules that intentionally remain warnings until consumer-corpus precision is proven. */
286
- declare const advisoryRules: readonly ["no-vague-suppression-description", "source-coupled-test"];
287
+ declare const advisoryRules: readonly ["no-vague-suppression-description", "iac-source-coupled-test", "source-coupled-test"];
287
288
  declare const recommendedRules: {
289
+ readonly "@sarj/iac-source-coupled-test": "warn";
288
290
  readonly "@sarj/duplicate-test-body": "error";
289
291
  readonly "@sarj/enforce-file-structure": "error";
290
292
  readonly "@sarj/no-client-side-data-fetching": "error";
@@ -350,6 +352,7 @@ declare const recommendedRules: {
350
352
  readonly "@sarj/zod-naming-convention": "error";
351
353
  };
352
354
  declare const strictRules: {
355
+ readonly "@sarj/iac-source-coupled-test": "warn";
353
356
  readonly "@sarj/duplicate-test-body": "error";
354
357
  readonly "@sarj/enforce-file-structure": "error";
355
358
  readonly "@sarj/no-client-side-data-fetching": "error";
@@ -426,9 +429,10 @@ type FlatPreset = {
426
429
  declare const plugin: {
427
430
  readonly meta: {
428
431
  readonly name: "@sarj/eslint-plugin";
429
- readonly version: "15.6.9";
432
+ readonly version: "15.7.0";
430
433
  };
431
434
  readonly rules: {
435
+ readonly "iac-source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
432
436
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
433
437
  readonly "enforce-file-structure": DocumentedRule<readonly [], "importsFirst" | "useServerDirective">;
434
438
  readonly "no-client-side-data-fetching": DocumentedRule<readonly [], "noClientFetch">;
package/dist/index.js CHANGED
@@ -14845,7 +14845,7 @@ var stepdown_default = createRule({
14845
14845
 
14846
14846
  // src/rules/source-coupled-test.ts
14847
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;
14848
+ var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|py|[cm]?[jt]s)$/iu;
14849
14849
  var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
14850
14850
  var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
14851
14851
  var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
@@ -14859,7 +14859,8 @@ var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
14859
14859
  "trimEnd",
14860
14860
  "trimStart",
14861
14861
  "replace",
14862
- "replaceAll"
14862
+ "replaceAll",
14863
+ "split"
14863
14864
  ]);
14864
14865
  var TEXT_PREDICATES = /* @__PURE__ */ new Set(["endsWith", "includes", "indexOf", "lastIndexOf", "match", "matchAll", "search", "startsWith"]);
14865
14866
  var REGEXP_PREDICATES = /* @__PURE__ */ new Set(["exec", "test"]);
@@ -14884,7 +14885,7 @@ var ASSERT_MATCHERS = /* @__PURE__ */ new Set(["deepEqual", "doesNotMatch", "equ
14884
14885
  var sourceCoupledTestDocumentation = {
14885
14886
  summary: "Disallow raw repository source text as a test oracle; parse or execute the artifact instead.",
14886
14887
  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
+ remediation: "Parse the artifact, execute its validator, or assert on another runtime contract.",
14888
14889
  category: "testing",
14889
14890
  limitations: [
14890
14891
  "The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
@@ -14901,10 +14902,10 @@ var sourceCoupledTestDocumentation = {
14901
14902
  public: true
14902
14903
  },
14903
14904
  {
14904
- id: "terraform-substring-contract",
14905
- title: "Do not prove Terraform behavior with a regex",
14905
+ id: "workflow-substring-contract",
14906
+ title: "Do not prove workflow behavior with a regex",
14906
14907
  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
+ files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const source = readFileSync('workflow.yml', 'utf8'); expect(source).toMatch(/permissions/); });" }],
14908
14909
  focusPath: "src/policy.test.ts",
14909
14910
  expectedCount: 1,
14910
14911
  public: true
@@ -14939,191 +14940,237 @@ function requireSource(node) {
14939
14940
  function newScope() {
14940
14941
  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
  }
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
- }
14943
+ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
14944
+ return createRule({
14945
+ name,
14946
+ documentation,
14947
+ meta: {
14948
+ type: "suggestion",
14949
+ docs: { description: documentation.summary },
14950
+ schema: [],
14951
+ messages: { rawSourceOracle: "Raw repository source text is the oracle. Parse or execute the artifact so comments, formatting, and unreachable blocks cannot satisfy the contract." }
14952
+ },
14953
+ defaultOptions: [],
14954
+ create(context) {
14955
+ if (!isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
14956
+ const scopes = [newScope()];
14957
+ const reportedOrigins = /* @__PURE__ */ new Set();
14958
+ const currentScope = () => scopes.at(-1) ?? scopes[0];
14959
+ const visible = (kind, name2) => {
14960
+ for (let index = scopes.length - 1; index >= 0; index--) {
14961
+ const scope = scopes[index];
14962
+ if (scope.declared.has(name2)) return scope[kind].has(name2);
15087
14963
  }
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;
14964
+ return false;
14965
+ };
14966
+ const visibleRawOrigins = (name2) => {
14967
+ for (let index = scopes.length - 1; index >= 0; index--) {
14968
+ const scope = scopes[index];
14969
+ if (scope.declared.has(name2)) return scope.rawOrigins.get(name2) ?? /* @__PURE__ */ new Set();
15097
14970
  }
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 });
14971
+ return /* @__PURE__ */ new Set();
14972
+ };
14973
+ const sourcePath = (node) => {
14974
+ const current = unwrap5(node);
14975
+ const value = stringValue(current);
14976
+ if (value !== null) return sourceSuffixRe.test(value);
14977
+ if (current.type === AST_NODE_TYPES56.Identifier) return visible("paths", current.name);
14978
+ if (current.type === AST_NODE_TYPES56.BinaryExpression && current.operator === "+") {
14979
+ return sourcePath(current.left) || sourcePath(current.right);
14980
+ }
14981
+ if (current.type === AST_NODE_TYPES56.TemplateLiteral) return current.expressions.some(sourcePath);
14982
+ if (current.type === AST_NODE_TYPES56.CallExpression || current.type === AST_NODE_TYPES56.NewExpression) {
14983
+ return current.arguments.some((argument) => argument.type !== AST_NODE_TYPES56.SpreadElement && sourcePath(argument));
14984
+ }
14985
+ if (current.type === AST_NODE_TYPES56.MemberExpression) return sourcePath(current.object);
14986
+ return false;
14987
+ };
14988
+ const rawRead = (node) => {
14989
+ const current = unwrap5(node);
14990
+ if (current.type !== AST_NODE_TYPES56.CallExpression || current.arguments.length === 0) return false;
14991
+ const callee = unwrap5(current.callee);
14992
+ if (callee.type === AST_NODE_TYPES56.Identifier) {
14993
+ return visible("fsReaders", callee.name) && sourcePath(current.arguments[0]);
14994
+ }
14995
+ if (callee.type !== AST_NODE_TYPES56.MemberExpression) return false;
14996
+ const name2 = staticMemberName5(callee);
14997
+ const object = unwrap5(callee.object);
14998
+ return name2 !== null && FS_READERS.has(name2) && object.type === AST_NODE_TYPES56.Identifier && visible("fsObjects", object.name) && sourcePath(current.arguments[0]);
14999
+ };
15000
+ const rawOrigins = (node) => {
15001
+ const current = unwrap5(node);
15002
+ if (current.type === AST_NODE_TYPES56.Identifier) return visibleRawOrigins(current.name);
15003
+ if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
15004
+ if (current.type === AST_NODE_TYPES56.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
15005
+ if (current.type === AST_NODE_TYPES56.MemberExpression && staticMemberName5(current) === "length") return rawOrigins(current.object);
15006
+ if (current.type !== AST_NODE_TYPES56.CallExpression) return /* @__PURE__ */ new Set();
15007
+ const callee = unwrap5(current.callee);
15008
+ if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
15009
+ const name2 = staticMemberName5(callee);
15010
+ return name2 !== null && TEXT_TRANSFORMS.has(name2) ? rawOrigins(callee.object) : /* @__PURE__ */ new Set();
15011
+ };
15012
+ const evidenceOrigins = (node) => {
15013
+ const current = unwrap5(node);
15014
+ const direct = rawOrigins(current);
15015
+ if (direct.size > 0) return direct;
15016
+ if (current.type === AST_NODE_TYPES56.BinaryExpression || current.type === AST_NODE_TYPES56.LogicalExpression) return /* @__PURE__ */ new Set([...evidenceOrigins(current.left), ...evidenceOrigins(current.right)]);
15017
+ if (current.type === AST_NODE_TYPES56.UnaryExpression) return evidenceOrigins(current.argument);
15018
+ if (current.type !== AST_NODE_TYPES56.CallExpression) return /* @__PURE__ */ new Set();
15019
+ const callee = unwrap5(current.callee);
15020
+ if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
15021
+ const name2 = staticMemberName5(callee);
15022
+ if (name2 !== null && TEXT_PREDICATES.has(name2)) return rawOrigins(callee.object);
15023
+ if (name2 !== null && REGEXP_PREDICATES.has(name2)) return new Set(current.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...rawOrigins(argument)]));
15024
+ return /* @__PURE__ */ new Set();
15025
+ };
15026
+ const rawAssertionOrigins = (node) => {
15027
+ const callee = unwrap5(node.callee);
15028
+ if (callee.type === AST_NODE_TYPES56.Identifier && callee.name === "assert") {
15029
+ return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15030
+ }
15031
+ if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
15032
+ const matcher = staticMemberName5(callee);
15033
+ if (matcher === null) return /* @__PURE__ */ new Set();
15034
+ let receiver = unwrap5(callee.object);
15035
+ while (receiver.type === AST_NODE_TYPES56.MemberExpression && EXPECT_MODIFIERS.has(staticMemberName5(receiver) ?? "")) receiver = unwrap5(receiver.object);
15036
+ if (receiver.type === AST_NODE_TYPES56.CallExpression && receiver.callee.type === AST_NODE_TYPES56.Identifier && receiver.callee.name === "expect") {
15037
+ if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
15038
+ return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15039
+ }
15040
+ if (receiver.type !== AST_NODE_TYPES56.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
15041
+ return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15042
+ };
15043
+ const declare = (name2, state) => {
15044
+ const scope = currentScope();
15045
+ scope.declared.add(name2);
15046
+ scope.collections.delete(name2);
15047
+ scope.fsObjects.delete(name2);
15048
+ scope.fsReaders.delete(name2);
15049
+ scope.paths.delete(name2);
15050
+ scope.rawOrigins.delete(name2);
15051
+ if (state.collection === true) scope.collections.add(name2);
15052
+ if (state.fsObject === true) scope.fsObjects.add(name2);
15053
+ if (state.fsReader === true) scope.fsReaders.add(name2);
15054
+ if (state.path === true) scope.paths.add(name2);
15055
+ if (state.rawOrigins !== void 0 && state.rawOrigins.size > 0) {
15056
+ scope.rawOrigins.set(name2, state.rawOrigins);
15057
+ }
15058
+ };
15059
+ const sourceCollection = (node) => {
15060
+ const current = unwrap5(node);
15061
+ 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));
15062
+ };
15063
+ const declaredNames2 = (node) => {
15064
+ const current = unwrap5(node);
15065
+ if (current.type === AST_NODE_TYPES56.Identifier) return [current.name];
15066
+ if (current.type === AST_NODE_TYPES56.AssignmentPattern) return declaredNames2(current.left);
15067
+ if (current.type === AST_NODE_TYPES56.RestElement) return declaredNames2(current.argument);
15068
+ if (current.type === AST_NODE_TYPES56.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
15069
+ if (current.type === AST_NODE_TYPES56.ObjectPattern) return current.properties.flatMap((property) => property.type === AST_NODE_TYPES56.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
15070
+ return [];
15071
+ };
15072
+ const enterFunction = (node) => {
15073
+ scopes.push(newScope());
15074
+ for (const parameter of node.params) for (const name2 of declaredNames2(parameter)) declare(name2, {});
15075
+ };
15076
+ const exitFunction = () => {
15077
+ scopes.pop();
15078
+ };
15079
+ return {
15080
+ ImportDeclaration(node) {
15081
+ const source = importSource(node);
15082
+ if (source === null || !FS_MODULES.has(source)) return;
15083
+ for (const specifier of node.specifiers) {
15084
+ if (specifier.type === AST_NODE_TYPES56.ImportSpecifier) {
15085
+ const imported = specifier.imported.type === AST_NODE_TYPES56.Identifier ? specifier.imported.name : String(specifier.imported.value);
15086
+ if (FS_READERS.has(imported)) declare(specifier.local.name, { fsReader: true });
15087
+ } else {
15088
+ declare(specifier.local.name, { fsObject: true });
15089
+ }
15103
15090
  }
15104
- return;
15091
+ },
15092
+ ":function": enterFunction,
15093
+ ":function:exit": exitFunction,
15094
+ VariableDeclarator(node) {
15095
+ if (node.init === null) return;
15096
+ const required = requireSource(node.init);
15097
+ if (required !== null && FS_MODULES.has(required) && node.id.type === AST_NODE_TYPES56.Identifier) {
15098
+ declare(node.id.name, { fsObject: true });
15099
+ return;
15100
+ }
15101
+ if (node.id.type === AST_NODE_TYPES56.ObjectPattern && required !== null && FS_MODULES.has(required)) {
15102
+ for (const property of node.id.properties) {
15103
+ if (property.type !== AST_NODE_TYPES56.Property || property.value.type !== AST_NODE_TYPES56.Identifier) continue;
15104
+ const key = property.key.type === AST_NODE_TYPES56.Identifier ? property.key.name : property.key.type === AST_NODE_TYPES56.Literal ? String(property.key.value) : "";
15105
+ if (FS_READERS.has(key)) declare(property.value.name, { fsReader: true });
15106
+ }
15107
+ return;
15108
+ }
15109
+ if (node.id.type !== AST_NODE_TYPES56.Identifier) return;
15110
+ declare(node.id.name, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
15111
+ },
15112
+ AssignmentExpression(node) {
15113
+ if (node.left.type === AST_NODE_TYPES56.Identifier) declare(node.left.name, { path: sourcePath(node.right), rawOrigins: rawOrigins(node.right) });
15114
+ },
15115
+ ForOfStatement(node) {
15116
+ const right = unwrap5(node.right);
15117
+ const collection = right.type === AST_NODE_TYPES56.Identifier && visible("collections", right.name);
15118
+ const left = node.left.type === AST_NODE_TYPES56.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
15119
+ if (collection && left?.type === AST_NODE_TYPES56.Identifier) declare(left.name, { path: true });
15120
+ },
15121
+ CallExpression(node) {
15122
+ const origins = rawAssertionOrigins(node);
15123
+ if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
15124
+ for (const origin of origins) reportedOrigins.add(origin);
15125
+ context.report({ node, messageId: "rawSourceOracle" });
15105
15126
  }
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
+ };
15128
+ }
15129
+ });
15130
+ }
15131
+ var source_coupled_test_default = createSourceCoupledRule(
15132
+ "source-coupled-test",
15133
+ sourceCoupledTestDocumentation,
15134
+ GENERAL_SOURCE_SUFFIX_RE
15135
+ );
15136
+
15137
+ // src/rules/iac-source-coupled-test.ts
15138
+ var IAC_SOURCE_SUFFIX_RE = /(?:\.tf\.json|\.tftest\.(?:hcl|json)|\.(?:hcl|tf|tfvars))$/iu;
15139
+ var iacSourceCoupledTestDocumentation = {
15140
+ summary: "Disallow raw IaC source text as a test oracle; inspect a rendered plan, provider state, or runtime behavior.",
15141
+ rationale: "Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration while clients fail silently.",
15142
+ remediation: "Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
15143
+ category: "testing",
15144
+ limitations: [
15145
+ "The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
15146
+ "The warning-stage rule remains suppressible for calibration; promotion may make the locked policy non-suppressible."
15147
+ ],
15148
+ examples: [
15149
+ {
15150
+ id: "rendered-plan-contract",
15151
+ title: "Assert on rendered plan behavior",
15152
+ outcome: "no-match",
15153
+ files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const plan = JSON.parse(readFileSync('plan.json', 'utf8')); expect(validate(plan)).toEqual([]); });" }],
15154
+ focusPath: "src/policy.test.ts",
15155
+ expectedCount: 0,
15156
+ public: true
15157
+ },
15158
+ {
15159
+ id: "terraform-substring-contract",
15160
+ title: "Do not prove Terraform behavior with a regex",
15161
+ outcome: "match",
15162
+ 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/); });" }],
15163
+ focusPath: "src/policy.test.ts",
15164
+ expectedCount: 1,
15165
+ public: true
15166
+ }
15167
+ ]
15168
+ };
15169
+ var iac_source_coupled_test_default = createSourceCoupledRule(
15170
+ "iac-source-coupled-test",
15171
+ iacSourceCoupledTestDocumentation,
15172
+ IAC_SOURCE_SUFFIX_RE
15173
+ );
15127
15174
 
15128
15175
  // src/rules/zod-naming-convention.ts
15129
15176
  import {
@@ -15351,6 +15398,7 @@ var retiredRules = {
15351
15398
 
15352
15399
  // src/index.ts
15353
15400
  var rules = {
15401
+ "iac-source-coupled-test": iac_source_coupled_test_default,
15354
15402
  "duplicate-test-body": duplicate_test_body_default,
15355
15403
  "enforce-file-structure": enforce_file_structure_default,
15356
15404
  "no-client-side-data-fetching": no_client_side_data_fetching_default,
@@ -15420,15 +15468,16 @@ var rules = {
15420
15468
  };
15421
15469
  var meta = {
15422
15470
  name: "@sarj/eslint-plugin",
15423
- version: "15.6.9"
15471
+ version: "15.7.0"
15424
15472
  };
15425
15473
  var applicationOnlyRules = [
15426
15474
  "no-restricted-library-load",
15427
15475
  "prefer-native-random-uuid",
15428
15476
  "prefer-shadcn-primitives"
15429
15477
  ];
15430
- var advisoryRules = ["no-vague-suppression-description", "source-coupled-test"];
15478
+ var advisoryRules = ["no-vague-suppression-description", "iac-source-coupled-test", "source-coupled-test"];
15431
15479
  var recommendedRules = {
15480
+ "@sarj/iac-source-coupled-test": "warn",
15432
15481
  "@sarj/duplicate-test-body": "error",
15433
15482
  "@sarj/enforce-file-structure": "error",
15434
15483
  "@sarj/no-client-side-data-fetching": "error",
@@ -15490,6 +15539,7 @@ var recommendedRules = {
15490
15539
  "@sarj/zod-naming-convention": "error"
15491
15540
  };
15492
15541
  var strictRules = {
15542
+ "@sarj/iac-source-coupled-test": "warn",
15493
15543
  "@sarj/duplicate-test-body": "error",
15494
15544
  "@sarj/enforce-file-structure": "error",
15495
15545
  "@sarj/no-client-side-data-fetching": "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "15.6.9",
3
+ "version": "15.7.0",
4
4
  "packageManager": "npm@12.0.2",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",