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