@sarj/eslint-plugin 16.22.1 → 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.
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.1",
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",
@@ -27134,8 +27134,11 @@ function classMethodCounts(body2, methods) {
27134
27134
  }
27135
27135
 
27136
27136
  // src/rules/source-coupled-test.ts
27137
+ var import_node_path3 = require("path");
27137
27138
  var import_utils143 = require("@typescript-eslint/utils");
27138
- 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"]);
27139
27142
  var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
27140
27143
  var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
27141
27144
  var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
@@ -27177,11 +27180,13 @@ var ASSERT_MODULES = /* @__PURE__ */ new Set(["assert", "assert/strict", "node:a
27177
27180
  var SOURCE_COUPLED_TEST_DOCUMENTATION = {
27178
27181
  summary: "Disallow raw repository source text as a test oracle; parse or execute the artifact instead.",
27179
27182
  rationale: "Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
27180
- 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.",
27181
27184
  category: "testing",
27182
27185
  limitations: [
27183
- "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.",
27184
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.",
27185
27190
  "When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."
27186
27191
  ],
27187
27192
  examples: [
@@ -27243,7 +27248,7 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27243
27248
  type: "suggestion",
27244
27249
  docs: { description: documentation.summary },
27245
27250
  schema: [],
27246
- 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." }
27247
27252
  },
27248
27253
  defaultOptions: [],
27249
27254
  create(context) {
@@ -27284,17 +27289,79 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27284
27289
  const sourcePath = (node) => {
27285
27290
  const current = unwrap6(node);
27286
27291
  const value = stringValue(current);
27287
- if (value !== null) return sourceSuffixRe.test(value);
27292
+ if (value !== null) return sourcePathValue(value);
27288
27293
  if (current.type === import_utils143.AST_NODE_TYPES.Identifier) return visible("paths", current);
27289
27294
  if (current.type === import_utils143.AST_NODE_TYPES.CallExpression || current.type === import_utils143.AST_NODE_TYPES.NewExpression) {
27290
27295
  const callee = unwrapExpression(current.callee);
27291
27296
  const first = current.arguments[0];
27292
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
+ }
27293
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);
27294
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);
27295
27313
  }
27296
27314
  return false;
27297
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
+ };
27298
27365
  const rawRead = (node) => {
27299
27366
  const current = unwrap6(node);
27300
27367
  if (current.type !== import_utils143.AST_NODE_TYPES.CallExpression || current.arguments.length === 0) return false;
@@ -27336,6 +27403,26 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27336
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)]));
27337
27404
  return /* @__PURE__ */ new Set();
