@sarj/eslint-plugin 16.22.1 → 16.22.3

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 +113 -20
  2. package/dist/index.js +108 -15
  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.1",
869
+ version: "16.22.3",
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: [
@@ -27224,6 +27229,17 @@ function stringValue(node) {
27224
27229
  }
27225
27230
  return null;
27226
27231
  }
27232
+ function staticPathSuffix(pathArguments, builder) {
27233
+ const trailing = [];
27234
+ for (const argument of pathArguments.toReversed()) {
27235
+ if (argument.type === import_utils143.AST_NODE_TYPES.SpreadElement) return null;
27236
+ const segment = stringValue(argument);
27237
+ if (segment === null) break;
27238
+ trailing.unshift(segment);
27239
+ if (builder === "resolve" && import_node_path3.posix.isAbsolute(segment.replaceAll("\\", "/"))) break;
27240
+ }
27241
+ return trailing.length > 0 ? trailing.join("/") : null;
27242
+ }
27227
27243
  function importSource(node) {
27228
27244
  return typeof node.source.value === "string" ? node.source.value : null;
27229
27245
  }
@@ -27243,7 +27259,7 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27243
27259
  type: "suggestion",
27244
27260
  docs: { description: documentation.summary },
27245
27261
  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." }
27262
+ 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
27263
  },
27248
27264
  defaultOptions: [],
27249
27265
  create(context) {
@@ -27284,17 +27300,71 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27284
27300
  const sourcePath = (node) => {
27285
27301
  const current = unwrap6(node);
27286
27302
  const value = stringValue(current);
27287
- if (value !== null) return sourceSuffixRe.test(value);
27303
+ if (value !== null) return sourcePathValue(value);
27288
27304
  if (current.type === import_utils143.AST_NODE_TYPES.Identifier) return visible("paths", current);
27289
- if (current.type === import_utils143.AST_NODE_TYPES.CallExpression || current.type === import_utils143.AST_NODE_TYPES.NewExpression) {
27290
- const callee = unwrapExpression(current.callee);
27291
- const first = current.arguments[0];
27292
- if (first === void 0 || first.type === import_utils143.AST_NODE_TYPES.SpreadElement) return false;
27293
- 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
- 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
- }
27305
+ if (current.type !== import_utils143.AST_NODE_TYPES.CallExpression && current.type !== import_utils143.AST_NODE_TYPES.NewExpression) return false;
27306
+ const callee = unwrapExpression(current.callee);
27307
+ const first = current.arguments[0];
27308
+ if (first === void 0 || first.type === import_utils143.AST_NODE_TYPES.SpreadElement) return false;
27309
+ const pathBuilder = callee.type === import_utils143.AST_NODE_TYPES.Identifier ? nativePathBuilder(callee) : null;
27310
+ if (current.type === import_utils143.AST_NODE_TYPES.CallExpression && pathBuilder !== null) {
27311
+ if (temporaryDescendant(current, pathBuilder)) return false;
27312
+ const suffix = staticPathSuffix(current.arguments, pathBuilder);
27313
+ return suffix !== null && sourcePathValue(suffix);
27314
+ }
27315
+ 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);
27316
+ 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);
27296
27317
  return false;
27297
27318
  };
