@sarj/eslint-plugin 16.22.0 → 16.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.cjs +152 -140
  2. package/dist/index.js +451 -439
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -866,7 +866,7 @@ function isSearchParamReceiver(call, services) {
866
866
  // package.json
867
867
  var package_default = {
868
868
  name: "@sarj/eslint-plugin",
869
- version: "16.22.0",
869
+ version: "16.22.2",
870
870
  packageManager: "npm@12.2.0",
871
871
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
872
872
  type: "module",
@@ -7294,10 +7294,10 @@ var import_utils44 = require("@typescript-eslint/utils");
7294
7294
  function stripSqlNoise(text) {
7295
7295
  return scanSqlNoise(text);
7296
7296
  }
7297
- function sqlSingleQuotedRanges(text) {
7298
- const ranges = [];
7299
- scanSqlNoise(text, (start, end) => ranges.push([start, end]));
7300
- return ranges;
7297
+ function sqlNoiseInfo(text) {
7298
+ const singleQuotedRanges = [];
7299
+ const masked = scanSqlNoise(text, (start, end) => singleQuotedRanges.push([start, end]));
7300
+ return { text: masked, singleQuotedRanges };
7301
7301
  }
7302
7302
  function scanSqlNoise(text, onSingleQuoted) {
7303
7303
  const out = text.split("");
@@ -7383,6 +7383,7 @@ function maskSqlRange(text, out, start, end) {
7383
7383
  }
7384
7384
  var SUBSTITUTION_MARKER = "?";
7385
7385
  function sqlTextOf(node) {
7386
+ node = unwrapExpression(node);
7386
7387
  switch (node.type) {
7387
7388
  case import_utils44.AST_NODE_TYPES.Literal:
7388
7389
  return typeof node.value === "string" ? node.value : null;
@@ -7410,7 +7411,7 @@ function fragmentArrayText(node) {
7410
7411
  if (separator === null) return null;
7411
7412
  const parts = [];
7412
7413
  for (const element of node.elements) {
7413
- if (element === null || join4 !== null && element.type === import_utils44.AST_NODE_TYPES.ArrayExpression) {
7414
+ if (element === null || join4 !== null && unwrapExpression(element).type === import_utils44.AST_NODE_TYPES.ArrayExpression) {
7414
7415
  return null;
7415
7416
  }
7416
7417
  const part = sqlTextOf(element);
@@ -7422,8 +7423,10 @@ function fragmentArrayText(node) {
7422
7423
  return parts.length > 0 ? parts.join(separator) : null;
7423
7424
  }
7424
7425
  function joinedFragmentCall(node) {
7425
- const parent = node.parent;
7426
- return parent?.type === import_utils44.AST_NODE_TYPES.MemberExpression && parent.object === node && !parent.computed && parent.property.type === import_utils44.AST_NODE_TYPES.Identifier && parent.property.name === "join" && parent.parent?.type === import_utils44.AST_NODE_TYPES.CallExpression && parent.parent.callee === parent ? parent.parent : null;
7426
+ const value = outerExpression(node);
7427
+ const parent = value.parent;
7428
+ const callee = parent?.type === import_utils44.AST_NODE_TYPES.MemberExpression ? outerExpression(parent) : null;
7429
+ return parent?.type === import_utils44.AST_NODE_TYPES.MemberExpression && parent.object === value && import_utils44.ASTUtils.getPropertyName(parent) === "join" && callee?.parent?.type === import_utils44.AST_NODE_TYPES.CallExpression && callee.parent.callee === callee ? callee.parent : null;
7427
7430
  }
7428
7431
  function markConsumed(node, consumed) {
7429
7432
  consumed.add(node);
@@ -7465,6 +7468,8 @@ function createSqlListener(handler) {
7465
7468
  markConsumed(node, consumed);
7466
7469
  const separator = join4.arguments[0];
7467
7470
  if (separator !== void 0 && staticString(separator) !== null) markConsumed(separator, consumed);
7471
+ const member = unwrapExpression(join4.callee);
7472
+ if (member.type === import_utils44.AST_NODE_TYPES.MemberExpression && member.computed) markConsumed(member.property, consumed);
7468
7473
  },
7469
7474
  TemplateLiteral: (node) => {
7470
7475
  visit(node);
@@ -7475,11 +7480,11 @@ function createSqlListener(handler) {
7475
7480
  };
7476
7481
  }
7477
7482
  function isDiscardedSqlText(node) {
7478
- let current = node;
7483
+ let current = outerExpression(node);
7479
7484
  let parent = current.parent;
7480
7485
  while (parent !== void 0 && parent !== null) {
7481
- if (unwrapExpression(parent) === current || parent.type === import_utils44.AST_NODE_TYPES.BinaryExpression && parent.operator === "+") {
7482
- current = parent;
7486
+ if (parent.type === import_utils44.AST_NODE_TYPES.BinaryExpression && parent.operator === "+") {
7487
+ current = outerExpression(parent);
7483
7488
  parent = current.parent;
7484
7489
  continue;
7485
7490
  }
@@ -7561,8 +7566,7 @@ function isStaticFragment(expression) {
7561
7566
  function runtimeInterpolations(template) {
7562
7567
  const parts = template.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw);
7563
7568
  const statement = parts.join(RUNTIME_MARKER);
7564
- const ranges = sqlSingleQuotedRanges(statement);
7565
- const visible = stripSqlNoise(statement);
7569
+ const { singleQuotedRanges: ranges, text: visible } = sqlNoiseInfo(statement);
7566
7570
  let offset = 0;
7567
7571
  return template.expressions.flatMap(
7568
7572
  (expression, index) => {
@@ -7583,16 +7587,13 @@ function runtimeConcatOperands(node) {
7583
7587
  return [];
7584
7588
  }
7585
7589
  const operands = concatOperands(node);
7586
- const hasStringLiteral = operands.some(
7587
- (operand) => operand.type === import_utils45.AST_NODE_TYPES.Literal && typeof operand.value === "string" || operand.type === import_utils45.AST_NODE_TYPES.TemplateLiteral && operand.expressions.length === 0
7588
- );
7590
+ const hasStringLiteral = operands.some((operand) => staticString(operand) !== null);
7589
7591
  if (!hasStringLiteral) {
7590
7592
  return [];
7591
7593
  }
7592
7594
  const parts = operands.map((operand) => staticString(operand) ?? RUNTIME_MARKER);
7593
7595
  const statement = parts.join("");
7594
- const ranges = sqlSingleQuotedRanges(statement);
7595
- const visible = stripSqlNoise(statement);
7596
+ const { singleQuotedRanges: ranges, text: visible } = sqlNoiseInfo(statement);
7596
7597
  let offset = 0;
7597
7598
  return operands.flatMap((operand, index) => {
7598
7599
  const inValue = ranges.some(([start, end]) => start < offset && offset < end);
@@ -7606,10 +7607,17 @@ function runtimeConcatOperands(node) {
7606
7607
  });
7607
7608
  }
7608
7609
  function concatOperands(node) {
7609
- if (node.type === import_utils45.AST_NODE_TYPES.BinaryExpression && node.operator === "+") {
7610
- return [...concatOperands(node.left), ...concatOperands(node.right)];
7610
+ const operands = [];
7611
+ const pending = [node];
7612
+ while (pending.length > 0) {
7613
+ const next = pending.pop();
7614
+ if (next === void 0) continue;
7615
+ const value = unwrapExpression(next);
7616
+ if (value.type === import_utils45.AST_NODE_TYPES.BinaryExpression && value.operator === "+") {
7617
+ pending.push(value.right, value.left);
7618
+ } else operands.push(value);
7611
7619
  }
7612
- return [node];
7620
+ return operands;
7613
7621
  }
7614
7622
  var SQL_STATEMENT_RE = /\b(?:select\s|insert\s+into\b|insert\s+or\b|update\s+\w|delete\s+from\b|replace\s+into\b|merge\s+into\b|upsert\s+into\b|create\s+(?:temp(?:orary)?\s+)?(?:table|index|view|trigger|schema|database)\b|alter\s+table\b|drop\s+(?:table|index|view|trigger)\b|truncate\s+table\b|pragma\s+\w|with\s+\w+\s+as\s*\(|from\s+\w+\s+where\b)/i;
7615
7623
  var RUNTIME_MARKER = " ? ";
@@ -7617,6 +7625,7 @@ function looksLikeSql(node) {
7617
7625
  return SQL_STATEMENT_RE.test(stripSqlNoise(staticStatementText(node)));
7618
7626
  }
7619
7627
  function staticStatementText(node) {
7628
+ node = unwrapExpression(node);
7620
7629
  if (node.type === import_utils45.AST_NODE_TYPES.TemplateLiteral) {
7621
7630
  return node.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join(RUNTIME_MARKER);
7622
7631
  }
@@ -8686,46 +8695,15 @@ var STRONG_SECURITY_WORDS = /* @__PURE__ */ new Set([
8686
8695
  "uuid",
8687
8696
  "verificationcode"
8688
8697
  ]);
8689
- var NON_SECURITY_ID_WORDS = /* @__PURE__ */ new Set([
8690
- "aria",
8691
- "cache",
8692
- "component",
8693
- "correlation",
8694
- "dev",
8695
- "dialog",
8696
- "dom",
8697
- "element",
8698
- "execution",
8699
- "field",
8700
- "form",
8701
- "hmr",
8702
- "input",
8703
- "marker",
8704
- "menu",
8705
- "mock",
8706
- "perf",
8707
- "req",
8708
- "request",
8709
- "select",
8710
- "tab",
8711
- "temp",
8712
- "test",
8713
- "tmp",
8714
- "trace"
8715
- ]);
8716
- function nameWords(name) {
8717
- return name.replaceAll(/([a-z0-9])([A-Z])/gu, "$1 $2").split(/[^A-Za-z0-9]+/u).filter(Boolean).map((word) => word.toLowerCase());
8718
- }
8719
8698
  function isStrongSecurityName(name) {
8720
8699
  const words2 = nameWords(name);
8721
8700
  return words2.some((word) => STRONG_SECURITY_WORDS.has(word)) || words2.some(
8722
8701
  (word, index) => word === "api" && words2[index + 1] === "key" || word === "auth" && words2[index + 1] === "id" || word === "verification" && words2[index + 1] === "code"
8723
8702
  );
8724
8703
  }
8725
- function isNonSecurityName(name) {
8726
- return nameWords(name).some((word) => NON_SECURITY_ID_WORDS.has(word));
8704
+ function nameWords(name) {
8705
+ return name.replaceAll(/([a-z0-9])([A-Z])/gu, "$1 $2").split(/[^A-Za-z0-9]+/u).filter(Boolean).map((word) => word.toLowerCase());
8727
8706
  }
8728
- var PATH_OR_DOM_MARKER = /[\\/#]|\.[A-Za-z0-9]/;
8729
8707
  function isMathRandomCall(node) {
8730
8708
  if (node.type !== "CallExpression") {
8731
8709
  return false;
@@ -8769,67 +8747,6 @@ function findEnclosingNames(node) {
8769
8747
  }
8770
8748
  return names;
8771
8749
  }
8772
- function isConcatenatedIntoPathOrDomId(node) {
8773
- const valueNode = climbValueChain(node);
8774
- let current = valueNode;
8775
- let parent = current.parent;
8776
- let top;
8777
- while (parent) {
8778
- if (parent.type === "BinaryExpression" && parent.operator === "+" && (parent.left === current || parent.right === current)) {
8779
- top = parent;
8780
- current = parent;
8781
- parent = current.parent;
8782
- continue;
8783
- }
8784
- if (parent.type === "TemplateLiteral") {
8785
- top = parent;
8786
- current = parent;
8787
- parent = current.parent;
8788
- continue;
8789
- }
8790
- break;
8791
- }
8792
- if (!top) {
8793
- return false;
8794
- }
8795
- const parts = [];
8796
- collectStaticStringParts(top, parts);
8797
- return parts.some((part) => PATH_OR_DOM_MARKER.test(part));
8798
- }
8799
- function climbValueChain(node) {
8800
- let current = node;
8801
- let parent = current.parent;
8802
- while (parent) {
8803
- if (parent.type === "MemberExpression" && parent.object === current && import_utils51.ASTUtils.getPropertyName(parent) !== null) {
8804
- current = parent;
8805
- parent = current.parent;
8806
- continue;
8807
- }
8808
- if (parent.type === "CallExpression" && parent.callee === current) {
8809
- current = parent;
8810
- parent = current.parent;
8811
- continue;
8812
- }
8813
- break;
8814
- }
8815
- return current;
8816
- }
8817
- function collectStaticStringParts(node, out) {
8818
- if (node.type === "Literal" && typeof node.value === "string") {
8819
- out.push(node.value);
8820
- return;
8821
- }
8822
- if (node.type === "TemplateLiteral") {
8823
- for (const quasi of node.quasis) {
8824
- out.push(quasi.value.cooked ?? quasi.value.raw);
8825
- }
8826
- return;
8827
- }
8828
- if (node.type === "BinaryExpression" && node.operator === "+") {
8829
- collectStaticStringParts(node.left, out);
8830
- collectStaticStringParts(node.right, out);
8831
- }
8832
- }
8833
8750
  var no_insecure_random_id_default = createRule({
8834
8751
  name: "no-insecure-random-id",
8835
8752
  documentation: NO_INSECURE_RANDOM_ID_DOCUMENTATION,
@@ -8857,13 +8774,6 @@ var no_insecure_random_id_default = createRule({
8857
8774
  const names = findEnclosingNames(node);
8858
8775
  if (names.some(isStrongSecurityName)) {
8859
8776
  context.report({ node, messageId: "insecureRandomId" });
8860
- return;
8861
- }
8862
- if (names.some(isNonSecurityName)) {
8863
- return;
8864
- }
8865
- if (isConcatenatedIntoPathOrDomId(node)) {
8866
- return;
8867
8777
  }
8868
8778
  }
8869
8779
  };
@@ -9795,15 +9705,24 @@ function typeMayContainObject(type, checker) {
9795
9705
  function syntaxMayContainObject(node) {
9796
9706
  const expression = unwrapComparisonExpression(node);
9797
9707
  if (expression.type === import_utils55.AST_NODE_TYPES.ObjectExpression) return true;
9708
+ if (expression.type === import_utils55.AST_NODE_TYPES.Literal) return "regex" in expression;
9709
+ if (expression.type === import_utils55.AST_NODE_TYPES.TemplateLiteral || expression.type === import_utils55.AST_NODE_TYPES.UnaryExpression) return false;
9798
9710
  if (expression.type !== import_utils55.AST_NODE_TYPES.ArrayExpression) return null;
9711
+ let unknown = false;
9799
9712
  for (const element of expression.elements) {
9800
9713
  if (element === null) continue;
9801
- if (element.type === import_utils55.AST_NODE_TYPES.SpreadElement) return null;
9802
- if (element.type !== import_utils55.AST_NODE_TYPES.Literal && !(element.type === import_utils55.AST_NODE_TYPES.TemplateLiteral && element.expressions.length === 0)) {
9803
- return true;
9804
- }
9714
+ const result = element.type === import_utils55.AST_NODE_TYPES.SpreadElement ? null : syntaxMayContainObject(element);
9715
+ if (result === true) return true;
9716
+ if (result === null) unknown = true;
9805
9717
  }
9806
- return false;
9718
+ return unknown ? null : false;
9719
+ }
9720
+ function untypedArrayMayContainObject(node) {
9721
+ return node.elements.some((element) => {
9722
+ if (element === null || element.type === import_utils55.AST_NODE_TYPES.SpreadElement) return false;
9723
+ const value = unwrapComparisonExpression(element);
9724
+ return value.type === import_utils55.AST_NODE_TYPES.ArrayExpression ? untypedArrayMayContainObject(value) : syntaxMayContainObject(value) !== false;
9725
+ });
9807
9726
  }
9808
9727
  var no_json_stringify_object_equality_default = createRule({
9809
9728
  name: "no-json-stringify-object-equality",
@@ -9832,7 +9751,10 @@ var no_json_stringify_object_equality_default = createRule({
9832
9751
  function mayContainObject(node) {
9833
9752
  const syntax = syntaxMayContainObject(node);
9834
9753
  if (syntax !== null) return syntax;
9835
- if (services === null || checker === void 0) return false;
9754
+ if (services === null || checker === void 0) {
9755
+ const expression = unwrapComparisonExpression(node);
9756
+ return expression.type === import_utils55.AST_NODE_TYPES.ArrayExpression && untypedArrayMayContainObject(expression);
9757
+ }
9836
9758
  const type = checker.getTypeAtLocation(
9837
9759
  services.esTreeNodeToTSNodeMap.get(node)
9838
9760
  );
@@ -27212,8 +27134,11 @@ function classMethodCounts(body2, methods) {
27212
27134
  }
27213
27135
 
27214
27136
  // src/rules/source-coupled-test.ts
27137
+ var import_node_path3 = require("path");
27215
27138
  var import_utils143 = require("@typescript-eslint/utils");
27216
- var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|jsonc|toml|py|[cm]?[jt]s)$|(?<!\.(?:tf|tftest))\.json$/iu;
27139
+ var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|jsonc|toml|py|css|html|[cm]?[jt]s|[jt]sx)$|(?<!\.(?:tf|tftest))\.json$/iu;
27140
+ var REPRESENTATION_DIR_RE = /(?:^|\/)(?:__fixtures__|fixtures?|goldens?|snapshots?)(?:\/|$)/iu;
27141
+ var PATH_MODULES = /* @__PURE__ */ new Set(["path", "node:path"]);
27217
27142
  var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
27218
27143
  var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
27219
27144
  var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
@@ -27255,11 +27180,13 @@ var ASSERT_MODULES = /* @__PURE__ */ new Set(["assert", "assert/strict", "node:a
27255
27180
  var SOURCE_COUPLED_TEST_DOCUMENTATION = {
27256
27181
  summary: "Disallow raw repository source text as a test oracle; parse or execute the artifact instead.",
27257
27182
  rationale: "Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
27258
- remediation: "Parse the artifact, execute its validator, or assert on another runtime contract.",
27183
+ remediation: "Reuse adequate existing validation; otherwise assert a distinct parsed or runtime contract. Preserve deliberate representation contracts with an exact justified suppression.",
27259
27184
  category: "testing",
27260
27185
  limitations: [
27261
- "The rule follows stable lexical bindings, static source paths, awaited reads, and common text operations. Reassigned bindings, dynamic paths, unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.",
27186
+ "The rule follows stable lexical bindings, static source paths including JSX/TSX, CSS and HTML, awaited reads, and common text operations. Named imports of node:path/path join and resolve are followed when their complete trailing static path proves a source suffix. Reassigned bindings, dynamic trailing segments, namespace path wrappers, unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.",
27262
27187
  "Assertion roots are scope-resolved for supported test runners and Node assert imports; local or unknown helpers with assertion-like names are not inferred.",
27188
+ "Known fixture, golden, snapshot, and generated source paths are representation contracts and remain unreported; dot segments in the known path are normalized first.",
27189
+ "Contained static paths beneath a stable native mkdtemp/mkdtempSync result are generated artifacts. Literal replace/replaceAll nonidentity guards comparing the same raw read remain unreported; fixed-string comparisons remain candidates.",
27263
27190
  "When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."
27264
27191
  ],
27265
27192
  examples: [
@@ -27321,7 +27248,7 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27321
27248
  type: "suggestion",
27322
27249
  docs: { description: documentation.summary },
27323
27250
  schema: [],
27324
- messages: { rawSourceOracle: "Raw repository source text is the oracle. Parse or execute the artifact so comments, formatting, and unreachable blocks cannot satisfy the contract." }
27251
+ messages: { rawSourceOracle: "Raw repository source text is the oracle. Reuse adequate existing validation; otherwise assert a distinct parsed or runtime contract. Preserve a deliberate representation contract with an exact justified suppression." }
27325
27252
  },
27326
27253
  defaultOptions: [],
27327
27254
  create(context) {
@@ -27362,17 +27289,79 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27362
27289
  const sourcePath = (node) => {
27363
27290
  const current = unwrap6(node);
27364
27291
  const value = stringValue(current);
27365
- if (value !== null) return sourceSuffixRe.test(value);
27292
+ if (value !== null) return sourcePathValue(value);
27366
27293
  if (current.type === import_utils143.AST_NODE_TYPES.Identifier) return visible("paths", current);
27367
27294
  if (current.type === import_utils143.AST_NODE_TYPES.CallExpression || current.type === import_utils143.AST_NODE_TYPES.NewExpression) {
27368
27295
  const callee = unwrapExpression(current.callee);
27369
27296
  const first = current.arguments[0];
27370
27297
  if (first === void 0 || first.type === import_utils143.AST_NODE_TYPES.SpreadElement) return false;
27298
+ const pathBuilder = callee.type === import_utils143.AST_NODE_TYPES.Identifier ? nativePathBuilder(callee) : null;
27299
+ if (current.type === import_utils143.AST_NODE_TYPES.CallExpression && pathBuilder !== null) {
27300
+ if (temporaryDescendant(current, pathBuilder)) return false;
27301
+ const trailing = [];
27302
+ for (const argument of current.arguments.toReversed()) {
27303
+ if (argument.type === import_utils143.AST_NODE_TYPES.SpreadElement) return false;
27304
+ const segment = stringValue(argument);
27305
+ if (segment === null) break;
27306
+ trailing.unshift(segment);
27307
+ if (pathBuilder === "resolve" && import_node_path3.posix.isAbsolute(segment.replaceAll("\\", "/"))) break;
27308
+ }
27309
+ return trailing.length > 0 && sourcePathValue(trailing.join("/"));
27310
+ }
27371
27311
  if (current.type === import_utils143.AST_NODE_TYPES.NewExpression && callee.type === import_utils143.AST_NODE_TYPES.Identifier && callee.name === "URL" && (bindingOf(callee)?.defs.length ?? 0) === 0) return sourcePath(first);
27372
27312
  if (callee.type === import_utils143.AST_NODE_TYPES.Identifier && bindingOf(callee)?.defs.some((definition) => definition.node.type === import_utils143.AST_NODE_TYPES.ImportSpecifier && definition.node.imported.type === import_utils143.AST_NODE_TYPES.Identifier && definition.node.imported.name === "fileURLToPath" && definition.node.parent.type === import_utils143.AST_NODE_TYPES.ImportDeclaration && ["node:url", "url"].includes(String(definition.node.parent.source.value)))) return sourcePath(first);
27373
27313
  }
27374
27314
  return false;
27375
27315
  };
27316
+ const nativePathBuilder = (node) => {
27317
+ const imported = nativeImportedName(node, PATH_MODULES);
27318
+ return imported === "join" || imported === "resolve" ? imported : null;
27319
+ };
27320
+ const nativeImportedName = (node, modules) => {
27321
+ const binding = bindingOf(node);
27322
+ if (binding === null || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return null;
27323
+ for (const definition of binding.defs) {
27324
+ const imported = definition.node;
27325
+ if (imported.type === import_utils143.AST_NODE_TYPES.ImportSpecifier && imported.imported.type === import_utils143.AST_NODE_TYPES.Identifier && imported.parent.type === import_utils143.AST_NODE_TYPES.ImportDeclaration && modules.has(String(imported.parent.source.value))) {
27326
+ return imported.imported.name;
27327
+ }
27328
+ }
27329
+ return null;
27330
+ };
27331
+ const stableInitializer = (node) => {
27332
+ const binding = bindingOf(node);
27333
+ if (binding === null || binding.references.some((reference) => reference.isWrite() && reference.init !== true) || binding.defs.length !== 1) return null;
27334
+ const definition = binding.defs[0]?.node;
27335
+ return definition?.type === import_utils143.AST_NODE_TYPES.VariableDeclarator ? definition.init : null;
27336
+ };
27337
+ const temporaryDirectory = (node, depth = 0) => {
27338
+ if (depth > 8) return false;
27339
+ const current = unwrap6(node);
27340
+ if (current.type === import_utils143.AST_NODE_TYPES.Identifier) {
27341
+ const initializer = stableInitializer(current);
27342
+ return initializer !== null && temporaryDirectory(initializer, depth + 1);
27343
+ }
27344
+ if (current.type !== import_utils143.AST_NODE_TYPES.CallExpression) return false;
27345
+ const callee = unwrap6(current.callee);
27346
+ return callee.type === import_utils143.AST_NODE_TYPES.Identifier && ["mkdtemp", "mkdtempSync"].includes(nativeImportedName(callee, FS_MODULES) ?? "");
27347
+ };
27348
+ const temporaryDescendant = (node, builder) => {
27349
+ const [directory, ...segments] = node.arguments;
27350
+ if (directory === void 0 || directory.type === import_utils143.AST_NODE_TYPES.SpreadElement || !temporaryDirectory(directory)) return false;
27351
+ const values = [];
27352
+ for (const segment of segments) {
27353
+ if (segment.type === import_utils143.AST_NODE_TYPES.SpreadElement) return false;
27354
+ const value = stringValue(segment);
27355
+ if (value === null) return false;
27356
+ values.push(value.replaceAll("\\", "/"));
27357
+ }
27358
+ const sentinel = "/__source_oracle_temporary_directory__";
27359
+ return import_node_path3.posix[builder](sentinel, ...values).startsWith(`${sentinel}/`);
27360
+ };
27361
+ const sourcePathValue = (value) => {
27362
+ const normalized = import_node_path3.posix.normalize(value.replaceAll("\\", "/"));
27363
+ return sourceSuffixRe.test(normalized) && !REPRESENTATION_DIR_RE.test(normalized) && !isGeneratedFile(normalized);
27364
+ };
27376
27365
  const rawRead = (node) => {
27377
27366
  const current = unwrap6(node);
27378
27367
  if (current.type !== import_utils143.AST_NODE_TYPES.CallExpression || current.arguments.length === 0) return false;
@@ -27414,6 +27403,26 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27414
27403
  if (name2 !== null && REGEXP_PREDICATES.has(name2)) return new Set(current.arguments.flatMap((argument) => argument.type === import_utils143.AST_NODE_TYPES.SpreadElement ? [] : [...rawOrigins(argument)]));
27415
27404
  return /* @__PURE__ */ new Set();
27416
27405
  };
27406
+ const literalMutation = (node, depth = 0) => {
27407
+ if (depth > 8) return false;
27408
+ const current = unwrap6(node);
27409
+ if (current.type === import_utils143.AST_NODE_TYPES.Identifier) {
27410
+ const initializer = stableInitializer(current);
27411
+ return initializer !== null && literalMutation(initializer, depth + 1);
27412
+ }
27413
+ if (current.type !== import_utils143.AST_NODE_TYPES.CallExpression || current.arguments.length !== 2) return false;
27414
+ const callee = unwrap6(current.callee);
27415
+ const [pattern, replacement] = current.arguments;
27416
+ return callee.type === import_utils143.AST_NODE_TYPES.MemberExpression && ["replace", "replaceAll"].includes(import_utils143.ASTUtils.getPropertyName(callee) ?? "") && pattern !== void 0 && replacement !== void 0 && pattern.type !== import_utils143.AST_NODE_TYPES.SpreadElement && replacement.type !== import_utils143.AST_NODE_TYPES.SpreadElement && (stringValue(pattern) !== null || pattern.type === import_utils143.AST_NODE_TYPES.Literal && "regex" in pattern) && stringValue(replacement) !== null;
27417
+ };
27418
+ const mutationGuard = (operands) => {
27419
+ if (operands.length !== 2) return false;
27420
+ const [left, right] = operands;
27421
+ if (left === void 0 || right === void 0 || left.type === import_utils143.AST_NODE_TYPES.SpreadElement || right.type === import_utils143.AST_NODE_TYPES.SpreadElement || !literalMutation(left) && !literalMutation(right)) return false;
27422
+ const leftOrigins = rawOrigins(left);
27423
+ const rightOrigins = rawOrigins(right);
27424
+ return leftOrigins.size > 0 && leftOrigins.size === rightOrigins.size && [...leftOrigins].every((origin) => rightOrigins.has(origin));
27425
+ };
27417
27426
  const rawAssertionOrigins = (node) => {
27418
27427
  const callee = unwrap6(node.callee);
27419
27428
  const calleeReceiver = callee.type === import_utils143.AST_NODE_TYPES.MemberExpression ? unwrapExpression(callee.object) : callee;
@@ -27428,9 +27437,11 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27428
27437
  const expectCallee = receiver.type === import_utils143.AST_NODE_TYPES.CallExpression ? unwrapExpression(receiver.callee) : null;
27429
27438
  if (receiver.type === import_utils143.AST_NODE_TYPES.CallExpression && expectCallee?.type === import_utils143.AST_NODE_TYPES.Identifier && assertionKind(expectCallee) === "expect") {
27430
27439
  if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
27440
+ if (["toBe", "toEqual", "toStrictEqual"].includes(matcher) && calleeReceiver.type === import_utils143.AST_NODE_TYPES.MemberExpression && import_utils143.ASTUtils.getPropertyName(calleeReceiver) === "not" && mutationGuard([...receiver.arguments, ...node.arguments])) return /* @__PURE__ */ new Set();
27431
27441
  return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === import_utils143.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
27432
27442
  }
27433
27443
  if (receiver.type !== import_utils143.AST_NODE_TYPES.Identifier || assertionKind(receiver) !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
27444
+ if (["notEqual", "notStrictEqual"].includes(matcher) && mutationGuard(node.arguments)) return /* @__PURE__ */ new Set();
27434
27445
  return new Set(node.arguments.flatMap((argument) => argument.type === import_utils143.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
27435
27446
  };
27436
27447
  const declare = (node, state) => {
@@ -27543,7 +27554,7 @@ var import_utils144 = require("@typescript-eslint/utils");
27543
27554
 
27544
27555
  // src/rules/_is-framework-entrypoint.ts
27545
27556
  var import_node_fs2 = require("fs");
27546
- var import_node_path3 = require("path");
27557
+ var import_node_path4 = require("path");
27547
27558
  var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
27548
27559
  var NEXT_COMPONENT = /^(?:src\/)?app\/(?:.*\/)?(?:page|layout|loading|not-found|template|error|global-error|default|forbidden|unauthorized)\.[jt]sx?$/u;
27549
27560
  var NEXT_METADATA = /^(?:src\/)?app\/(?:.*\/)?(?:(?:opengraph-image|twitter-image|icon|apple-icon)\.[jt]sx?|(?:robots|sitemap|manifest)\.[jt]s)$/u;
@@ -27553,7 +27564,7 @@ function isFrameworkEntrypoint(filename, exportKey) {
27553
27564
  if (!/(?:^|\/)(?:app|pages)\/|(?:^|\/)(?:middleware|proxy|instrumentation)\.[jt]s$/u.test(normalized)) return false;
27554
27565
  const owner = packageOwner(normalized);
27555
27566
  if (owner === null) return false;
27556
- const path = (0, import_node_path3.relative)(owner.root, normalized).replaceAll("\\", "/");
27567
+ const path = (0, import_node_path4.relative)(owner.root, normalized).replaceAll("\\", "/");
27557
27568
  if (declaresFramework(owner.metadata, "next") && isNextEntrypoint(path, exportKey)) return true;
27558
27569
  return declaresFramework(owner.metadata, "astro") && isAstroEntrypoint(path, exportKey);
27559
27570
  }
@@ -27575,14 +27586,14 @@ function isAstroEntrypoint(path, exportKey) {
27575
27586
  );
27576
27587
  }
27577
27588
  function packageOwner(filename) {
27578
- let root = (0, import_node_path3.dirname)(filename);
27589
+ let root = (0, import_node_path4.dirname)(filename);
27579
27590
  while (true) {
27580
27591
  try {
27581
- return { root, metadata: JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path3.join)(root, "package.json"), "utf8")) };
27592
+ return { root, metadata: JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path4.join)(root, "package.json"), "utf8")) };
27582
27593
  } catch (error) {
27583
27594
  if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "ENOENT") return null;
27584
27595
  }
27585
- const parent = (0, import_node_path3.dirname)(root);
27596
+ const parent = (0, import_node_path4.dirname)(root);
27586
27597
  if (parent === root) return null;
27587
27598
  root = parent;
27588
27599
  }
@@ -27671,10 +27682,11 @@ var IAC_SOURCE_SUFFIX_RE = /(?:\.tf\.json|\.tftest\.(?:hcl|json)|\.(?:hcl|tf|tfv
27671
27682
  var IAC_SOURCE_COUPLED_TEST_DOCUMENTATION = {
27672
27683
  summary: "Disallow raw IaC source text as a test oracle; inspect a rendered plan, provider state, or runtime behavior.",
27673
27684
  rationale: "Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration while clients fail silently.",
27674
- remediation: "Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
27685
+ remediation: "Reuse adequate existing validation; otherwise assert a distinct rendered-plan, provider-state, or runtime contract. Preserve deliberate representation contracts with an exact justified suppression.",
27675
27686
  category: "testing",
27676
27687
  limitations: [
27677
- "The rule follows stable lexical bindings and static source paths. Reassigned bindings, dynamic paths, unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.",
27688
+ "The rule follows stable lexical bindings and static source paths, including named node:path/path join and resolve imports with a complete trailing static path. Reassigned bindings, dynamic trailing paths, namespace or unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.",
27689
+ "Known fixture, golden, snapshot, and generated source paths remain unreported after dot-segment normalization, as do contained static paths beneath stable native temporary directories and literal mutation nonidentity guards comparing the same read.",
27678
27690
  "When raw representation is genuinely the contract, use an exact line suppression explaining that contract."
27679
27691
  ],
27680
27692
  examples: [