27338
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
+ };
27339
27426
  const rawAssertionOrigins = (node) => {
27340
27427
  const callee = unwrap6(node.callee);
27341
27428
  const calleeReceiver = callee.type === import_utils143.AST_NODE_TYPES.MemberExpression ? unwrapExpression(callee.object) : callee;
@@ -27350,9 +27437,11 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27350
27437
  const expectCallee = receiver.type === import_utils143.AST_NODE_TYPES.CallExpression ? unwrapExpression(receiver.callee) : null;
27351
27438
  if (receiver.type === import_utils143.AST_NODE_TYPES.CallExpression && expectCallee?.type === import_utils143.AST_NODE_TYPES.Identifier && assertionKind(expectCallee) === "expect") {
27352
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();
27353
27441
  return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === import_utils143.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
27354
27442
  }
27355
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();
27356
27445
  return new Set(node.arguments.flatMap((argument) => argument.type === import_utils143.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
27357
27446
  };
27358
27447
  const declare = (node, state) => {
@@ -27465,7 +27554,7 @@ var import_utils144 = require("@typescript-eslint/utils");
27465
27554
 
27466
27555
  // src/rules/_is-framework-entrypoint.ts
27467
27556
  var import_node_fs2 = require("fs");
27468
- var import_node_path3 = require("path");
27557
+ var import_node_path4 = require("path");
27469
27558
  var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
27470
27559
  var NEXT_COMPONENT = /^(?:src\/)?app\/(?:.*\/)?(?:page|layout|loading|not-found|template|error|global-error|default|forbidden|unauthorized)\.[jt]sx?$/u;
27471
27560
  var NEXT_METADATA = /^(?:src\/)?app\/(?:.*\/)?(?:(?:opengraph-image|twitter-image|icon|apple-icon)\.[jt]sx?|(?:robots|sitemap|manifest)\.[jt]s)$/u;
@@ -27475,7 +27564,7 @@ function isFrameworkEntrypoint(filename, exportKey) {
27475
27564
  if (!/(?:^|\/)(?:app|pages)\/|(?:^|\/)(?:middleware|proxy|instrumentation)\.[jt]s$/u.test(normalized)) return false;
27476
27565
  const owner = packageOwner(normalized);
27477
27566
  if (owner === null) return false;
27478
- const path = (0, import_node_path3.relative)(owner.root, normalized).replaceAll("\\", "/");
27567
+ const path = (0, import_node_path4.relative)(owner.root, normalized).replaceAll("\\", "/");
27479
27568
  if (declaresFramework(owner.metadata, "next") && isNextEntrypoint(path, exportKey)) return true;
27480
27569
  return declaresFramework(owner.metadata, "astro") && isAstroEntrypoint(path, exportKey);
27481
27570
  }
@@ -27497,14 +27586,14 @@ function isAstroEntrypoint(path, exportKey) {
27497
27586
  );
27498
27587
  }
27499
27588
  function packageOwner(filename) {
27500
- let root = (0, import_node_path3.dirname)(filename);
27589
+ let root = (0, import_node_path4.dirname)(filename);
27501
27590
  while (true) {
27502
27591
  try {
27503
- 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")) };
27504
27593
  } catch (error) {
27505
27594
  if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "ENOENT") return null;
27506
27595
  }
27507
- const parent = (0, import_node_path3.dirname)(root);
27596
+ const parent = (0, import_node_path4.dirname)(root);
27508
27597
  if (parent === root) return null;
27509
27598
  root = parent;
27510
27599
  }
@@ -27593,10 +27682,11 @@ var IAC_SOURCE_SUFFIX_RE = /(?:\.tf\.json|\.tftest\.(?:hcl|json)|\.(?:hcl|tf|tfv
27593
27682
  var IAC_SOURCE_COUPLED_TEST_DOCUMENTATION = {
27594
27683
  summary: "Disallow raw IaC source text as a test oracle; inspect a rendered plan, provider state, or runtime behavior.",
27595
27684
  rationale: "Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration while clients fail silently.",
27596
- 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.",
27597
27686
  category: "testing",
27598
27687
  limitations: [
27599
- "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.",
27600
27690
  "When raw representation is genuinely the contract, use an exact line suppression explaining that contract."
27601
27691
  ],
27602
27692
  examples: [
package/dist/index.js CHANGED
@@ -819,7 +819,7 @@ function isSearchParamReceiver(call, services) {
819
819
  // package.json
820
820
  var package_default = {
821
821
  name: "@sarj/eslint-plugin",
822
- version: "16.22.1",
822
+ version: "16.22.2",
823
823
  packageManager: "npm@12.2.0",
824
824
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
825
825
  type: "module",
@@ -27158,8 +27158,11 @@ function classMethodCounts(body2, methods) {
27158
27158
  }
27159
27159
 
27160
27160
  // src/rules/source-coupled-test.ts
27161
+ import { posix as posix2 } from "path";
27161
27162
  import { AST_NODE_TYPES as AST_NODE_TYPES122, ASTUtils as ASTUtils81 } from "@typescript-eslint/utils";
27162
- var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|jsonc|toml|py|[cm]?[jt]s)$|(?<!\.(?:tf|tftest))\.json$/iu;
27163
+ var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|jsonc|toml|py|css|html|[cm]?[jt]s|[jt]sx)$|(?<!\.(?:tf|tftest))\.json$/iu;
27164
+ var REPRESENTATION_DIR_RE = /(?:^|\/)(?:__fixtures__|fixtures?|goldens?|snapshots?)(?:\/|$)/iu;
27165
+ var PATH_MODULES = /* @__PURE__ */ new Set(["path", "node:path"]);
27163
27166
  var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
27164
27167
  var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
27165
27168
  var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
@@ -27201,11 +27204,13 @@ var ASSERT_MODULES = /* @__PURE__ */ new Set(["assert", "assert/strict", "node:a
27201
27204
  var SOURCE_COUPLED_TEST_DOCUMENTATION = {
27202
27205
  summary: "Disallow raw repository source text as a test oracle; parse or execute the artifact instead.",
27203
27206
  rationale: "Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
27204
- remediation: "Parse the artifact, execute its validator, or assert on another runtime contract.",
27207
+ remediation: "Reuse adequate existing validation; otherwise assert a distinct parsed or runtime contract. Preserve deliberate representation contracts with an exact justified suppression.",
27205
27208
  category: "testing",
27206
27209
  limitations: [
27207
- "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.",
27210
+ "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.",
27208
27211
  "Assertion roots are scope-resolved for supported test runners and Node assert imports; local or unknown helpers with assertion-like names are not inferred.",
27212
+ "Known fixture, golden, snapshot, and generated source paths are representation contracts and remain unreported; dot segments in the known path are normalized first.",
27213
+ "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.",
27209
27214
  "When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."
27210
27215
  ],
27211
27216
  examples: [
@@ -27267,7 +27272,7 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27267
27272
  type: "suggestion",
27268
27273
  docs: { description: documentation.summary },
27269
27274
  schema: [],
27270
- messages: { rawSourceOracle: "Raw repository source text is the oracle. Parse or execute the artifact so comments, formatting, and unreachable blocks cannot satisfy the contract." }
27275
+ 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." }
27271
27276
  },
27272
27277
  defaultOptions: [],
27273
27278
  create(context) {
@@ -27308,17 +27313,79 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27308
27313
  const sourcePath = (node) => {
27309
27314
  const current = unwrap6(node);
27310
27315
  const value = stringValue(current);
27311
- if (value !== null) return sourceSuffixRe.test(value);
27316
+ if (value !== null) return sourcePathValue(value);
27312
27317
  if (current.type === AST_NODE_TYPES122.Identifier) return visible("paths", current);
27313
27318
  if (current.type === AST_NODE_TYPES122.CallExpression || current.type === AST_NODE_TYPES122.NewExpression) {
27314
27319
  const callee = unwrapExpression(current.callee);
27315
27320
  const first = current.arguments[0];
27316
27321
  if (first === void 0 || first.type === AST_NODE_TYPES122.SpreadElement) return false;
27322
+ const pathBuilder = callee.type === AST_NODE_TYPES122.Identifier ? nativePathBuilder(callee) : null;
27323
+ if (current.type === AST_NODE_TYPES122.CallExpression && pathBuilder !== null) {
27324
+ if (temporaryDescendant(current, pathBuilder)) return false;
27325
+ const trailing = [];
27326
+ for (const argument of current.arguments.toReversed()) {
27327
+ if (argument.type === AST_NODE_TYPES122.SpreadElement) return false;
27328
+ const segment = stringValue(argument);
27329
+ if (segment === null) break;
27330
+ trailing.unshift(segment);
27331
+ if (pathBuilder === "resolve" && posix2.isAbsolute(segment.replaceAll("\\", "/"))) break;
27332
+ }
27333
+ return trailing.length > 0 && sourcePathValue(trailing.join("/"));
27334
+ }
27317
27335
  if (current.type === AST_NODE_TYPES122.NewExpression && callee.type === AST_NODE_TYPES122.Identifier && callee.name === "URL" && (bindingOf(callee)?.defs.length ?? 0) === 0) return sourcePath(first);
27318
27336
  if (callee.type === AST_NODE_TYPES122.Identifier && bindingOf(callee)?.defs.some((definition) => definition.node.type === AST_NODE_TYPES122.ImportSpecifier && definition.node.imported.type === AST_NODE_TYPES122.Identifier && definition.node.imported.name === "fileURLToPath" && definition.node.parent.type === AST_NODE_TYPES122.ImportDeclaration && ["node:url", "url"].includes(String(definition.node.parent.source.value)))) return sourcePath(first);
27319
27337
  }
27320
27338
  return false;
27321
27339
  };
27340
+ const nativePathBuilder = (node) => {
27341
+ const imported = nativeImportedName(node, PATH_MODULES);
27342
+ return imported === "join" || imported === "resolve" ? imported : null;
27343
+ };
27344
+ const nativeImportedName = (node, modules) => {
27345
+ const binding = bindingOf(node);
27346
+ if (binding === null || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return null;
27347
+ for (const definition of binding.defs) {
27348
+ const imported = definition.node;
27349
+ if (imported.type === AST_NODE_TYPES122.ImportSpecifier && imported.imported.type === AST_NODE_TYPES122.Identifier && imported.parent.type === AST_NODE_TYPES122.ImportDeclaration && modules.has(String(imported.parent.source.value))) {
27350
+ return imported.imported.name;
27351
+ }
27352
+ }
27353
+ return null;
27354
+ };
27355
+ const stableInitializer = (node) => {
27356
+ const binding = bindingOf(node);
27357
+ if (binding === null || binding.references.some((reference) => reference.isWrite() && reference.init !== true) || binding.defs.length !== 1) return null;
27358
+ const definition = binding.defs[0]?.node;
27359
+ return definition?.type === AST_NODE_TYPES122.VariableDeclarator ? definition.init : null;
27360
+ };
27361
+ const temporaryDirectory = (node, depth = 0) => {
27362
+ if (depth > 8) return false;
27363
+ const current = unwrap6(node);
27364
+ if (current.type === AST_NODE_TYPES122.Identifier) {
27365
+ const initializer = stableInitializer(current);
27366
+ return initializer !== null && temporaryDirectory(initializer, depth + 1);
27367
+ }
27368
+ if (current.type !== AST_NODE_TYPES122.CallExpression) return false;
27369
+ const callee = unwrap6(current.callee);
27370
+ return callee.type === AST_NODE_TYPES122.Identifier && ["mkdtemp", "mkdtempSync"].includes(nativeImportedName(callee, FS_MODULES) ?? "");
27371
+ };
27372
+ const temporaryDescendant = (node, builder) => {
27373
+ const [directory, ...segments] = node.arguments;
27374
+ if (directory === void 0 || directory.type === AST_NODE_TYPES122.SpreadElement || !temporaryDirectory(directory)) return false;
27375
+ const values = [];
27376
+ for (const segment of segments) {
27377
+ if (segment.type === AST_NODE_TYPES122.SpreadElement) return false;
27378
+ const value = stringValue(segment);
27379
+ if (value === null) return false;
27380
+ values.push(value.replaceAll("\\", "/"));
27381
+ }
27382
+ const sentinel = "/__source_oracle_temporary_directory__";
27383
+ return posix2[builder](sentinel, ...values).startsWith(`${sentinel}/`);
27384
+ };
27385
+ const sourcePathValue = (value) => {
27386
+ const normalized = posix2.normalize(value.replaceAll("\\", "/"));
27387
+ return sourceSuffixRe.test(normalized) && !REPRESENTATION_DIR_RE.test(normalized) && !isGeneratedFile(normalized);
27388
+ };
27322
27389
  const rawRead = (node) => {
27323
27390
  const current = unwrap6(node);
27324
27391
  if (current.type !== AST_NODE_TYPES122.CallExpression || current.arguments.length === 0) return false;
@@ -27360,6 +27427,26 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27360
27427
  if (name2 !== null && REGEXP_PREDICATES.has(name2)) return new Set(current.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES122.SpreadElement ? [] : [...rawOrigins(argument)]));
27361
27428
  return /* @__PURE__ */ new Set();
27362
27429
  };
27430
+ const literalMutation = (node, depth = 0) => {
27431
+ if (depth > 8) return false;
27432
+ const current = unwrap6(node);
27433
+ if (current.type === AST_NODE_TYPES122.Identifier) {
27434
+ const initializer = stableInitializer(current);
27435
+ return initializer !== null && literalMutation(initializer, depth + 1);
27436
+ }
27437
+ if (current.type !== AST_NODE_TYPES122.CallExpression || current.arguments.length !== 2) return false;
27438
+ const callee = unwrap6(current.callee);
27439
+ const [pattern, replacement] = current.arguments;
27440
+ return callee.type === AST_NODE_TYPES122.MemberExpression && ["replace", "replaceAll"].includes(ASTUtils81.getPropertyName(callee) ?? "") && pattern !== void 0 && replacement !== void 0 && pattern.type !== AST_NODE_TYPES122.SpreadElement && replacement.type !== AST_NODE_TYPES122.SpreadElement && (stringValue(pattern) !== null || pattern.type === AST_NODE_TYPES122.Literal && "regex" in pattern) && stringValue(replacement) !== null;
27441
+ };
27442
+ const mutationGuard = (operands) => {
27443
+ if (operands.length !== 2) return false;
27444
+ const [left, right] = operands;
27445
+ if (left === void 0 || right === void 0 || left.type === AST_NODE_TYPES122.SpreadElement || right.type === AST_NODE_TYPES122.SpreadElement || !literalMutation(left) && !literalMutation(right)) return false;
27446
+ const leftOrigins = rawOrigins(left);
27447
+ const rightOrigins = rawOrigins(right);
27448
+ return leftOrigins.size > 0 && leftOrigins.size === rightOrigins.size && [...leftOrigins].every((origin) => rightOrigins.has(origin));
27449
+ };
27363
27450
  const rawAssertionOrigins = (node) => {
27364
27451
  const callee = unwrap6(node.callee);
27365
27452
  const calleeReceiver = callee.type === AST_NODE_TYPES122.MemberExpression ? unwrapExpression(callee.object) : callee;
@@ -27374,9 +27461,11 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27374
27461
  const expectCallee = receiver.type === AST_NODE_TYPES122.CallExpression ? unwrapExpression(receiver.callee) : null;
27375
27462
  if (receiver.type === AST_NODE_TYPES122.CallExpression && expectCallee?.type === AST_NODE_TYPES122.Identifier && assertionKind(expectCallee) === "expect") {
27376
27463
  if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
27464
+ if (["toBe", "toEqual", "toStrictEqual"].includes(matcher) && calleeReceiver.type === AST_NODE_TYPES122.MemberExpression && ASTUtils81.getPropertyName(calleeReceiver) === "not" && mutationGuard([...receiver.arguments, ...node.arguments])) return /* @__PURE__ */ new Set();
27377
27465
  return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === AST_NODE_TYPES122.SpreadElement ? [] : [...evidenceOrigins(argument)]));
27378
27466
  }
27379
27467
  if (receiver.type !== AST_NODE_TYPES122.Identifier || assertionKind(receiver) !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
27468
+ if (["notEqual", "notStrictEqual"].includes(matcher) && mutationGuard(node.arguments)) return /* @__PURE__ */ new Set();
27380
27469
  return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES122.SpreadElement ? [] : [...evidenceOrigins(argument)]));
27381
27470
  };
27382
27471
  const declare = (node, state) => {
@@ -27617,10 +27706,11 @@ var IAC_SOURCE_SUFFIX_RE = /(?:\.tf\.json|\.tftest\.(?:hcl|json)|\.(?:hcl|tf|tfv
27617
27706
  var IAC_SOURCE_COUPLED_TEST_DOCUMENTATION = {
27618
27707
  summary: "Disallow raw IaC source text as a test oracle; inspect a rendered plan, provider state, or runtime behavior.",
27619
27708
  rationale: "Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration while clients fail silently.",
27620
- remediation: "Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
27709
+ 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.",
27621
27710
  category: "testing",
27622
27711
  limitations: [
27623
- "The rule follows stable lexical bindings and static source paths. Reassigned bindings, dynamic paths, unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.",
27712
+ "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.",
27713
+ "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.",
27624
27714
  "When raw representation is genuinely the contract, use an exact line suppression explaining that contract."
27625
27715
  ],
27626
27716
  examples: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.22.1",
3
+ "version": "16.22.2",
4
4
  "packageManager": "npm@12.2.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",