27319
+ const nativePathBuilder = (node) => {
27320
+ const imported = nativeImportedName(node, PATH_MODULES);
27321
+ return imported === "join" || imported === "resolve" ? imported : null;
27322
+ };
27323
+ const nativeImportedName = (node, modules) => {
27324
+ const binding = bindingOf(node);
27325
+ if (binding === null || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return null;
27326
+ for (const definition of binding.defs) {
27327
+ const imported = definition.node;
27328
+ 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))) {
27329
+ return imported.imported.name;
27330
+ }
27331
+ }
27332
+ return null;
27333
+ };
27334
+ const stableInitializer = (node) => {
27335
+ const binding = bindingOf(node);
27336
+ if (binding === null || binding.references.some((reference) => reference.isWrite() && reference.init !== true) || binding.defs.length !== 1) return null;
27337
+ const definition = binding.defs[0]?.node;
27338
+ return definition?.type === import_utils143.AST_NODE_TYPES.VariableDeclarator ? definition.init : null;
27339
+ };
27340
+ const temporaryDirectory = (node, depth = 0) => {
27341
+ if (depth > 8) return false;
27342
+ const current = unwrap6(node);
27343
+ if (current.type === import_utils143.AST_NODE_TYPES.Identifier) {
27344
+ const initializer = stableInitializer(current);
27345
+ return initializer !== null && temporaryDirectory(initializer, depth + 1);
27346
+ }
27347
+ if (current.type !== import_utils143.AST_NODE_TYPES.CallExpression) return false;
27348
+ const callee = unwrap6(current.callee);
27349
+ return callee.type === import_utils143.AST_NODE_TYPES.Identifier && ["mkdtemp", "mkdtempSync"].includes(nativeImportedName(callee, FS_MODULES) ?? "");
27350
+ };
27351
+ const temporaryDescendant = (node, builder) => {
27352
+ const [directory, ...segments] = node.arguments;
27353
+ if (directory === void 0 || directory.type === import_utils143.AST_NODE_TYPES.SpreadElement || !temporaryDirectory(directory)) return false;
27354
+ const values = [];
27355
+ for (const segment of segments) {
27356
+ if (segment.type === import_utils143.AST_NODE_TYPES.SpreadElement) return false;
27357
+ const value = stringValue(segment);
27358
+ if (value === null) return false;
27359
+ values.push(value.replaceAll("\\", "/"));
27360
+ }
27361
+ const sentinel = "/__source_oracle_temporary_directory__";
27362
+ return import_node_path3.posix[builder](sentinel, ...values).startsWith(`${sentinel}/`);
27363
+ };
27364
+ const sourcePathValue = (value) => {
27365
+ const normalized = import_node_path3.posix.normalize(value.replaceAll("\\", "/"));
27366
+ return sourceSuffixRe.test(normalized) && !REPRESENTATION_DIR_RE.test(normalized) && !isGeneratedFile(normalized);
27367
+ };
27298
27368
  const rawRead = (node) => {
27299
27369
  const current = unwrap6(node);
27300
27370
  if (current.type !== import_utils143.AST_NODE_TYPES.CallExpression || current.arguments.length === 0) return false;
@@ -27336,6 +27406,26 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27336
27406
  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
27407
  return /* @__PURE__ */ new Set();
27338
27408
  };
