@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.
- package/dist/index.cjs +113 -20
- package/dist/index.js +108 -15
- 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.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: "
|
|
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: [
|
|
@@ -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.
|
|
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
|
|
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
|
|
27290
|
-
|
|
27291
|
-
|
|
27292
|
-
|
|
27293
|
-
|
|
27294
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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: "
|
|
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.
|
|
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: "
|
|
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: [
|
|
@@ -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.
|
|
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
|
|
27327
|
+
if (value !== null) return sourcePathValue(value);
|
|
27312
27328
|
if (current.type === AST_NODE_TYPES122.Identifier) return visible("paths", current);
|
|
27313
|
-
if (current.type
|
|
27314
|
-
|
|
27315
|
-
|
|
27316
|
-
|
|
27317
|
-
|
|
27318
|
-
|
|
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: "
|
|
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: [
|