@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 +103 -13
- package/dist/index.js +98 -8
- 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.
|
|
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: "
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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: "
|
|
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.
|
|
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: "
|
|
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
|
|
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.
|
|
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
|
|
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: "
|
|
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: [
|