27409
+ const literalMutation = (node, depth = 0) => {
27410
+ if (depth > 8) return false;
27411
+ const current = unwrap6(node);
27412
+ if (current.type === import_utils143.AST_NODE_TYPES.Identifier) {
27413
+ const initializer = stableInitializer(current);
27414
+ return initializer !== null && literalMutation(initializer, depth + 1);
27415
+ }
27416
+ if (current.type !== import_utils143.AST_NODE_TYPES.CallExpression || current.arguments.length !== 2) return false;
27417
+ const callee = unwrap6(current.callee);
27418
+ const [pattern, replacement] = current.arguments;
27419
+ 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;
27420
+ };
27421
+ const mutationGuard = (operands) => {
27422
+ if (operands.length !== 2) return false;
27423
+ const [left, right] = operands;
27424
+ 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;
27425
+ const leftOrigins = rawOrigins(left);
27426
+ const rightOrigins = rawOrigins(right);
27427
+ return leftOrigins.size > 0 && leftOrigins.size === rightOrigins.size && [...leftOrigins].every((origin) => rightOrigins.has(origin));
27428
+ };
27339
27429
  const rawAssertionOrigins = (node) => {
27340
27430
  const callee = unwrap6(node.callee);
27341
27431
  const calleeReceiver = callee.type === import_utils143.AST_NODE_TYPES.MemberExpression ? unwrapExpression(callee.object) : callee;
@@ -27350,9 +27440,11 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27350
27440
  const expectCallee = receiver.type === import_utils143.AST_NODE_TYPES.CallExpression ? unwrapExpression(receiver.callee) : null;
27351
27441
  if (receiver.type === import_utils143.AST_NODE_TYPES.CallExpression && expectCallee?.type === import_utils143.AST_NODE_TYPES.Identifier && assertionKind(expectCallee) === "expect") {
27352
27442
  if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
27443
+ 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
27444
  return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === import_utils143.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
27354
27445
  }
27355
27446
  if (receiver.type !== import_utils143.AST_NODE_TYPES.Identifier || assertionKind(receiver) !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
27447
+ if (["notEqual", "notStrictEqual"].includes(matcher) && mutationGuard(node.arguments)) return /* @__PURE__ */ new Set();
27356
27448
  return new Set(node.arguments.flatMap((argument) => argument.type === import_utils143.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
27357
27449
  };
27358
27450
  const declare = (node, state) => {
@@ -27465,7 +27557,7 @@ var import_utils144 = require("@typescript-eslint/utils");
27465
27557
 
27466
27558
  // src/rules/_is-framework-entrypoint.ts
27467
27559
  var import_node_fs2 = require("fs");
27468
- var import_node_path3 = require("path");
27560
+ var import_node_path4 = require("path");
27469
27561
  var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
27470
27562
  var NEXT_COMPONENT = /^(?:src\/)?app\/(?:.*\/)?(?:page|layout|loading|not-found|template|error|global-error|default|forbidden|unauthorized)\.[jt]sx?$/u;
27471
27563
  var NEXT_METADATA = /^(?:src\/)?app\/(?:.*\/)?(?:(?:opengraph-image|twitter-image|icon|apple-icon)\.[jt]sx?|(?:robots|sitemap|manifest)\.[jt]s)$/u;
@@ -27475,7 +27567,7 @@ function isFrameworkEntrypoint(filename, exportKey) {
27475
27567
  if (!/(?:^|\/)(?:app|pages)\/|(?:^|\/)(?:middleware|proxy|instrumentation)\.[jt]s$/u.test(normalized)) return false;
27476
27568
  const owner = packageOwner(normalized);
27477
27569
  if (owner === null) return false;
27478
- const path = (0, import_node_path3.relative)(owner.root, normalized).replaceAll("\\", "/");
27570
+ const path = (0, import_node_path4.relative)(owner.root, normalized).replaceAll("\\", "/");
27479
27571
  if (declaresFramework(owner.metadata, "next") && isNextEntrypoint(path, exportKey)) return true;
27480
27572
  return declaresFramework(owner.metadata, "astro") && isAstroEntrypoint(path, exportKey);
27481
27573
  }
@@ -27497,14 +27589,14 @@ function isAstroEntrypoint(path, exportKey) {
27497
27589
  );
27498
27590
  }
27499
27591
  function packageOwner(filename) {
27500
- let root = (0, import_node_path3.dirname)(filename);
27592
+ let root = (0, import_node_path4.dirname)(filename);
27501
27593
  while (true) {
27502
27594
  try {
27503
- return { root, metadata: JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path3.join)(root, "package.json"), "utf8")) };
27595
+ return { root, metadata: JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path4.join)(root, "package.json"), "utf8")) };
27504
27596
  } catch (error) {
27505
27597
  if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "ENOENT") return null;
27506
27598
  }
27507
- const parent = (0, import_node_path3.dirname)(root);
27599
+ const parent = (0, import_node_path4.dirname)(root);
27508
27600
  if (parent === root) return null;
27509
27601
  root = parent;
27510
27602
  }
@@ -27593,10 +27685,11 @@ var IAC_SOURCE_SUFFIX_RE = /(?:\.tf\.json|\.tftest\.(?:hcl|json)|\.(?:hcl|tf|tfv
27593
27685
  var IAC_SOURCE_COUPLED_TEST_DOCUMENTATION = {
27594
27686
  summary: "Disallow raw IaC source text as a test oracle; inspect a rendered plan, provider state, or runtime behavior.",
27595
27687
  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.",
27688
+ 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
27689
  category: "testing",
27598
27690
  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.",
27691
+ "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.",
27692
+ "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
27693
  "When raw representation is genuinely the contract, use an exact line suppression explaining that contract."
27601
27694
  ],
27602
27695
  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.3",
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: [
@@ -27248,6 +27253,17 @@ function stringValue(node) {
27248
27253
  }
27249
27254
  return null;
27250
27255
  }
27256
+ function staticPathSuffix(pathArguments, builder) {
27257
+ const trailing = [];
27258
+ for (const argument of pathArguments.toReversed()) {
27259
+ if (argument.type === AST_NODE_TYPES122.SpreadElement) return null;
27260
+ const segment = stringValue(argument);
27261
+ if (segment === null) break;
27262
+ trailing.unshift(segment);
27263
+ if (builder === "resolve" && posix2.isAbsolute(segment.replaceAll("\\", "/"))) break;
27264
+ }
27265
+ return trailing.length > 0 ? trailing.join("/") : null;
27266
+ }
27251
27267
  function importSource(node) {
27252
27268
  return typeof node.source.value === "string" ? node.source.value : null;
27253
27269
  }
@@ -27267,7 +27283,7 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27267
27283
  type: "suggestion",
27268
27284
  docs: { description: documentation.summary },
27269
27285
  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." }
27286
+ 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
27287
  },
27272
27288
  defaultOptions: [],
27273
27289
  create(context) {
@@ -27308,17 +27324,71 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27308
27324
  const sourcePath = (node) => {
27309
27325
  const current = unwrap6(node);
27310
27326
  const value = stringValue(current);
27311
- if (value !== null) return sourceSuffixRe.test(value);
27327
+ if (value !== null) return sourcePathValue(value);
27312
27328
  if (current.type === AST_NODE_TYPES122.Identifier) return visible("paths", current);
27313
- if (current.type === AST_NODE_TYPES122.CallExpression || current.type === AST_NODE_TYPES122.NewExpression) {
27314
- const callee = unwrapExpression(current.callee);
27315
- const first = current.arguments[0];
27316
- if (first === void 0 || first.type === AST_NODE_TYPES122.SpreadElement) return false;
27317
- 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
- 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
- }
27329
+ if (current.type !== AST_NODE_TYPES122.CallExpression && current.type !== AST_NODE_TYPES122.NewExpression) return false;
27330
+ const callee = unwrapExpression(current.callee);
27331
+ const first = current.arguments[0];
27332
+ if (first === void 0 || first.type === AST_NODE_TYPES122.SpreadElement) return false;
27333
+ const pathBuilder = callee.type === AST_NODE_TYPES122.Identifier ? nativePathBuilder(callee) : null;
27334
+ if (current.type === AST_NODE_TYPES122.CallExpression && pathBuilder !== null) {
27335
+ if (temporaryDescendant(current, pathBuilder)) return false;
27336
+ const suffix = staticPathSuffix(current.arguments, pathBuilder);
27337
+ return suffix !== null && sourcePathValue(suffix);
27338
+ }
27339
+ 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);
27340
+ 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);
27320
27341
  return false;
27321
27342
  };
27343
+ const nativePathBuilder = (node) => {
27344
+ const imported = nativeImportedName(node, PATH_MODULES);
27345
+ return imported === "join" || imported === "resolve" ? imported : null;
27346
+ };
27347
+ const nativeImportedName = (node, modules) => {
27348
+ const binding = bindingOf(node);
27349
+ if (binding === null || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return null;
27350
+ for (const definition of binding.defs) {
27351
+ const imported = definition.node;
27352
+ 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))) {
27353
+ return imported.imported.name;
27354
+ }
27355
+ }
27356
+ return null;
27357
+ };
27358
+ const stableInitializer = (node) => {
27359
+ const binding = bindingOf(node);
27360
+ if (binding === null || binding.references.some((reference) => reference.isWrite() && reference.init !== true) || binding.defs.length !== 1) return null;
27361
+ const definition = binding.defs[0]?.node;
27362
+ return definition?.type === AST_NODE_TYPES122.VariableDeclarator ? definition.init : null;
27363
+ };
27364
+ const temporaryDirectory = (node, depth = 0) => {
27365
+ if (depth > 8) return false;
27366
+ const current = unwrap6(node);
27367
+ if (current.type === AST_NODE_TYPES122.Identifier) {
27368
+ const initializer = stableInitializer(current);
27369
+ return initializer !== null && temporaryDirectory(initializer, depth + 1);
27370
+ }
27371
+ if (current.type !== AST_NODE_TYPES122.CallExpression) return false;
27372
+ const callee = unwrap6(current.callee);
27373
+ return callee.type === AST_NODE_TYPES122.Identifier && ["mkdtemp", "mkdtempSync"].includes(nativeImportedName(callee, FS_MODULES) ?? "");
27374
+ };
27375
+ const temporaryDescendant = (node, builder) => {
27376
+ const [directory, ...segments] = node.arguments;
27377
+ if (directory === void 0 || directory.type === AST_NODE_TYPES122.SpreadElement || !temporaryDirectory(directory)) return false;
27378
+ const values = [];
27379
+ for (const segment of segments) {
27380
+ if (segment.type === AST_NODE_TYPES122.SpreadElement) return false;
27381
+ const value = stringValue(segment);
27382
+ if (value === null) return false;
27383
+ values.push(value.replaceAll("\\", "/"));
27384
+ }
27385
+ const sentinel = "/__source_oracle_temporary_directory__";
27386
+ return posix2[builder](sentinel, ...values).startsWith(`${sentinel}/`);
27387
+ };
27388
+ const sourcePathValue = (value) => {
27389
+ const normalized = posix2.normalize(value.replaceAll("\\", "/"));
27390
+ return sourceSuffixRe.test(normalized) && !REPRESENTATION_DIR_RE.test(normalized) && !isGeneratedFile(normalized);
27391
+ };
27322
27392
  const rawRead = (node) => {
27323
27393
  const current = unwrap6(node);
27324
27394
  if (current.type !== AST_NODE_TYPES122.CallExpression || current.arguments.length === 0) return false;
@@ -27360,6 +27430,26 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27360
27430
  if (name2 !== null && REGEXP_PREDICATES.has(name2)) return new Set(current.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES122.SpreadElement ? [] : [...rawOrigins(argument)]));
27361
27431
  return /* @__PURE__ */ new Set();
27362
27432
  };
27433
+ const literalMutation = (node, depth = 0) => {
27434
+ if (depth > 8) return false;
27435
+ const current = unwrap6(node);
27436
+ if (current.type === AST_NODE_TYPES122.Identifier) {
27437
+ const initializer = stableInitializer(current);
27438
+ return initializer !== null && literalMutation(initializer, depth + 1);
27439
+ }
27440
+ if (current.type !== AST_NODE_TYPES122.CallExpression || current.arguments.length !== 2) return false;
27441
+ const callee = unwrap6(current.callee);
27442
+ const [pattern, replacement] = current.arguments;
27443
+ 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;
27444
+ };
27445
+ const mutationGuard = (operands) => {
27446
+ if (operands.length !== 2) return false;
27447
+ const [left, right] = operands;
27448
+ 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;
27449
+ const leftOrigins = rawOrigins(left);
27450
+ const rightOrigins = rawOrigins(right);
27451
+ return leftOrigins.size > 0 && leftOrigins.size === rightOrigins.size && [...leftOrigins].every((origin) => rightOrigins.has(origin));
27452
+ };
27363
27453
  const rawAssertionOrigins = (node) => {
27364
27454
  const callee = unwrap6(node.callee);
27365
27455
  const calleeReceiver = callee.type === AST_NODE_TYPES122.MemberExpression ? unwrapExpression(callee.object) : callee;
@@ -27374,9 +27464,11 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
27374
27464
  const expectCallee = receiver.type === AST_NODE_TYPES122.CallExpression ? unwrapExpression(receiver.callee) : null;
27375
27465
  if (receiver.type === AST_NODE_TYPES122.CallExpression && expectCallee?.type === AST_NODE_TYPES122.Identifier && assertionKind(expectCallee) === "expect") {
27376
27466
  if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
27467
+ 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
27468
  return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === AST_NODE_TYPES122.SpreadElement ? [] : [...evidenceOrigins(argument)]));
27378
27469
  }
27379
27470
  if (receiver.type !== AST_NODE_TYPES122.Identifier || assertionKind(receiver) !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
27471
+ if (["notEqual", "notStrictEqual"].includes(matcher) && mutationGuard(node.arguments)) return /* @__PURE__ */ new Set();
27380
27472
  return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES122.SpreadElement ? [] : [...evidenceOrigins(argument)]));
27381
27473
  };
27382
27474
  const declare = (node, state) => {
@@ -27617,10 +27709,11 @@ var IAC_SOURCE_SUFFIX_RE = /(?:\.tf\.json|\.tftest\.(?:hcl|json)|\.(?:hcl|tf|tfv
27617
27709
  var IAC_SOURCE_COUPLED_TEST_DOCUMENTATION = {
27618
27710
  summary: "Disallow raw IaC source text as a test oracle; inspect a rendered plan, provider state, or runtime behavior.",
27619
27711
  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.",
27712
+ 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
27713
  category: "testing",
27622
27714
  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.",
27715
+ "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.",
27716
+ "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
27717
  "When raw representation is genuinely the contract, use an exact line suppression explaining that contract."
27625
27718
  ],
27626
27719
  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.3",
4
4
  "packageManager": "npm@12.2.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",