@sarj/eslint-plugin 15.17.10 → 15.17.11
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 +725 -278
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +816 -369
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -678,10 +678,11 @@ var TEST_MODULES = /* @__PURE__ */ new Set(["@jest/globals", "@playwright/test",
|
|
|
678
678
|
var DUPLICATE_TEST_BODY_DOCUMENTATION = {
|
|
679
679
|
summary: "Disallow substantial sibling tests with the same body shape; express their differing inputs as a parameterized case table.",
|
|
680
680
|
rationale: "Copy-pasted test bodies hide the cases that differ and allow equivalent assertions to drift independently.",
|
|
681
|
-
remediation: "
|
|
681
|
+
remediation: "Consider a case table with one named test or subtest per case; preserve setup lifetime, test modifiers, and each case's assertions rather than deleting coverage.",
|
|
682
682
|
category: "testing",
|
|
683
683
|
limitations: [
|
|
684
|
-
"The rule compares substantial sibling tests within one suite and skips inline snapshots and materially different comments."
|
|
684
|
+
"The rule compares substantial sibling tests within one suite and skips inline snapshots and materially different comments.",
|
|
685
|
+
"Matching normalized body shapes do not prove runtime equivalence or independent setup; parameterization is a manual review, not an automatic deletion."
|
|
685
686
|
],
|
|
686
687
|
examples: [
|
|
687
688
|
{
|
|
@@ -866,6 +867,10 @@ function isDuplicateTestFrameworkIdentifier(identifier, sourceCode) {
|
|
|
866
867
|
const variable = import_utils3.ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
867
868
|
if (variable === null || variable.defs.length === 0) return true;
|
|
868
869
|
return variable.defs.some((definition) => {
|
|
870
|
+
if (definition.node.type === import_utils3.AST_NODE_TYPES.ImportDefaultSpecifier) return definition.node.parent.source.value === "node:test";
|
|
871
|
+
if (definition.node.type !== import_utils3.AST_NODE_TYPES.ImportSpecifier) return false;
|
|
872
|
+
const imported = definition.node.imported;
|
|
873
|
+
if (!TEST_CALLERS.has(imported.type === import_utils3.AST_NODE_TYPES.Identifier ? imported.name : String(imported.value))) return false;
|
|
869
874
|
let current = definition.node;
|
|
870
875
|
while (current != null && current.type !== import_utils3.AST_NODE_TYPES.ImportDeclaration) current = current.parent;
|
|
871
876
|
return current?.type === import_utils3.AST_NODE_TYPES.ImportDeclaration && typeof current.source.value === "string" && TEST_MODULES.has(current.source.value);
|
|
@@ -2609,12 +2614,33 @@ var import_utils13 = require("@typescript-eslint/utils");
|
|
|
2609
2614
|
// src/rules/_sql.ts
|
|
2610
2615
|
var import_utils12 = require("@typescript-eslint/utils");
|
|
2611
2616
|
function stripSqlNoise(text) {
|
|
2612
|
-
|
|
2617
|
+
return scanSqlNoise(text);
|
|
2618
|
+
}
|
|
2619
|
+
function sqlSingleQuotedRanges(text) {
|
|
2620
|
+
const ranges = [];
|
|
2621
|
+
scanSqlNoise(text, (start, end) => ranges.push([start, end]));
|
|
2622
|
+
return ranges;
|
|
2623
|
+
}
|
|
2624
|
+
function scanSqlNoise(text, onSingleQuoted) {
|
|
2625
|
+
const out = text.split("");
|
|
2613
2626
|
const n = text.length;
|
|
2614
2627
|
let i = 0;
|
|
2615
2628
|
while (i < n) {
|
|
2616
2629
|
const ch = text[i];
|
|
2630
|
+
if (ch === "$" && !/[\w$]/u.test(text[i - 1] ?? "")) {
|
|
2631
|
+
const delimiter = /^\$(?:[A-Za-z_][A-Za-z_0-9]*)?\$/u.exec(text.slice(i))?.[0];
|
|
2632
|
+
if (delimiter !== void 0) {
|
|
2633
|
+
const closing = text.indexOf(delimiter, i + delimiter.length);
|
|
2634
|
+
const end = closing < 0 ? n : closing + delimiter.length;
|
|
2635
|
+
while (i < end) {
|
|
2636
|
+
if (text[i] !== "\n") out[i] = " ";
|
|
2637
|
+
i += 1;
|
|
2638
|
+
}
|
|
2639
|
+
continue;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2617
2642
|
if (ch === "'" || ch === '"') {
|
|
2643
|
+
const start = i;
|
|
2618
2644
|
out[i] = " ";
|
|
2619
2645
|
i += 1;
|
|
2620
2646
|
while (i < n) {
|
|
@@ -2628,6 +2654,7 @@ function stripSqlNoise(text) {
|
|
|
2628
2654
|
}
|
|
2629
2655
|
out[i] = " ";
|
|
2630
2656
|
i += 1;
|
|
2657
|
+
if (ch === "'") onSingleQuoted?.(start, i);
|
|
2631
2658
|
break;
|
|
2632
2659
|
}
|
|
2633
2660
|
if (c !== "\n") {
|
|
@@ -2648,17 +2675,20 @@ function stripSqlNoise(text) {
|
|
|
2648
2675
|
out[i] = " ";
|
|
2649
2676
|
out[i + 1] = " ";
|
|
2650
2677
|
i += 2;
|
|
2651
|
-
|
|
2678
|
+
let depth = 1;
|
|
2679
|
+
while (i < n && depth > 0) {
|
|
2680
|
+
if (text[i] === "/" && text[i + 1] === "*" || text[i] === "*" && text[i + 1] === "/") {
|
|
2681
|
+
depth += text[i] === "/" ? 1 : -1;
|
|
2682
|
+
out[i] = " ";
|
|
2683
|
+
out[i + 1] = " ";
|
|
2684
|
+
i += 2;
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2652
2687
|
if (text[i] !== "\n") {
|
|
2653
2688
|
out[i] = " ";
|
|
2654
2689
|
}
|
|
2655
2690
|
i += 1;
|
|
2656
2691
|
}
|
|
2657
|
-
if (i < n) {
|
|
2658
|
-
out[i] = " ";
|
|
2659
|
-
out[i + 1] = " ";
|
|
2660
|
-
i += 2;
|
|
2661
|
-
}
|
|
2662
2692
|
continue;
|
|
2663
2693
|
}
|
|
2664
2694
|
i += 1;
|
|
@@ -2756,8 +2786,9 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
|
|
|
2756
2786
|
remediation: "Use SQL placeholders and pass runtime values through the driver's binding API.",
|
|
2757
2787
|
category: "security",
|
|
2758
2788
|
limitations: [
|
|
2759
|
-
"
|
|
2760
|
-
"
|
|
2789
|
+
"Only single-quoted SQL values are inspected. Double-quoted identifiers, comments, dollar strings, and unquoted fragments are excluded; this is not a general SQL injection detector.",
|
|
2790
|
+
"Literal fragments, legacy uppercase fragment names, and parameterizing tagged templates are exempt; uppercase spelling does not prove a value is static.",
|
|
2791
|
+
"The bounded lexer recognizes doubled quotes, comments, and PostgreSQL dollar strings; dialect-specific escape modes and SQL generated through other APIs require separate security review."
|
|
2761
2792
|
],
|
|
2762
2793
|
examples: [
|
|
2763
2794
|
{
|
|
@@ -2798,15 +2829,23 @@ function isStaticFragment(expression) {
|
|
|
2798
2829
|
return false;
|
|
2799
2830
|
}
|
|
2800
2831
|
function runtimeInterpolations(template) {
|
|
2832
|
+
const parts = template.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw);
|
|
2833
|
+
const ranges = sqlSingleQuotedRanges(parts.join(RUNTIME_MARKER));
|
|
2834
|
+
let offset = 0;
|
|
2801
2835
|
return template.expressions.filter(
|
|
2802
|
-
(expression, index) =>
|
|
2836
|
+
(expression, index) => {
|
|
2837
|
+
offset += parts[index]?.length ?? 0;
|
|
2838
|
+
const inValue = ranges.some(([start, end]) => start < offset && offset < end);
|
|
2839
|
+
offset += RUNTIME_MARKER.length;
|
|
2840
|
+
return inValue && !isStaticFragment(expression) && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "");
|
|
2841
|
+
}
|
|
2803
2842
|
);
|
|
2804
2843
|
}
|
|
2805
2844
|
function endsWithSqlQuote(text) {
|
|
2806
|
-
return /
|
|
2845
|
+
return /'\s*$/u.test(text);
|
|
2807
2846
|
}
|
|
2808
2847
|
function startsWithSqlQuote(text) {
|
|
2809
|
-
return /^\s*
|
|
2848
|
+
return /^\s*'/u.test(text);
|
|
2810
2849
|
}
|
|
2811
2850
|
function staticLiteralText(node) {
|
|
2812
2851
|
if (node.type === import_utils13.AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
@@ -2828,7 +2867,13 @@ function runtimeConcatOperands(node) {
|
|
|
2828
2867
|
if (!hasStringLiteral) {
|
|
2829
2868
|
return [];
|
|
2830
2869
|
}
|
|
2870
|
+
const parts = operands.map((operand) => staticLiteralText(operand) ?? RUNTIME_MARKER);
|
|
2871
|
+
const ranges = sqlSingleQuotedRanges(parts.join(""));
|
|
2872
|
+
let offset = 0;
|
|
2831
2873
|
return operands.filter((operand, index) => {
|
|
2874
|
+
const inValue = ranges.some(([start, end]) => start < offset && offset < end);
|
|
2875
|
+
offset += parts[index]?.length ?? 0;
|
|
2876
|
+
if (!inValue) return false;
|
|
2832
2877
|
if (isStaticFragment(operand)) return false;
|
|
2833
2878
|
const before = operands[index - 1];
|
|
2834
2879
|
const after = operands[index + 1];
|
|
@@ -2921,9 +2966,11 @@ var no_dynamic_sql_default = createRule({
|
|
|
2921
2966
|
var import_utils14 = require("@typescript-eslint/utils");
|
|
2922
2967
|
var NO_ENUM_DOCUMENTATION = {
|
|
2923
2968
|
summary: "Disallow TypeScript `enum`; use string-literal unions or `as const` objects instead.",
|
|
2924
|
-
rationale: "
|
|
2925
|
-
remediation: "
|
|
2969
|
+
rationale: "Literal unions keep type-only domains explicit, while constant objects make runtime values deliberate. This policy also avoids compiler-dependent const-enum inlining contracts.",
|
|
2970
|
+
remediation: "Use a literal union or an `as const` object after checking runtime member access, numeric reverse mappings, serialized values, and public consumers.",
|
|
2926
2971
|
category: "maintainability",
|
|
2972
|
+
limitations: ["This is an explicit style policy for regular and const enums, not a claim that every enum emits an object. Generated files and configured exclusions are preserved; migration is manual."],
|
|
2973
|
+
references: ["https://www.typescriptlang.org/docs/handbook/enums.html"],
|
|
2927
2974
|
examples: [
|
|
2928
2975
|
{
|
|
2929
2976
|
id: "string-literal-union",
|
|
@@ -3003,17 +3050,17 @@ var no_enum_default = createRule({
|
|
|
3003
3050
|
// src/rules/no-fat-try-blocks.ts
|
|
3004
3051
|
var import_utils15 = require("@typescript-eslint/utils");
|
|
3005
3052
|
var NO_FAT_TRY_BLOCKS_DOCUMENTATION = {
|
|
3006
|
-
summary: "
|
|
3053
|
+
summary: "Review try blocks exceeding the configured count of syntactically selected operations.",
|
|
3007
3054
|
rationale: "A broad `try` block obscures which operation failed and encourages one catch clause to recover from unrelated errors.",
|
|
3008
3055
|
remediation: "Keep only the operations that share one recovery policy inside the `try` block and move other work outside it.",
|
|
3009
3056
|
category: "correctness",
|
|
3010
3057
|
limitations: [
|
|
3011
|
-
"The
|
|
3058
|
+
"The default threshold is three selected top-level operations, not a proof of every possible throw. A shared recovery policy may legitimately cover several operations; generated files, catchless finally blocks, rethrows, and terminal error boundaries are excluded."
|
|
3012
3059
|
],
|
|
3013
3060
|
examples: [
|
|
3014
3061
|
{
|
|
3015
3062
|
id: "focused-try-block",
|
|
3016
|
-
title: "
|
|
3063
|
+
title: "Three selected operations stay within the default threshold",
|
|
3017
3064
|
outcome: "no-match",
|
|
3018
3065
|
files: [{
|
|
3019
3066
|
path: "src/load.ts",
|
|
@@ -3025,7 +3072,7 @@ var NO_FAT_TRY_BLOCKS_DOCUMENTATION = {
|
|
|
3025
3072
|
},
|
|
3026
3073
|
{
|
|
3027
3074
|
id: "broad-try-block",
|
|
3028
|
-
title: "
|
|
3075
|
+
title: "Review whether four selected operations share one recovery policy",
|
|
3029
3076
|
outcome: "match",
|
|
3030
3077
|
files: [{
|
|
3031
3078
|
path: "src/load.ts",
|
|
@@ -3395,7 +3442,7 @@ var no_fat_try_blocks_default = createRule({
|
|
|
3395
3442
|
meta: {
|
|
3396
3443
|
type: "problem",
|
|
3397
3444
|
docs: {
|
|
3398
|
-
description: "
|
|
3445
|
+
description: "Review try blocks exceeding the configured count of syntactically selected operations."
|
|
3399
3446
|
},
|
|
3400
3447
|
schema: [
|
|
3401
3448
|
{
|
|
@@ -3407,7 +3454,7 @@ var no_fat_try_blocks_default = createRule({
|
|
|
3407
3454
|
}
|
|
3408
3455
|
],
|
|
3409
3456
|
messages: {
|
|
3410
|
-
fatTryBlock: "This `try` block has {{count}}
|
|
3457
|
+
fatTryBlock: "This `try` block has {{count}} syntactically selected operations (max {{max}}). Review whether they share one recovery policy; move unrelated work outside the boundary."
|
|
3411
3458
|
}
|
|
3412
3459
|
},
|
|
3413
3460
|
defaultOptions: [{ max: MAX_TRY_BODY_STATEMENTS }],
|
|
@@ -3454,7 +3501,8 @@ var NO_HAND_ROLLED_SLEEP_DOCUMENTATION = {
|
|
|
3454
3501
|
remediation: "Use `node:timers/promises` with an abort signal for delays, or pass `AbortSignal.timeout(...)` to the timed operation.",
|
|
3455
3502
|
category: "correctness",
|
|
3456
3503
|
limitations: [
|
|
3457
|
-
"The rule skips tests, scripts, generated files, and client modules by default, and supports explicit path exemptions."
|
|
3504
|
+
"The rule skips tests, scripts, generated files, and client modules by default, and supports explicit path exemptions.",
|
|
3505
|
+
"Locally shadowed constructors/timers and value-returning timers are excluded. Only recognized browser markers are excluded; choose a runtime-compatible cancellation API for other browser modules."
|
|
3458
3506
|
],
|
|
3459
3507
|
examples: [
|
|
3460
3508
|
{
|
|
@@ -3598,6 +3646,21 @@ var no_hand_rolled_sleep_default = createRule({
|
|
|
3598
3646
|
return {};
|
|
3599
3647
|
}
|
|
3600
3648
|
const checkClientModules = optionsArg?.checkClientModules ?? false;
|
|
3649
|
+
const bindingOf = (identifier) => import_utils16.ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
3650
|
+
const isGlobal = (identifier) => (bindingOf(identifier)?.defs.length ?? 0) === 0;
|
|
3651
|
+
const isBuiltinTimer = (callee) => {
|
|
3652
|
+
if (!isSetTimeoutCallee(callee)) return false;
|
|
3653
|
+
if (callee.type === import_utils16.AST_NODE_TYPES.MemberExpression && callee.object.type === import_utils16.AST_NODE_TYPES.Identifier) return isGlobal(callee.object);
|
|
3654
|
+
if (callee.type !== import_utils16.AST_NODE_TYPES.Identifier) return false;
|
|
3655
|
+
const binding = bindingOf(callee);
|
|
3656
|
+
return binding === null || binding.defs.length === 0 || binding.defs.every((definition) => definition.node.type === import_utils16.AST_NODE_TYPES.ImportSpecifier && definition.node.imported.type === import_utils16.AST_NODE_TYPES.Identifier && definition.node.imported.name === "setTimeout" && definition.node.parent.type === import_utils16.AST_NODE_TYPES.ImportDeclaration && ["node:timers", "timers"].includes(String(definition.node.parent.source.value)));
|
|
3657
|
+
};
|
|
3658
|
+
const settlesParameter = (callback, executor, index) => {
|
|
3659
|
+
const parameter = executor.params[index];
|
|
3660
|
+
if (parameter?.type !== import_utils16.AST_NODE_TYPES.Identifier) return false;
|
|
3661
|
+
const callee = callback.type === import_utils16.AST_NODE_TYPES.Identifier ? callback : callback.type === import_utils16.AST_NODE_TYPES.ArrowFunctionExpression || callback.type === import_utils16.AST_NODE_TYPES.FunctionExpression ? soleCall(callback)?.callee : null;
|
|
3662
|
+
return callee?.type === import_utils16.AST_NODE_TYPES.Identifier && bindingOf(callee) === bindingOf(parameter);
|
|
3663
|
+
};
|
|
3601
3664
|
function isClientModule2() {
|
|
3602
3665
|
if (/\.[cm]?[jt]sx$/.test(filename)) {
|
|
3603
3666
|
return true;
|
|
@@ -3623,7 +3686,7 @@ var no_hand_rolled_sleep_default = createRule({
|
|
|
3623
3686
|
};
|
|
3624
3687
|
return {
|
|
3625
3688
|
NewExpression(node) {
|
|
3626
|
-
if (node.callee.type !== import_utils16.AST_NODE_TYPES.Identifier || node.callee.name !== "Promise") {
|
|
3689
|
+
if (node.callee.type !== import_utils16.AST_NODE_TYPES.Identifier || node.callee.name !== "Promise" || !isGlobal(node.callee)) {
|
|
3627
3690
|
return;
|
|
3628
3691
|
}
|
|
3629
3692
|
const executor = node.arguments[0];
|
|
@@ -3631,7 +3694,7 @@ var no_hand_rolled_sleep_default = createRule({
|
|
|
3631
3694
|
return;
|
|
3632
3695
|
}
|
|
3633
3696
|
const call = soleCall(executor);
|
|
3634
|
-
if (call === null || !
|
|
3697
|
+
if (call === null || !isBuiltinTimer(call.callee)) {
|
|
3635
3698
|
return;
|
|
3636
3699
|
}
|
|
3637
3700
|
const [callback, delay] = call.arguments;
|
|
@@ -3639,14 +3702,14 @@ var no_hand_rolled_sleep_default = createRule({
|
|
|
3639
3702
|
return;
|
|
3640
3703
|
}
|
|
3641
3704
|
const resolveName = parameterName(executor, 0);
|
|
3642
|
-
if (resolveName !== null && settlesWithoutValue(callback, resolveName)) {
|
|
3705
|
+
if (resolveName !== null && call.arguments.length === 2 && settlesWithoutValue(callback, resolveName) && settlesParameter(callback, executor, 0)) {
|
|
3643
3706
|
if (reportsSleepHere()) {
|
|
3644
3707
|
context.report({ node, messageId: "handRolledSleep" });
|
|
3645
3708
|
}
|
|
3646
3709
|
return;
|
|
3647
3710
|
}
|
|
3648
3711
|
const rejectName = parameterName(executor, 1);
|
|
3649
|
-
if (rejectName !== null && isRaceArm(node) && rejectsInCallback(callback, rejectName)) {
|
|
3712
|
+
if (rejectName !== null && isRaceArm(node) && settlesParameter(callback, executor, 1) && rejectsInCallback(callback, rejectName)) {
|
|
3650
3713
|
context.report({ node, messageId: "handRolledTimeoutRace" });
|
|
3651
3714
|
}
|
|
3652
3715
|
}
|
|
@@ -3745,7 +3808,7 @@ var NO_INSECURE_RANDOM_ID_DOCUMENTATION = {
|
|
|
3745
3808
|
rationale: "Math.random is predictable and lacks the entropy required for security-sensitive values.",
|
|
3746
3809
|
remediation: "Generate the value with crypto.randomUUID or crypto.getRandomValues.",
|
|
3747
3810
|
category: "security",
|
|
3748
|
-
limitations: ["
|
|
3811
|
+
limitations: ["Names select security-sensitive bindings heuristically; they do not prove sensitivity. Sampling in unrelated bindings or branch tests, locally shadowed Math objects, ambiguous identifiers and test files are excluded. This is not interprocedural data-flow analysis."],
|
|
3749
3812
|
examples: [
|
|
3750
3813
|
{ id: "cryptographic-id", title: "Use the Web Crypto API", outcome: "no-match", files: [{ path: "src/session.ts", source: "const sessionToken = crypto.randomUUID();" }], focusPath: "src/session.ts", expectedCount: 0, public: true },
|
|
3751
3814
|
{ id: "predictable-token", title: "Do not derive a token from Math.random", outcome: "match", files: [{ path: "src/session.ts", source: "const sessionToken = Math.random();" }], focusPath: "src/session.ts", expectedCount: 1, public: true }
|
|
@@ -3826,6 +3889,7 @@ function findEnclosingNames(node) {
|
|
|
3826
3889
|
if (directBinding && parent.id.type === "Identifier") {
|
|
3827
3890
|
names.push(parent.id.name);
|
|
3828
3891
|
}
|
|
3892
|
+
return names;
|
|
3829
3893
|
}
|
|
3830
3894
|
if (parent.type === "Property" && parent.value === current) {
|
|
3831
3895
|
const key = parent.key;
|
|
@@ -3860,7 +3924,7 @@ function findEnclosingNames(node) {
|
|
|
3860
3924
|
if (directBinding && parent.id !== null) names.push(parent.id.name);
|
|
3861
3925
|
return names;
|
|
3862
3926
|
}
|
|
3863
|
-
if (parent.type === "ExpressionStatement") {
|
|
3927
|
+
if (parent.type === "ExpressionStatement" || parent.type === "IfStatement" || parent.type === "ForStatement" || parent.type === "WhileStatement" || parent.type === "DoWhileStatement" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression") {
|
|
3864
3928
|
return names;
|
|
3865
3929
|
}
|
|
3866
3930
|
current = parent;
|
|
@@ -3952,6 +4016,7 @@ var no_insecure_random_id_default = createRule({
|
|
|
3952
4016
|
if (!isMathRandomCall(node)) {
|
|
3953
4017
|
return;
|
|
3954
4018
|
}
|
|
4019
|
+
if ((import_utils18.ASTUtils.findVariable(context.sourceCode.getScope(node), "Math")?.defs.length ?? 0) > 0) return;
|
|
3955
4020
|
const names = findEnclosingNames(node);
|
|
3956
4021
|
if (names.some(isStrongSecurityName)) {
|
|
3957
4022
|
context.report({ node, messageId: "insecureRandomId" });
|
|
@@ -4816,6 +4881,39 @@ var no_log_only_catch_default = createRule({
|
|
|
4816
4881
|
const matcher = createLogMatcher(loggingOptions);
|
|
4817
4882
|
const filename = context.filename;
|
|
4818
4883
|
const sourceCode = context.sourceCode;
|
|
4884
|
+
function hasCoercionValidation(node) {
|
|
4885
|
+
const owner = node.parent;
|
|
4886
|
+
const statement = owner.block.body[0];
|
|
4887
|
+
if (node.body.body.length !== 0 || owner.finalizer !== null || owner.block.body.length !== 1 || statement?.type !== import_utils24.AST_NODE_TYPES.ExpressionStatement || statement.expression.type !== import_utils24.AST_NODE_TYPES.AssignmentExpression || statement.expression.operator !== "=") return false;
|
|
4888
|
+
const { left, right } = statement.expression;
|
|
4889
|
+
if (left.type !== import_utils24.AST_NODE_TYPES.MemberExpression || left.computed || left.object.type !== import_utils24.AST_NODE_TYPES.Identifier || right.type !== import_utils24.AST_NODE_TYPES.CallExpression || right.optional || right.callee.type !== import_utils24.AST_NODE_TYPES.Identifier || !["String", "Number", "Boolean", "BigInt"].includes(right.callee.name) || right.arguments.length !== 1) return false;
|
|
4890
|
+
const argument = right.arguments[0];
|
|
4891
|
+
if (argument === void 0 || sourceCode.getText(left) !== sourceCode.getText(argument)) return false;
|
|
4892
|
+
const global = import_utils24.ASTUtils.findVariable(sourceCode.getScope(right.callee), right.callee.name);
|
|
4893
|
+
if (global !== null && global.defs.length > 0) return false;
|
|
4894
|
+
const root = import_utils24.ASTUtils.findVariable(sourceCode.getScope(left.object), left.object.name);
|
|
4895
|
+
if (root === null || root.references.some((reference) => reference.isWrite() && !reference.init)) return false;
|
|
4896
|
+
let current = owner;
|
|
4897
|
+
let slot = statementSlot(current);
|
|
4898
|
+
while (slot === null && current.parent !== void 0 && !FUNCTION_TYPES2.has(current.parent.type)) {
|
|
4899
|
+
current = current.parent;
|
|
4900
|
+
slot = statementSlot(current);
|
|
4901
|
+
}
|
|
4902
|
+
let next = slot?.list[slot.index + 1];
|
|
4903
|
+
let target = sourceCode.getText(left);
|
|
4904
|
+
if (next?.type === import_utils24.AST_NODE_TYPES.VariableDeclaration && next.kind === "const" && next.declarations.length === 1) {
|
|
4905
|
+
const alias = next.declarations[0];
|
|
4906
|
+
if (alias?.id.type !== import_utils24.AST_NODE_TYPES.Identifier || alias.init === null || sourceCode.getText(alias.init) !== target) return false;
|
|
4907
|
+
target = alias.id.name;
|
|
4908
|
+
next = slot?.list[slot.index + 2];
|
|
4909
|
+
}
|
|
4910
|
+
if (next?.type !== import_utils24.AST_NODE_TYPES.IfStatement) return false;
|
|
4911
|
+
let condition = next.test;
|
|
4912
|
+
while (condition.type === import_utils24.AST_NODE_TYPES.LogicalExpression && condition.operator === "&&") condition = condition.left;
|
|
4913
|
+
if (condition.type !== import_utils24.AST_NODE_TYPES.BinaryExpression || !["==", "==="].includes(condition.operator)) return false;
|
|
4914
|
+
const test = condition.left;
|
|
4915
|
+
return test.type === import_utils24.AST_NODE_TYPES.UnaryExpression && test.operator === "typeof" && sourceCode.getText(test.argument) === target && condition.right.type === import_utils24.AST_NODE_TYPES.Literal && condition.right.value === right.callee.name.toLowerCase();
|
|
4916
|
+
}
|
|
4819
4917
|
function isLoggingCallStatement(statement) {
|
|
4820
4918
|
if (statement.type !== "ExpressionStatement") {
|
|
4821
4919
|
return false;
|
|
@@ -4842,17 +4940,11 @@ var no_log_only_catch_default = createRule({
|
|
|
4842
4940
|
CatchClause(node) {
|
|
4843
4941
|
const statements = node.body.body;
|
|
4844
4942
|
const isDocumented = sourceCode.getCommentsInside(node.body).length > 0 || hasAdjacentRationale(node);
|
|
4845
|
-
if (
|
|
4846
|
-
if (isDocumented) {
|
|
4847
|
-
return;
|
|
4848
|
-
}
|
|
4849
|
-
if (fallbackFollowsTry(node.parent) || seededFallbackHandled(node.parent, sourceCode.getScope(node))) {
|
|
4850
|
-
return;
|
|
4851
|
-
}
|
|
4852
|
-
context.report({ node, messageId: "emptyCatch" });
|
|
4943
|
+
if (isDocumented || hasCoercionValidation(node) || fallbackFollowsTry(node.parent) || seededFallbackHandled(node.parent, sourceCode.getScope(node))) {
|
|
4853
4944
|
return;
|
|
4854
4945
|
}
|
|
4855
|
-
if (
|
|
4946
|
+
if (statements.length === 0) {
|
|
4947
|
+
context.report({ node, messageId: "emptyCatch" });
|
|
4856
4948
|
return;
|
|
4857
4949
|
}
|
|
4858
4950
|
const everyStatementIsLogging = statements.every(
|
|
@@ -4870,7 +4962,7 @@ var no_log_only_catch_default = createRule({
|
|
|
4870
4962
|
var import_utils25 = require("@typescript-eslint/utils");
|
|
4871
4963
|
var NO_BARE_RETURN_FROM_TEST_CATCH_DOCUMENTATION = {
|
|
4872
4964
|
summary: "Disallow a bare return from a test catch block when it skips a later assertion.",
|
|
4873
|
-
rationale: "
|
|
4965
|
+
rationale: "An unasserted catch return can swallow a failure and skip later assertions; the complete test result also depends on other assertions and hooks.",
|
|
4874
4966
|
remediation: "Rethrow the error, assert on it, or use the runner's explicit skip mechanism when the capability is optional.",
|
|
4875
4967
|
category: "testing",
|
|
4876
4968
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
@@ -4957,7 +5049,7 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
4957
5049
|
type: "problem",
|
|
4958
5050
|
docs: { description: "Disallow a bare return from a test catch block when it skips a later assertion." },
|
|
4959
5051
|
schema: [],
|
|
4960
|
-
messages: { bareReturnFromTestCatch: "This bare return
|
|
5052
|
+
messages: { bareReturnFromTestCatch: "This bare return can swallow the caught failure and skips a later assertion. Rethrow, assert on the error, or explicitly skip the test." }
|
|
4961
5053
|
},
|
|
4962
5054
|
defaultOptions: [],
|
|
4963
5055
|
create(context) {
|
|
@@ -4976,6 +5068,23 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
4976
5068
|
if (current === null || current === void 0) break;
|
|
4977
5069
|
}
|
|
4978
5070
|
if (catchClause === null || catchClause.parent.finalizer !== null) return;
|
|
5071
|
+
const parameter = catchClause.param;
|
|
5072
|
+
if (parameter?.type === import_utils25.AST_NODE_TYPES.Identifier && node.parent === catchClause.body) {
|
|
5073
|
+
const errorBinding = import_utils25.ASTUtils.findVariable(context.sourceCode.getScope(parameter), parameter.name);
|
|
5074
|
+
const assertedError = catchClause.body.body.some((statement) => {
|
|
5075
|
+
if (statement.range[1] >= node.range[0] || statement.type !== import_utils25.AST_NODE_TYPES.ExpressionStatement) return false;
|
|
5076
|
+
const expression = statement.expression;
|
|
5077
|
+
if (expression.type !== import_utils25.AST_NODE_TYPES.CallExpression || !isAssertion(expression, context)) return false;
|
|
5078
|
+
const root = rootIdentifier2(expression.callee);
|
|
5079
|
+
if (root === null) return false;
|
|
5080
|
+
const assertionName = importedName3(root, context, ASSERTION_MODULES);
|
|
5081
|
+
let operand = expression.callee.type === import_utils25.AST_NODE_TYPES.MemberExpression ? expression.callee.object : null;
|
|
5082
|
+
if (operand?.type === import_utils25.AST_NODE_TYPES.MemberExpression && staticMemberName2(operand) === "not") operand = operand.object;
|
|
5083
|
+
if (assertionName !== "assert" && (assertionName !== "expect" || operand?.type !== import_utils25.AST_NODE_TYPES.CallExpression || operand.callee !== root)) return false;
|
|
5084
|
+
return walkOwnScope(expression, (current) => current.type === import_utils25.AST_NODE_TYPES.Identifier && errorBinding?.references.some((reference) => reference.identifier === current) === true);
|
|
5085
|
+
});
|
|
5086
|
+
if (assertedError) return;
|
|
5087
|
+
}
|
|
4979
5088
|
if (walkOwnScope(catchClause.body, (current) => current.type === import_utils25.AST_NODE_TYPES.ThrowStatement || isExplicitSkip(current, context))) return;
|
|
4980
5089
|
if (!walkOwnScope(owner.body, (current) => current.range[0] > node.range[1] && isAssertion(current, context))) return;
|
|
4981
5090
|
context.report({ node, messageId: "bareReturnFromTestCatch" });
|
|
@@ -4987,13 +5096,13 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
4987
5096
|
// src/rules/no-bespoke-api-case-conversion.ts
|
|
4988
5097
|
var import_utils26 = require("@typescript-eslint/utils");
|
|
4989
5098
|
var NO_BESPOKE_API_CASE_CONVERSION_DOCUMENTATION = {
|
|
4990
|
-
summary: "
|
|
4991
|
-
rationale: "
|
|
5099
|
+
summary: "Review direct snake_case/camelCase mirror mappings on explicitly API-typed adapter values.",
|
|
5100
|
+
rationale: "Duplicating wire-name translation can drift from an API client contract. When the SDK owns application-facing names, centralizing conversion avoids maintaining another mirror by hand.",
|
|
4992
5101
|
remediation: "Move wire-name ownership and case conversion into the generated SDK/model layer; keep application adapters on the generated typed surface.",
|
|
4993
5102
|
category: "architecture",
|
|
4994
5103
|
autofix: "none",
|
|
4995
5104
|
limitations: [
|
|
4996
|
-
"Only
|
|
5105
|
+
"Only adapter/adapters files and receivers explicitly annotated with a scope-resolved type imported from an API/client/SDK/contract/generated module are checked. Unrelated imports, local type shadows, reassigned receivers and inferred receiver types are excluded.",
|
|
4997
5106
|
"Only object properties that directly translate the same identifier between snake_case and lowerCamelCase are reported.",
|
|
4998
5107
|
"Quoted/computed protocol keys, generated/vendor code, tests, fixtures, and indirect conversions are intentionally excluded."
|
|
4999
5108
|
],
|
|
@@ -5065,7 +5174,7 @@ var no_bespoke_api_case_conversion_default = createRule({
|
|
|
5065
5174
|
docs: { description: NO_BESPOKE_API_CASE_CONVERSION_DOCUMENTATION.summary },
|
|
5066
5175
|
schema: [],
|
|
5067
5176
|
messages: {
|
|
5068
|
-
noBespokeApiCaseConversion: "This API adapter
|
|
5177
|
+
noBespokeApiCaseConversion: "This API-typed adapter value mirrors `{{wireName}}` and `{{applicationName}}`. If the SDK owns application-facing names, move this conversion to its model boundary."
|
|
5069
5178
|
}
|
|
5070
5179
|
},
|
|
5071
5180
|
defaultOptions: [],
|
|
@@ -5075,16 +5184,33 @@ var no_bespoke_api_case_conversion_default = createRule({
|
|
|
5075
5184
|
if (!ADAPTER_BASENAME_RE.test(basename) || isGeneratedFile(filename, context.sourceCode.text) || isTestFile(filename, ["fixtureTree"])) {
|
|
5076
5185
|
return {};
|
|
5077
5186
|
}
|
|
5078
|
-
const
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5187
|
+
const hasApiReceiver = (value) => {
|
|
5188
|
+
let current = value;
|
|
5189
|
+
while (true) {
|
|
5190
|
+
if (current.type === import_utils26.AST_NODE_TYPES.MemberExpression) current = current.object;
|
|
5191
|
+
else if (current.type === import_utils26.AST_NODE_TYPES.TSAsExpression || current.type === import_utils26.AST_NODE_TYPES.TSNonNullExpression || current.type === import_utils26.AST_NODE_TYPES.TSTypeAssertion) current = current.expression;
|
|
5192
|
+
else break;
|
|
5193
|
+
}
|
|
5194
|
+
if (current.type !== import_utils26.AST_NODE_TYPES.Identifier) return false;
|
|
5195
|
+
const binding = import_utils26.ASTUtils.findVariable(context.sourceCode.getScope(current), current.name);
|
|
5196
|
+
if (binding?.defs.length !== 1 || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
5197
|
+
const identifier = binding.defs[0]?.name;
|
|
5198
|
+
if (identifier?.type !== import_utils26.AST_NODE_TYPES.Identifier) return false;
|
|
5199
|
+
const annotation = identifier.typeAnnotation?.typeAnnotation;
|
|
5200
|
+
if (annotation?.type !== import_utils26.AST_NODE_TYPES.TSTypeReference) return false;
|
|
5201
|
+
let typeName = annotation.typeName;
|
|
5202
|
+
while (typeName.type === import_utils26.AST_NODE_TYPES.TSQualifiedName) typeName = typeName.left;
|
|
5203
|
+
if (typeName.type !== import_utils26.AST_NODE_TYPES.Identifier) return false;
|
|
5204
|
+
const typeBinding = import_utils26.ASTUtils.findVariable(context.sourceCode.getScope(typeName), typeName.name);
|
|
5205
|
+
return typeBinding?.defs.length === 1 && typeBinding.defs[0]?.type === "ImportBinding" && typeBinding.defs[0].parent.type === import_utils26.AST_NODE_TYPES.ImportDeclaration && API_BOUNDARY_IMPORT_RE.test(typeBinding.defs[0].parent.source.value);
|
|
5206
|
+
};
|
|
5082
5207
|
return {
|
|
5083
5208
|
Property(node) {
|
|
5084
5209
|
if (node.computed || node.method || node.shorthand) return;
|
|
5085
5210
|
const key = propertyName(node.key);
|
|
5086
5211
|
const value = memberName3(node.value);
|
|
5087
5212
|
if (key === null || value === null || !isDirectCaseTranslation(key, value)) return;
|
|
5213
|
+
if (!hasApiReceiver(node.value)) return;
|
|
5088
5214
|
const wireName = SNAKE_CASE_RE.test(key) ? key : value;
|
|
5089
5215
|
const applicationName = wireName === key ? value : key;
|
|
5090
5216
|
context.report({
|
|
@@ -5207,7 +5333,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
|
|
|
5207
5333
|
limitations: [
|
|
5208
5334
|
"Only ESLint disable comments and TypeScript expect-error directives are checked.",
|
|
5209
5335
|
"The rule uses a small anchored vocabulary and does not score prose quality generally.",
|
|
5210
|
-
"Generated files and descriptions
|
|
5336
|
+
"Generated files and descriptions outside that exact vocabulary are excluded; an unflagged reason is not proof of a justified suppression. Missing descriptions remain owned by the upstream description requirement."
|
|
5211
5337
|
],
|
|
5212
5338
|
examples: [
|
|
5213
5339
|
{
|
|
@@ -5217,7 +5343,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
|
|
|
5217
5343
|
files: [
|
|
5218
5344
|
{
|
|
5219
5345
|
path: "src/adapter.ts",
|
|
5220
|
-
source: "// @ts-expect-error -- vendor types omit the runtime requestId field\
|
|
5346
|
+
source: "function requestId(response: object) {\n // @ts-expect-error -- vendor types omit the runtime requestId field\n return response.requestId;\n}"
|
|
5221
5347
|
}
|
|
5222
5348
|
],
|
|
5223
5349
|
focusPath: "src/adapter.ts",
|
|
@@ -5231,7 +5357,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
|
|
|
5231
5357
|
files: [
|
|
5232
5358
|
{
|
|
5233
5359
|
path: "src/adapter.ts",
|
|
5234
|
-
source: "// @ts-expect-error -- false positive\
|
|
5360
|
+
source: "function requestId(response: object) {\n // @ts-expect-error -- false positive\n return response.requestId;\n}"
|
|
5235
5361
|
}
|
|
5236
5362
|
],
|
|
5237
5363
|
focusPath: "src/adapter.ts",
|
|
@@ -5464,11 +5590,11 @@ var no_generic_single_export_module_default = createRule({
|
|
|
5464
5590
|
// src/rules/no-offset-pagination.ts
|
|
5465
5591
|
var import_utils29 = require("@typescript-eslint/utils");
|
|
5466
5592
|
var NO_OFFSET_PAGINATION_DOCUMENTATION = {
|
|
5467
|
-
summary: "
|
|
5593
|
+
summary: "Prefer keyset pagination for embedded SQL queries using OFFSET.",
|
|
5468
5594
|
rationale: "Offset pagination scans skipped rows and shifts page boundaries under concurrent writes.",
|
|
5469
|
-
remediation: "
|
|
5595
|
+
remediation: "Consider a keyset cursor that preserves the query's complete ordering, tie-breakers, and filters.",
|
|
5470
5596
|
category: "performance",
|
|
5471
|
-
limitations: ["
|
|
5597
|
+
limitations: ["A SELECT/FROM query shape or adjacent LIMIT/OFFSET fragment is required. Isolated OFFSET fragments and test files are excluded; this lexical context does not prove a database execution sink. Performance and concurrent-write behavior depend on indexes, ordering, isolation, and dialect; bounded pages and random page access can justify OFFSET."],
|
|
5472
5598
|
examples: [
|
|
5473
5599
|
{ id: "keyset-pagination", title: "Page from a stable cursor", outcome: "no-match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT id FROM runs WHERE id > ? ORDER BY id LIMIT ?`).all();" }], focusPath: "src/runs.ts", expectedCount: 0, public: true },
|
|
5474
5600
|
{ id: "offset-pagination", title: "Do not page by offset", outcome: "match", files: [{ path: "src/runs.ts", source: "db.query(`SELECT id FROM runs ORDER BY id LIMIT ? OFFSET ?`);" }], focusPath: "src/runs.ts", expectedCount: 1, public: true }
|
|
@@ -5476,17 +5602,18 @@ var NO_OFFSET_PAGINATION_DOCUMENTATION = {
|
|
|
5476
5602
|
};
|
|
5477
5603
|
var OFFSET_PAGINATION = /\bOFFSET\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
|
|
5478
5604
|
var OFFSET_GATE = /offset/i;
|
|
5605
|
+
var PAGINATION_CONTEXT = /\bSELECT\b[\s\S]*\bFROM\b[\s\S]*\bOFFSET\b|\bLIMIT\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)\s+OFFSET\b/i;
|
|
5479
5606
|
var no_offset_pagination_default = createRule({
|
|
5480
5607
|
name: "no-offset-pagination",
|
|
5481
5608
|
documentation: NO_OFFSET_PAGINATION_DOCUMENTATION,
|
|
5482
5609
|
meta: {
|
|
5483
5610
|
type: "problem",
|
|
5484
5611
|
docs: {
|
|
5485
|
-
description: "
|
|
5612
|
+
description: "Prefer keyset pagination for embedded SQL queries using OFFSET."
|
|
5486
5613
|
},
|
|
5487
5614
|
schema: [],
|
|
5488
5615
|
messages: {
|
|
5489
|
-
noOffsetPagination: "OFFSET pagination
|
|
5616
|
+
noOffsetPagination: "Review OFFSET pagination for large or changing result sets. If a keyset cursor fits the access pattern, preserve the query's complete ordering, tie-breakers, and filters; bounded pages or random page access may justify OFFSET."
|
|
5490
5617
|
}
|
|
5491
5618
|
},
|
|
5492
5619
|
defaultOptions: [],
|
|
@@ -5495,7 +5622,7 @@ var no_offset_pagination_default = createRule({
|
|
|
5495
5622
|
return {};
|
|
5496
5623
|
}
|
|
5497
5624
|
return createSqlListener((sql, node) => {
|
|
5498
|
-
if (!OFFSET_PAGINATION.test(sql)) {
|
|
5625
|
+
if (!PAGINATION_CONTEXT.test(sql) || !OFFSET_PAGINATION.test(sql)) {
|
|
5499
5626
|
return;
|
|
5500
5627
|
}
|
|
5501
5628
|
context.report({ node, messageId: "noOffsetPagination" });
|
|
@@ -6526,7 +6653,7 @@ var NO_REPEATED_STRING_LITERAL_DOCUMENTATION = {
|
|
|
6526
6653
|
]
|
|
6527
6654
|
};
|
|
6528
6655
|
function isStructured(value) {
|
|
6529
|
-
return
|
|
6656
|
+
return SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value) || URL_PATH_RE.test(value);
|
|
6530
6657
|
}
|
|
6531
6658
|
function preview(value) {
|
|
6532
6659
|
const oneLine = value.replaceAll("\n", " ").trim();
|
|
@@ -6547,7 +6674,7 @@ function isScaffolding(node) {
|
|
|
6547
6674
|
}
|
|
6548
6675
|
const isNonComputedPropertyKey = (parent.type === import_utils35.AST_NODE_TYPES.Property || parent.type === import_utils35.AST_NODE_TYPES.PropertyDefinition || parent.type === import_utils35.AST_NODE_TYPES.MethodDefinition || parent.type === import_utils35.AST_NODE_TYPES.AccessorProperty) && parent.key === node && !parent.computed;
|
|
6549
6676
|
const isRequireSource = parent.type === import_utils35.AST_NODE_TYPES.CallExpression && parent.callee.type === import_utils35.AST_NODE_TYPES.Identifier && parent.callee.name === "require";
|
|
6550
|
-
return parent.type === import_utils35.AST_NODE_TYPES.ImportDeclaration || parent.type === import_utils35.AST_NODE_TYPES.ImportExpression || parent.type === import_utils35.AST_NODE_TYPES.ExportNamedDeclaration || parent.type === import_utils35.AST_NODE_TYPES.ExportAllDeclaration || parent.type === import_utils35.AST_NODE_TYPES.TSImportType || parent.type === import_utils35.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils35.AST_NODE_TYPES.TSLiteralType || isNonComputedPropertyKey || isRequireSource;
|
|
6677
|
+
return parent.type === import_utils35.AST_NODE_TYPES.ImportDeclaration || parent.type === import_utils35.AST_NODE_TYPES.ImportExpression || parent.type === import_utils35.AST_NODE_TYPES.ExportNamedDeclaration || parent.type === import_utils35.AST_NODE_TYPES.ExportAllDeclaration || parent.type === import_utils35.AST_NODE_TYPES.TSImportType || parent.type === import_utils35.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils35.AST_NODE_TYPES.JSXExpressionContainer && parent.parent.type === import_utils35.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils35.AST_NODE_TYPES.TSLiteralType || isNonComputedPropertyKey || isRequireSource;
|
|
6551
6678
|
}
|
|
6552
6679
|
var no_repeated_string_literal_default = createRule({
|
|
6553
6680
|
name: "no-repeated-string-literal",
|
|
@@ -7467,11 +7594,11 @@ var no_server_env_in_client_component_default = createRule({
|
|
|
7467
7594
|
// src/rules/no-select-star.ts
|
|
7468
7595
|
var import_utils41 = require("@typescript-eslint/utils");
|
|
7469
7596
|
var NO_SELECT_STAR_DOCUMENTATION = {
|
|
7470
|
-
summary: "
|
|
7597
|
+
summary: "Prefer explicit column projections over SELECT * in embedded SQL.",
|
|
7471
7598
|
rationale: "Wildcard projections couple row shape and query cost to unrelated schema changes.",
|
|
7472
7599
|
remediation: "List every required column explicitly in the projection.",
|
|
7473
7600
|
category: "correctness",
|
|
7474
|
-
limitations: ["Only statically visible embedded SQL is checked; function arguments such as COUNT(*) and stars inside EXISTS are excluded."],
|
|
7601
|
+
limitations: ["Only statically visible embedded SQL is checked; quoted strings (including PostgreSQL dollar strings), comments, function arguments such as COUNT(*), and stars inside EXISTS are excluded. This is a bounded lexical scan, not a complete SQL parser."],
|
|
7475
7602
|
examples: [
|
|
7476
7603
|
{ id: "explicit-projection", title: "Select the required columns", outcome: "no-match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT id, status FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 0, public: true },
|
|
7477
7604
|
{ id: "wildcard-projection", title: "Do not select every column", outcome: "match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT * FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 1, public: true }
|
|
@@ -7522,7 +7649,7 @@ var no_select_star_default = createRule({
|
|
|
7522
7649
|
meta: {
|
|
7523
7650
|
type: "problem",
|
|
7524
7651
|
docs: {
|
|
7525
|
-
description: "
|
|
7652
|
+
description: "Prefer explicit column projections over SELECT * in embedded SQL."
|
|
7526
7653
|
},
|
|
7527
7654
|
schema: [],
|
|
7528
7655
|
messages: {
|
|
@@ -8006,7 +8133,7 @@ var NO_SILENT_PROMISE_CATCH_DOCUMENTATION = {
|
|
|
8006
8133
|
rationale: "A swallowed rejection hides failures and gives callers an indistinguishable fallback value.",
|
|
8007
8134
|
remediation: "Log, rethrow, or explicitly recover from the rejection; explain intentional teardown suppression.",
|
|
8008
8135
|
category: "correctness",
|
|
8009
|
-
limitations: ["Test files, teardown calls, explanatory comments, non-function handlers, and handlers that consume or report the error are excluded."],
|
|
8136
|
+
limitations: ["Test files, teardown calls, explanatory comments, non-function handlers, and handlers that consume or report the error are excluded.", "Recognized imported Zod construction chains and their stable local aliases are excluded. Other untyped catch-like APIs are not proven to be Promises."],
|
|
8010
8137
|
examples: [
|
|
8011
8138
|
{ id: "reported-rejection", title: "Report the rejection", outcome: "no-match", files: [{ path: "src/load.ts", source: "load().catch((error) => logger.error({ error }, 'load failed'));" }], focusPath: "src/load.ts", expectedCount: 0, public: true },
|
|
8012
8139
|
{ id: "silent-rejection", title: "Do not swallow the rejection", outcome: "match", files: [{ path: "src/load.ts", source: "load().catch(() => null);" }], focusPath: "src/load.ts", expectedCount: 1, public: true }
|
|
@@ -8020,6 +8147,43 @@ var BODY_PARSE_METHODS = /* @__PURE__ */ new Set([
|
|
|
8020
8147
|
"json",
|
|
8021
8148
|
"text"
|
|
8022
8149
|
]);
|
|
8150
|
+
var ZOD_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
8151
|
+
"any",
|
|
8152
|
+
"array",
|
|
8153
|
+
"bigint",
|
|
8154
|
+
"boolean",
|
|
8155
|
+
"custom",
|
|
8156
|
+
"date",
|
|
8157
|
+
"enum",
|
|
8158
|
+
"literal",
|
|
8159
|
+
"map",
|
|
8160
|
+
"never",
|
|
8161
|
+
"null",
|
|
8162
|
+
"number",
|
|
8163
|
+
"object",
|
|
8164
|
+
"record",
|
|
8165
|
+
"set",
|
|
8166
|
+
"string",
|
|
8167
|
+
"tuple",
|
|
8168
|
+
"undefined",
|
|
8169
|
+
"union",
|
|
8170
|
+
"unknown"
|
|
8171
|
+
]);
|
|
8172
|
+
var ZOD_CHAIN_METHODS = /* @__PURE__ */ new Set([
|
|
8173
|
+
"array",
|
|
8174
|
+
"catch",
|
|
8175
|
+
"default",
|
|
8176
|
+
"describe",
|
|
8177
|
+
"max",
|
|
8178
|
+
"min",
|
|
8179
|
+
"nullable",
|
|
8180
|
+
"nullish",
|
|
8181
|
+
"optional",
|
|
8182
|
+
"readonly",
|
|
8183
|
+
"refine",
|
|
8184
|
+
"superRefine",
|
|
8185
|
+
"transform"
|
|
8186
|
+
]);
|
|
8023
8187
|
function isBodyParseCall(node) {
|
|
8024
8188
|
return node.type === import_utils43.AST_NODE_TYPES.CallExpression && node.arguments.length === 0 && node.callee.type === import_utils43.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils43.AST_NODE_TYPES.Identifier && BODY_PARSE_METHODS.has(node.callee.property.name);
|
|
8025
8189
|
}
|
|
@@ -8093,6 +8257,26 @@ var no_silent_promise_catch_default = createRule({
|
|
|
8093
8257
|
if (isTestFile(context.filename) || isScriptFile(context.filename)) {
|
|
8094
8258
|
return {};
|
|
8095
8259
|
}
|
|
8260
|
+
function isZodSchema(node, seen = /* @__PURE__ */ new Set()) {
|
|
8261
|
+
if (seen.has(node)) return false;
|
|
8262
|
+
seen.add(node);
|
|
8263
|
+
if (node.type === import_utils43.AST_NODE_TYPES.Identifier) {
|
|
8264
|
+
const binding = import_utils43.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
|
|
8265
|
+
if (binding === null || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
8266
|
+
const [definition] = binding.defs;
|
|
8267
|
+
return binding.defs.length === 1 && definition?.node.type === import_utils43.AST_NODE_TYPES.VariableDeclarator && definition.node.init !== null && isZodSchema(definition.node.init, seen);
|
|
8268
|
+
}
|
|
8269
|
+
if (node.type !== import_utils43.AST_NODE_TYPES.CallExpression || node.callee.type !== import_utils43.AST_NODE_TYPES.MemberExpression || node.callee.computed || node.callee.property.type !== import_utils43.AST_NODE_TYPES.Identifier) return false;
|
|
8270
|
+
const { object, property } = node.callee;
|
|
8271
|
+
if (object.type === import_utils43.AST_NODE_TYPES.Identifier && ZOD_CONSTRUCTORS.has(property.name)) {
|
|
8272
|
+
const binding = import_utils43.ASTUtils.findVariable(context.sourceCode.getScope(object), object.name);
|
|
8273
|
+
if (binding?.defs.some((definition) => {
|
|
8274
|
+
const specifier = definition.node;
|
|
8275
|
+
return (specifier.type === import_utils43.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils43.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils43.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils43.AST_NODE_TYPES.Identifier && specifier.imported.name === "z") && specifier.parent.type === import_utils43.AST_NODE_TYPES.ImportDeclaration && isZodModule(String(specifier.parent.source.value));
|
|
8276
|
+
})) return true;
|
|
8277
|
+
}
|
|
8278
|
+
return ZOD_CHAIN_METHODS.has(property.name) && isZodSchema(object, seen);
|
|
8279
|
+
}
|
|
8096
8280
|
const hasExplanatoryComment = (call, handler) => {
|
|
8097
8281
|
const sourceCode = context.sourceCode;
|
|
8098
8282
|
if (sourceCode.getCommentsInside(handler).some(isExplanatory)) {
|
|
@@ -8117,6 +8301,7 @@ var no_silent_promise_catch_default = createRule({
|
|
|
8117
8301
|
const method = node.callee.property.name;
|
|
8118
8302
|
const handlerIndex = method === "catch" ? 0 : method === "then" ? 1 : null;
|
|
8119
8303
|
if (handlerIndex === null) return;
|
|
8304
|
+
if (method === "catch" && isZodSchema(node.callee.object)) return;
|
|
8120
8305
|
if (isBodyParseCall(node.callee.object)) {
|
|
8121
8306
|
return;
|
|
8122
8307
|
}
|
|
@@ -8151,14 +8336,14 @@ var no_silent_promise_catch_default = createRule({
|
|
|
8151
8336
|
// src/rules/no-sleep-in-test-body.ts
|
|
8152
8337
|
var import_utils44 = require("@typescript-eslint/utils");
|
|
8153
8338
|
var NO_SLEEP_IN_TEST_BODY_DOCUMENTATION = {
|
|
8154
|
-
summary: "
|
|
8339
|
+
summary: "Avoid fixed timed sleeps directly in test bodies; synchronize on observable behavior or use controlled timers.",
|
|
8155
8340
|
rationale: "Wall-clock delays make test correctness depend on scheduler and machine speed.",
|
|
8156
|
-
remediation: "Await the observable signal or advance
|
|
8341
|
+
remediation: "Await the observable signal, or advance fake timers when supported and restore real timers in finally or a teardown hook.",
|
|
8157
8342
|
category: "testing",
|
|
8158
8343
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
8159
|
-
limitations: ["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked; nested fakes
|
|
8344
|
+
limitations: ["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked; nested fakes, parameterized delays, local helper bindings, and Promise executors with additional work or rejection callbacks are excluded."],
|
|
8160
8345
|
examples: [
|
|
8161
|
-
{ id: "fake-timer", title: "Advance time
|
|
8346
|
+
{ id: "fake-timer", title: "Advance controlled time and restore real timers", outcome: "no-match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { vi.useFakeTimers(); try { const result = retry(); await vi.advanceTimersByTimeAsync(50); await result; } finally { vi.useRealTimers(); } });" }], focusPath: "src/retry.test.ts", expectedCount: 0, public: true },
|
|
8162
8347
|
{ id: "fixed-sleep", title: "Do not wait for wall-clock time", outcome: "match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { await sleep(50); expect(done()).toBe(true); });" }], focusPath: "src/retry.test.ts", expectedCount: 1, public: true }
|
|
8163
8348
|
]
|
|
8164
8349
|
};
|
|
@@ -8177,9 +8362,6 @@ var FUNCTION_TYPES5 = /* @__PURE__ */ new Set([
|
|
|
8177
8362
|
function isNonzeroNumericLiteral(node) {
|
|
8178
8363
|
return node?.type === import_utils44.AST_NODE_TYPES.Literal && typeof node.value === "number" && node.value !== 0;
|
|
8179
8364
|
}
|
|
8180
|
-
function isTimedSetTimeout(node) {
|
|
8181
|
-
return node.type === import_utils44.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils44.AST_NODE_TYPES.Identifier && node.callee.name === "setTimeout" && node.arguments.length >= 2 && isNonzeroNumericLiteral(node.arguments[1]);
|
|
8182
|
-
}
|
|
8183
8365
|
function isPromiseSleep(node) {
|
|
8184
8366
|
if (node.callee.type !== import_utils44.AST_NODE_TYPES.Identifier || node.callee.name !== "Promise") {
|
|
8185
8367
|
return false;
|
|
@@ -8189,12 +8371,14 @@ function isPromiseSleep(node) {
|
|
|
8189
8371
|
return false;
|
|
8190
8372
|
}
|
|
8191
8373
|
const body2 = executor.body;
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8374
|
+
const resolve2 = executor.params[0];
|
|
8375
|
+
if (executor.params.length !== 1 || resolve2?.type !== import_utils44.AST_NODE_TYPES.Identifier || resolve2.name === "setTimeout") return false;
|
|
8376
|
+
const statement = body2.type === import_utils44.AST_NODE_TYPES.BlockStatement && body2.body.length === 1 ? body2.body[0] : null;
|
|
8377
|
+
const timer = body2.type !== import_utils44.AST_NODE_TYPES.BlockStatement ? body2 : statement?.type === import_utils44.AST_NODE_TYPES.ExpressionStatement ? statement.expression : null;
|
|
8378
|
+
return timer?.type === import_utils44.AST_NODE_TYPES.CallExpression && isTimedSetTimeout(timer) && timer.arguments[0]?.type === import_utils44.AST_NODE_TYPES.Identifier && timer.arguments[0].name === resolve2.name;
|
|
8379
|
+
}
|
|
8380
|
+
function isTimedSetTimeout(node) {
|
|
8381
|
+
return node.type === import_utils44.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils44.AST_NODE_TYPES.Identifier && node.callee.name === "setTimeout" && node.arguments.length >= 2 && isNonzeroNumericLiteral(node.arguments[1]);
|
|
8198
8382
|
}
|
|
8199
8383
|
function isHelperSleep(node) {
|
|
8200
8384
|
return node.callee.type === import_utils44.AST_NODE_TYPES.Identifier && SLEEP_HELPERS.has(node.callee.name) && node.arguments.length >= 1 && isNonzeroNumericLiteral(node.arguments[0]);
|
|
@@ -8248,11 +8432,11 @@ var no_sleep_in_test_body_default = createRule({
|
|
|
8248
8432
|
meta: {
|
|
8249
8433
|
type: "problem",
|
|
8250
8434
|
docs: {
|
|
8251
|
-
description: "
|
|
8435
|
+
description: "Avoid fixed timed sleeps directly in test bodies; synchronize on observable behavior or use controlled timers."
|
|
8252
8436
|
},
|
|
8253
8437
|
schema: [],
|
|
8254
8438
|
messages: {
|
|
8255
|
-
noSleepInTestBody: "A fixed sleep
|
|
8439
|
+
noSleepInTestBody: "A fixed sleep depends on wall-clock timing and can be flaky under load. Await observable completion or use controlled fake timers, restoring real timers afterward."
|
|
8256
8440
|
}
|
|
8257
8441
|
},
|
|
8258
8442
|
defaultOptions: [],
|
|
@@ -8273,11 +8457,17 @@ var no_sleep_in_test_body_default = createRule({
|
|
|
8273
8457
|
return {
|
|
8274
8458
|
NewExpression(node) {
|
|
8275
8459
|
if (isPromiseSleep(node)) {
|
|
8460
|
+
const constructor = import_utils44.ASTUtils.findVariable(context.sourceCode.getScope(node), "Promise");
|
|
8461
|
+
const timer = import_utils44.ASTUtils.findVariable(context.sourceCode.getScope(node), "setTimeout");
|
|
8462
|
+
if ((constructor?.defs.length ?? 0) > 0 || (timer?.defs.length ?? 0) > 0) return;
|
|
8276
8463
|
report2(node);
|
|
8277
8464
|
}
|
|
8278
8465
|
},
|
|
8279
8466
|
CallExpression(node) {
|
|
8280
8467
|
if (isHelperSleep(node)) {
|
|
8468
|
+
if (node.callee.type !== import_utils44.AST_NODE_TYPES.Identifier) return;
|
|
8469
|
+
const variable = import_utils44.ASTUtils.findVariable(context.sourceCode.getScope(node), node.callee.name);
|
|
8470
|
+
if (variable?.defs.some((definition) => definition.type !== "ImportBinding")) return;
|
|
8281
8471
|
report2(node);
|
|
8282
8472
|
}
|
|
8283
8473
|
}
|
|
@@ -8298,7 +8488,7 @@ var NO_STORAGE_IN_STATELESS_MODULES_DOCUMENTATION = {
|
|
|
8298
8488
|
rationale: "Private storage in a stateless workflow creates another source of truth that can silently diverge.",
|
|
8299
8489
|
remediation: "Read from the system of record or derive state from an artifact the workflow already produces.",
|
|
8300
8490
|
category: "architecture",
|
|
8301
|
-
limitations: ["
|
|
8491
|
+
limitations: ["This opt-in architectural policy requires configured module paths and storage method names. Overloaded `put` requires storage-like receiver evidence; `prepare` requires SQL-shaped literal text or a conventional database receiver for dynamic text. These syntax heuristics do not prove database provenance or identify the system of record."],
|
|
8302
8492
|
examples: [
|
|
8303
8493
|
{ id: "system-of-record", title: "Read from the system of record", outcome: "no-match", files: [{ path: "src/engineer-digest/post.ts", source: "const issues = await linear.listIssues();" }], focusPath: "src/engineer-digest/post.ts", expectedCount: 0, public: true },
|
|
8304
8494
|
{ id: "private-storage", title: "Do not write private state in a stateless module", outcome: "match", files: [{ path: "src/engineer-digest/post.ts", source: "await kv.put('digest:last', timestamp);" }], focusPath: "src/engineer-digest/post.ts", expectedCount: 1, public: true }
|
|
@@ -8332,6 +8522,17 @@ function storageMethodName(node, methods) {
|
|
|
8332
8522
|
if (name === "put" && !isStorageLikeReceiver(callee.object)) {
|
|
8333
8523
|
return null;
|
|
8334
8524
|
}
|
|
8525
|
+
if (name === "prepare") {
|
|
8526
|
+
const argument = node.arguments[0];
|
|
8527
|
+
const text = argument === void 0 ? null : sqlTextOf(argument);
|
|
8528
|
+
if (text !== null) {
|
|
8529
|
+
if (!/^\s*(?:SELECT|WITH|INSERT|UPDATE|DELETE|REPLACE|CREATE|ALTER|DROP|PRAGMA|EXPLAIN)\b/iu.test(stripSqlNoise(text))) return null;
|
|
8530
|
+
} else {
|
|
8531
|
+
const receiver = callee.object;
|
|
8532
|
+
const receiverName = receiver.type === import_utils45.AST_NODE_TYPES.Identifier ? receiver.name : receiver.type === import_utils45.AST_NODE_TYPES.MemberExpression && !receiver.computed && receiver.property.type === import_utils45.AST_NODE_TYPES.Identifier ? receiver.property.name : "";
|
|
8533
|
+
if (!/^(?:db|database|connection)$/iu.test(receiverName)) return null;
|
|
8534
|
+
}
|
|
8535
|
+
}
|
|
8335
8536
|
return name;
|
|
8336
8537
|
}
|
|
8337
8538
|
function isStorageLikeReceiver(node) {
|
|
@@ -8671,12 +8872,12 @@ var no_string_concat_in_loop_default = createRule({
|
|
|
8671
8872
|
// src/rules/no-tautological-expect.ts
|
|
8672
8873
|
var import_utils47 = require("@typescript-eslint/utils");
|
|
8673
8874
|
var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
8674
|
-
summary: "Disallow
|
|
8875
|
+
summary: "Disallow supported literal-only assertions that are statically known to pass.",
|
|
8675
8876
|
rationale: "An assertion determined entirely by literals does not observe the code under test and can keep passing after that code is removed.",
|
|
8676
8877
|
remediation: "Assert on a value produced by the behavior under test, or remove the assertion.",
|
|
8677
8878
|
category: "testing",
|
|
8678
8879
|
limitations: [
|
|
8679
|
-
"Only direct supported `expect` matcher calls in recognized test files are inspected."
|
|
8880
|
+
"Only direct supported `expect` matcher calls in recognized test files are inspected. Local expect bindings, regular expressions, and unsupported coercions are excluded; failing constant assertions are not tautologies."
|
|
8680
8881
|
],
|
|
8681
8882
|
examples: [
|
|
8682
8883
|
{
|
|
@@ -8713,11 +8914,11 @@ var NUMERIC_SIGNS = /* @__PURE__ */ new Set(["-", "+"]);
|
|
|
8713
8914
|
function isLiteral(node) {
|
|
8714
8915
|
switch (node.type) {
|
|
8715
8916
|
case import_utils47.AST_NODE_TYPES.Literal:
|
|
8716
|
-
return
|
|
8917
|
+
return !("regex" in node);
|
|
8717
8918
|
case import_utils47.AST_NODE_TYPES.TemplateLiteral:
|
|
8718
8919
|
return node.expressions.length === 0;
|
|
8719
8920
|
case import_utils47.AST_NODE_TYPES.UnaryExpression:
|
|
8720
|
-
return NUMERIC_SIGNS.has(node.operator) &&
|
|
8921
|
+
return NUMERIC_SIGNS.has(node.operator) && node.argument.type === import_utils47.AST_NODE_TYPES.Literal && typeof node.argument.value === "number";
|
|
8721
8922
|
case import_utils47.AST_NODE_TYPES.ArrayExpression:
|
|
8722
8923
|
return node.elements.every((element) => element !== null && isLiteral(element));
|
|
8723
8924
|
case import_utils47.AST_NODE_TYPES.ObjectExpression:
|
|
@@ -8731,6 +8932,43 @@ function isLiteral(node) {
|
|
|
8731
8932
|
function isStructuralLiteral(node) {
|
|
8732
8933
|
return node.type === import_utils47.AST_NODE_TYPES.ArrayExpression || node.type === import_utils47.AST_NODE_TYPES.ObjectExpression;
|
|
8733
8934
|
}
|
|
8935
|
+
function passesZeroArgumentMatcher(node, matcher) {
|
|
8936
|
+
let value;
|
|
8937
|
+
switch (node.type) {
|
|
8938
|
+
case import_utils47.AST_NODE_TYPES.Literal:
|
|
8939
|
+
value = node.value;
|
|
8940
|
+
break;
|
|
8941
|
+
case import_utils47.AST_NODE_TYPES.TemplateLiteral:
|
|
8942
|
+
value = node.quasis[0]?.value.cooked;
|
|
8943
|
+
break;
|
|
8944
|
+
case import_utils47.AST_NODE_TYPES.UnaryExpression:
|
|
8945
|
+
if (node.argument.type !== import_utils47.AST_NODE_TYPES.Literal || typeof node.argument.value !== "number") return false;
|
|
8946
|
+
value = node.operator === "-" ? -node.argument.value : node.argument.value;
|
|
8947
|
+
break;
|
|
8948
|
+
case import_utils47.AST_NODE_TYPES.ArrayExpression:
|
|
8949
|
+
case import_utils47.AST_NODE_TYPES.ObjectExpression:
|
|
8950
|
+
value = {};
|
|
8951
|
+
break;
|
|
8952
|
+
default:
|
|
8953
|
+
return false;
|
|
8954
|
+
}
|
|
8955
|
+
switch (matcher) {
|
|
8956
|
+
case "toBeDefined":
|
|
8957
|
+
return value !== void 0;
|
|
8958
|
+
case "toBeUndefined":
|
|
8959
|
+
return value === void 0;
|
|
8960
|
+
case "toBeNull":
|
|
8961
|
+
return value === null;
|
|
8962
|
+
case "toBeTruthy":
|
|
8963
|
+
return Boolean(value);
|
|
8964
|
+
case "toBeFalsy":
|
|
8965
|
+
return !value;
|
|
8966
|
+
case "toBeNaN":
|
|
8967
|
+
return typeof value === "number" && Number.isNaN(value);
|
|
8968
|
+
default:
|
|
8969
|
+
return false;
|
|
8970
|
+
}
|
|
8971
|
+
}
|
|
8734
8972
|
function expectOperand(callee) {
|
|
8735
8973
|
const receiver = callee.object;
|
|
8736
8974
|
if (receiver.type !== import_utils47.AST_NODE_TYPES.CallExpression || receiver.callee.type !== import_utils47.AST_NODE_TYPES.Identifier || receiver.callee.name !== "expect" || receiver.arguments.length !== 1) {
|
|
@@ -8744,12 +8982,12 @@ var no_tautological_expect_default = createRule({
|
|
|
8744
8982
|
meta: {
|
|
8745
8983
|
type: "problem",
|
|
8746
8984
|
docs: {
|
|
8747
|
-
description: "Disallow
|
|
8985
|
+
description: "Disallow supported literal-only assertions that are statically known to pass."
|
|
8748
8986
|
},
|
|
8749
8987
|
schema: [],
|
|
8750
8988
|
messages: {
|
|
8751
|
-
tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` compares
|
|
8752
|
-
tautologicalMatcher: "`expect({{operand}}).{{matcher}}()`
|
|
8989
|
+
tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` compares an identical literal and does not observe behavior. Assert on a produced value or remove only the redundant assertion, preserving other coverage.",
|
|
8990
|
+
tautologicalMatcher: "`expect({{operand}}).{{matcher}}()` is statically known to pass. Assert on a produced value or remove only the redundant assertion, preserving other coverage."
|
|
8753
8991
|
}
|
|
8754
8992
|
},
|
|
8755
8993
|
defaultOptions: [],
|
|
@@ -8771,11 +9009,20 @@ var no_tautological_expect_default = createRule({
|
|
|
8771
9009
|
return;
|
|
8772
9010
|
}
|
|
8773
9011
|
const matcher = callee.property.name;
|
|
9012
|
+
if (callee.object.type !== import_utils47.AST_NODE_TYPES.CallExpression || callee.object.callee.type !== import_utils47.AST_NODE_TYPES.Identifier) return;
|
|
9013
|
+
const expectIdentifier = callee.object.callee;
|
|
9014
|
+
const variable = import_utils47.ASTUtils.findVariable(context.sourceCode.getScope(expectIdentifier), expectIdentifier.name);
|
|
9015
|
+
if (variable !== null && variable.defs.some((definition) => {
|
|
9016
|
+
if (definition.node.type !== import_utils47.AST_NODE_TYPES.ImportSpecifier) return true;
|
|
9017
|
+
const declaration = definition.node.parent;
|
|
9018
|
+
const imported = definition.node.imported;
|
|
9019
|
+
return declaration.type !== import_utils47.AST_NODE_TYPES.ImportDeclaration || !["vitest", "@jest/globals", "@playwright/test", "bun:test"].includes(String(declaration.source.value)) || (imported.type === import_utils47.AST_NODE_TYPES.Identifier ? imported.name : imported.value) !== "expect";
|
|
9020
|
+
})) return;
|
|
8774
9021
|
const operand = expectOperand(callee);
|
|
8775
9022
|
if (operand === null || !isLiteral(operand)) {
|
|
8776
9023
|
return;
|
|
8777
9024
|
}
|
|
8778
|
-
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0) {
|
|
9025
|
+
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0 && passesZeroArgumentMatcher(operand, matcher)) {
|
|
8779
9026
|
context.report({
|
|
8780
9027
|
node,
|
|
8781
9028
|
messageId: "tautologicalMatcher",
|
|
@@ -10027,15 +10274,16 @@ var MOCK_MODULES = /* @__PURE__ */ new Set([
|
|
|
10027
10274
|
var NO_UNSAFE_MOCK_CASTING_DOCUMENTATION = {
|
|
10028
10275
|
summary: "Disallow casting to mock types like `jest.Mock` or `vi.Mock`. Use `vi.mocked()` or `jest.mocked()` instead.",
|
|
10029
10276
|
rationale: "A type assertion can claim an unmocked value is a mock and bypass checking between the original callable and the mock API.",
|
|
10030
|
-
remediation: "
|
|
10277
|
+
remediation: "Create the mock or spy first, then use the framework's mocked helper to preserve the original value's type. The helper does not create or verify a runtime mock.",
|
|
10031
10278
|
category: "testing",
|
|
10032
|
-
limitations: ["Only mock types imported from Vitest or Jest modules are inspected."],
|
|
10279
|
+
limitations: ["Only mock types imported from Vitest or Jest modules are inspected. mocked is a type helper, not runtime validation or a replacement for mock setup."],
|
|
10280
|
+
references: ["https://vitest.dev/api/vi.html#vi-mocked"],
|
|
10033
10281
|
examples: [
|
|
10034
10282
|
{
|
|
10035
10283
|
id: "typed-mock-helper",
|
|
10036
10284
|
title: "Use the framework helper",
|
|
10037
10285
|
outcome: "no-match",
|
|
10038
|
-
files: [{ path: "src/client.test.ts", source: "const m = vi.mocked(
|
|
10286
|
+
files: [{ path: "src/client.test.ts", source: "import { vi } from 'vitest'; const client = { read: () => 'value' }; vi.spyOn(client, 'read'); const m = vi.mocked(client.read);" }],
|
|
10039
10287
|
focusPath: "src/client.test.ts",
|
|
10040
10288
|
expectedCount: 0,
|
|
10041
10289
|
public: true
|
|
@@ -10061,7 +10309,7 @@ var no_unsafe_mock_casting_default = createRule({
|
|
|
10061
10309
|
},
|
|
10062
10310
|
schema: [],
|
|
10063
10311
|
messages: {
|
|
10064
|
-
unsafeMockCast: "
|
|
10312
|
+
unsafeMockCast: "Avoid a broad Mock cast. After creating the mock or spy, use `vi.mocked(fn)` or `jest.mocked(fn)` to retain its original type; the helper does not create a runtime mock."
|
|
10065
10313
|
}
|
|
10066
10314
|
},
|
|
10067
10315
|
defaultOptions: [],
|
|
@@ -10123,7 +10371,7 @@ var import_utils55 = require("@typescript-eslint/utils");
|
|
|
10123
10371
|
var ts2 = __toESM(require("typescript"), 1);
|
|
10124
10372
|
var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
|
|
10125
10373
|
summary: 'Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum(["a", "b"])` with a string-literal union instead.',
|
|
10126
|
-
rationale: "
|
|
10374
|
+
rationale: "The project prefers literal-first schema definitions. nativeEnum also accepts plain enum-like objects, so this is an explicit declaration policy, not proof that every call duplicates a TypeScript enum's runtime object.",
|
|
10127
10375
|
remediation: "Pass string literals directly to `z.enum` and derive the TypeScript type with `z.infer`.",
|
|
10128
10376
|
category: "maintainability",
|
|
10129
10377
|
autofix: "none",
|
|
@@ -10301,7 +10549,7 @@ var TEST_LOOPS_OVER_LITERAL_CASES_DOCUMENTATION = {
|
|
|
10301
10549
|
remediation: "Create one named parameterized test or runner-aware subtest for each literal case.",
|
|
10302
10550
|
category: "testing",
|
|
10303
10551
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**"],
|
|
10304
|
-
limitations: ["Only inline literal for-of cases containing framework assertions are reported."],
|
|
10552
|
+
limitations: ["Only inline literal for-of cases containing framework assertions are reported. References to setup or parameters owned by the enclosing test, and loops followed by statements in the same or enclosing block, are excluded because they can belong to an ordered scenario. External helper purity is not inferred."],
|
|
10305
10553
|
examples: [
|
|
10306
10554
|
{ id: "parameterized-cases", title: "Use a parameterized test", outcome: "no-match", files: [{ path: "src/parser.test.ts", source: "test.each(['a', 'b'])('parses %s', (value) => { expect(parse(value)).toBe(value); });" }], focusPath: "src/parser.test.ts", expectedCount: 0, public: true },
|
|
10307
10555
|
{ id: "looped-cases", title: "Do not hide cases in a loop", outcome: "match", files: [{ path: "src/parser.test.ts", source: "test('parses', () => { for (const value of ['a', 'b']) { expect(parse(value)).toBe(value); } });" }], focusPath: "src/parser.test.ts", expectedCount: 1, public: true }
|
|
@@ -10448,7 +10696,7 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10448
10696
|
},
|
|
10449
10697
|
schema: [],
|
|
10450
10698
|
messages: {
|
|
10451
|
-
literalCaseLoop: "This loop asserts over {{count}} inline cases
|
|
10699
|
+
literalCaseLoop: "This loop asserts over {{count}} inline cases in one test; a thrown assertion may prevent later cases from running. Create one named test or subtest per independent case; use `test.each(...)` or `it.each(...)` where supported."
|
|
10452
10700
|
}
|
|
10453
10701
|
},
|
|
10454
10702
|
defaultOptions: [],
|
|
@@ -10473,6 +10721,9 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10473
10721
|
if (enclosing === null || !isTestBody2(enclosing, isFrameworkTest)) {
|
|
10474
10722
|
return;
|
|
10475
10723
|
}
|
|
10724
|
+
for (let current = node; current !== void 0 && current !== enclosing; current = current.parent) {
|
|
10725
|
+
if (current.parent?.type === import_utils56.AST_NODE_TYPES.BlockStatement && current.parent.body.at(-1) !== current) return;
|
|
10726
|
+
}
|
|
10476
10727
|
const cases = unwrapExpression(node.right);
|
|
10477
10728
|
const callbackParameters = new Set(
|
|
10478
10729
|
enclosing.params.flatMap((parameter) => parameter.type === import_utils56.AST_NODE_TYPES.Identifier ? [parameter.name] : [])
|
|
@@ -10482,6 +10733,16 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10482
10733
|
) || !walkOwnScope2(node.body, (current) => isAssertion2(current, isFrameworkAssertion)) || walkOwnScope2(node.body, (current) => opensSubtest(current, callbackParameters)) || walkOwnScope2(node.body, (current) => LOOP_CARRIED_CONTROL.has(current.type))) {
|
|
10483
10734
|
return;
|
|
10484
10735
|
}
|
|
10736
|
+
const capturesSetup = walkOwnScope2(node.body, (current) => {
|
|
10737
|
+
if (current.type !== import_utils56.AST_NODE_TYPES.Identifier) return false;
|
|
10738
|
+
const variable = import_utils56.ASTUtils.findVariable(context.sourceCode.getScope(current), current.name);
|
|
10739
|
+
if (variable === null || !variable.references.some((reference) => reference.identifier === current)) return false;
|
|
10740
|
+
return variable.defs.some((definition) => {
|
|
10741
|
+
const declaration = definition.name;
|
|
10742
|
+
return declaration.range[0] >= enclosing.range[0] && declaration.range[1] <= enclosing.range[1] && (declaration.range[0] < node.range[0] || declaration.range[1] > node.range[1]);
|
|
10743
|
+
});
|
|
10744
|
+
});
|
|
10745
|
+
if (capturesSetup) return;
|
|
10485
10746
|
context.report({
|
|
10486
10747
|
node,
|
|
10487
10748
|
messageId: "literalCaseLoop",
|
|
@@ -11102,7 +11363,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
|
11102
11363
|
category: "correctness",
|
|
11103
11364
|
autofix: "none",
|
|
11104
11365
|
limitations: [
|
|
11105
|
-
"Only direct identifier keys in application-owned z.object/z.strictObject schemas are checked.",
|
|
11366
|
+
"Only direct identifier keys with recognizable numeric Zod leaves in application-owned z.object/z.strictObject schemas are checked; aliases and transformations are not inferred.",
|
|
11106
11367
|
"The rule covers control timings such as timeout, delay, interval, backoff, TTL, lease, heartbeat, debounce, and throttle; observed durations and business-domain periods are excluded.",
|
|
11107
11368
|
"Quoted/computed protocol keys, generated/vendor code, tests, fixtures, and non-Zod schemas are excluded."
|
|
11108
11369
|
],
|
|
@@ -11114,7 +11375,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
|
11114
11375
|
files: [
|
|
11115
11376
|
{
|
|
11116
11377
|
path: "src/request.ts",
|
|
11117
|
-
source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeoutMs: z.number().int().min(
|
|
11378
|
+
source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeoutMs: z.number().int().min(1000).max(300000).default(30000) });"
|
|
11118
11379
|
}
|
|
11119
11380
|
],
|
|
11120
11381
|
focusPath: "src/request.ts",
|
|
@@ -11128,7 +11389,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
|
11128
11389
|
files: [
|
|
11129
11390
|
{
|
|
11130
11391
|
path: "src/request.ts",
|
|
11131
|
-
source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeout_seconds: z.number().int().min(1) });"
|
|
11392
|
+
source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeout_seconds: z.number().int().min(1).max(300).default(30) });"
|
|
11132
11393
|
}
|
|
11133
11394
|
],
|
|
11134
11395
|
focusPath: "src/request.ts",
|
|
@@ -11160,6 +11421,7 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11160
11421
|
}
|
|
11161
11422
|
const zodNamespaces = /* @__PURE__ */ new Set();
|
|
11162
11423
|
const objectFactories = /* @__PURE__ */ new Set();
|
|
11424
|
+
const numberFactories = /* @__PURE__ */ new Set();
|
|
11163
11425
|
function binding(identifier) {
|
|
11164
11426
|
return import_utils63.ASTUtils.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
11165
11427
|
}
|
|
@@ -11179,10 +11441,27 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11179
11441
|
const variable = binding(callee.object);
|
|
11180
11442
|
return variable !== null && zodNamespaces.has(variable);
|
|
11181
11443
|
}
|
|
11444
|
+
function isNumericSchema(node) {
|
|
11445
|
+
if (node.type !== import_utils63.AST_NODE_TYPES.CallExpression) return false;
|
|
11446
|
+
const callee = node.callee;
|
|
11447
|
+
if (callee.type === import_utils63.AST_NODE_TYPES.Identifier) {
|
|
11448
|
+
const variable = binding(callee);
|
|
11449
|
+
return variable !== null && numberFactories.has(variable);
|
|
11450
|
+
}
|
|
11451
|
+
if (callee.type !== import_utils63.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils63.AST_NODE_TYPES.Identifier) return false;
|
|
11452
|
+
if (callee.object.type === import_utils63.AST_NODE_TYPES.Identifier) {
|
|
11453
|
+
const variable = binding(callee.object);
|
|
11454
|
+
return callee.property.name === "number" && variable !== null && zodNamespaces.has(variable);
|
|
11455
|
+
}
|
|
11456
|
+
return ["int", "min", "max", "positive", "nonnegative", "finite", "multipleOf", "optional", "nullable", "nullish", "default", "describe", "brand", "readonly"].includes(callee.property.name) && isNumericSchema(callee.object);
|
|
11457
|
+
}
|
|
11182
11458
|
return {
|
|
11183
11459
|
ImportDeclaration(node) {
|
|
11184
11460
|
if (!isZodModule(node.source.value)) return;
|
|
11185
11461
|
for (const specifier of node.specifiers) {
|
|
11462
|
+
if (specifier.type === import_utils63.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils63.AST_NODE_TYPES.Identifier && specifier.imported.name === "number") {
|
|
11463
|
+
record(numberFactories, specifier.local);
|
|
11464
|
+
}
|
|
11186
11465
|
if (specifier.type === import_utils63.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils63.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils63.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils63.AST_NODE_TYPES.Identifier && specifier.imported.name === "z") {
|
|
11187
11466
|
record(zodNamespaces, specifier.local);
|
|
11188
11467
|
} else if (specifier.type === import_utils63.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils63.AST_NODE_TYPES.Identifier && (specifier.imported.name === "object" || specifier.imported.name === "strictObject")) {
|
|
@@ -11195,7 +11474,7 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11195
11474
|
const shape = node.arguments[0];
|
|
11196
11475
|
if (shape?.type !== import_utils63.AST_NODE_TYPES.ObjectExpression) return;
|
|
11197
11476
|
for (const member of shape.properties) {
|
|
11198
|
-
if (member.type !== import_utils63.AST_NODE_TYPES.Property) continue;
|
|
11477
|
+
if (member.type !== import_utils63.AST_NODE_TYPES.Property || !isNumericSchema(member.value)) continue;
|
|
11199
11478
|
const key = directIdentifierKey(member);
|
|
11200
11479
|
if (key === null || !CONTROL_SECONDS_RE.test(key.name) && !CONTROL_SECONDS_CAMEL_RE.test(key.name)) {
|
|
11201
11480
|
continue;
|
|
@@ -11215,7 +11494,7 @@ var PREFER_IMMUTABLE_MODULE_CONSTANT_DOCUMENTATION = {
|
|
|
11215
11494
|
remediation: "Expose literals with `as const` or a readonly type, and expose Set or Map values through ReadonlySet or ReadonlyMap.",
|
|
11216
11495
|
category: "correctness",
|
|
11217
11496
|
limitations: [
|
|
11218
|
-
"
|
|
11497
|
+
"Generated, test and JavaScript files are skipped. Private constants with observed direct or alias mutation are excluded; exported mutable collections remain advisory candidates. Reassigned aliases are conservatively followed, not flow-proven."
|
|
11219
11498
|
],
|
|
11220
11499
|
examples: [
|
|
11221
11500
|
{
|
|
@@ -11369,7 +11648,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11369
11648
|
}
|
|
11370
11649
|
const exportedNames2 = /* @__PURE__ */ new Set();
|
|
11371
11650
|
const typeAliases2 = /* @__PURE__ */ new Map();
|
|
11372
|
-
const
|
|
11651
|
+
const mutatesThroughAlias = (root) => {
|
|
11373
11652
|
const pending = [root];
|
|
11374
11653
|
const seen = /* @__PURE__ */ new Set();
|
|
11375
11654
|
while (pending.length > 0) {
|
|
@@ -11381,7 +11660,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11381
11660
|
if (identifier.type !== import_utils64.AST_NODE_TYPES.Identifier) continue;
|
|
11382
11661
|
if (referenceMutates(identifier, isUnshadowedGlobal3)) return true;
|
|
11383
11662
|
const declarator = identifier.parent;
|
|
11384
|
-
if (declarator.type !== import_utils64.AST_NODE_TYPES.VariableDeclarator || declarator.init !== identifier || declarator.id.type !== import_utils64.AST_NODE_TYPES.Identifier || declarator.parent.type !== import_utils64.AST_NODE_TYPES.VariableDeclaration
|
|
11663
|
+
if (declarator.type !== import_utils64.AST_NODE_TYPES.VariableDeclarator || declarator.init !== identifier || declarator.id.type !== import_utils64.AST_NODE_TYPES.Identifier || declarator.parent.type !== import_utils64.AST_NODE_TYPES.VariableDeclaration) {
|
|
11385
11664
|
continue;
|
|
11386
11665
|
}
|
|
11387
11666
|
const alias = sourceCode.getDeclaredVariables(declarator)[0];
|
|
@@ -11430,7 +11709,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11430
11709
|
return;
|
|
11431
11710
|
}
|
|
11432
11711
|
const variable = sourceCode.getDeclaredVariables(node)[0];
|
|
11433
|
-
if (!directlyExported && !exportedNames2.has(node.id.name) && variable !== void 0 &&
|
|
11712
|
+
if (!directlyExported && !exportedNames2.has(node.id.name) && variable !== void 0 && mutatesThroughAlias(variable)) {
|
|
11434
11713
|
return;
|
|
11435
11714
|
}
|
|
11436
11715
|
context.report({
|
|
@@ -11857,7 +12136,7 @@ var PREFER_MODULE_LEVEL_CONSTANT_DOCUMENTATION = {
|
|
|
11857
12136
|
rationale: "Recreating immutable lookup data on every call wastes allocations and obscures its constant nature.",
|
|
11858
12137
|
remediation: "Declare immutable literal collections and non-stateful regular expressions once at module scope.",
|
|
11859
12138
|
category: "performance",
|
|
11860
|
-
limitations: ["
|
|
12139
|
+
limitations: ["Small collections, observed direct or nested mutations, nested aliases, direct escapes, and dependencies on local values are excluded. Directly invoked function expressions are excluded rather than assuming they run repeatedly. Callback effects and indirect escapes are not analyzed interprocedurally; review those before hoisting."],
|
|
11861
12140
|
examples: [
|
|
11862
12141
|
{ id: "hoisted-collection", title: "Hoist a constant collection", outcome: "no-match", files: [{ path: "src/keys.ts", source: "const KEYS = ['a', 'b', 'c'] as const; function isAllowed(key: string) { return KEYS.includes(key); }" }], focusPath: "src/keys.ts", expectedCount: 0, public: true },
|
|
11863
12142
|
{ id: "local-collection", title: "Do not recreate a constant collection", outcome: "match", files: [{ path: "src/keys.ts", source: "function isAllowed(key: string) { const KEYS = ['a', 'b', 'c']; return KEYS.includes(key); }" }], focusPath: "src/keys.ts", expectedCount: 1, public: true }
|
|
@@ -12012,12 +12291,16 @@ var NON_RETAINING_BUILTINS = /* @__PURE__ */ new Map(
|
|
|
12012
12291
|
]
|
|
12013
12292
|
);
|
|
12014
12293
|
function isSafeRead(identifier) {
|
|
12015
|
-
|
|
12294
|
+
let parent = identifier.parent;
|
|
12016
12295
|
if (parent.type === import_utils66.AST_NODE_TYPES.MemberExpression) {
|
|
12017
12296
|
if (parent.object !== identifier) {
|
|
12018
12297
|
return true;
|
|
12019
12298
|
}
|
|
12299
|
+
while (parent.parent.type === import_utils66.AST_NODE_TYPES.MemberExpression && parent.parent.object === parent) {
|
|
12300
|
+
parent = parent.parent;
|
|
12301
|
+
}
|
|
12020
12302
|
const grandparent = parent.parent;
|
|
12303
|
+
if (grandparent.type === import_utils66.AST_NODE_TYPES.VariableDeclarator || grandparent.type === import_utils66.AST_NODE_TYPES.SpreadElement) return false;
|
|
12021
12304
|
if (grandparent.type === import_utils66.AST_NODE_TYPES.AssignmentExpression && grandparent.left === parent) {
|
|
12022
12305
|
return false;
|
|
12023
12306
|
}
|
|
@@ -12027,7 +12310,7 @@ function isSafeRead(identifier) {
|
|
|
12027
12310
|
if (grandparent.type === import_utils66.AST_NODE_TYPES.UnaryExpression && grandparent.operator === "delete") {
|
|
12028
12311
|
return false;
|
|
12029
12312
|
}
|
|
12030
|
-
if (
|
|
12313
|
+
if (grandparent.type === import_utils66.AST_NODE_TYPES.CallExpression && grandparent.callee === parent && (parent.computed ? parent.property.type !== import_utils66.AST_NODE_TYPES.Literal || typeof parent.property.value !== "string" || MUTATING_METHODS2.has(parent.property.value) : parent.property.type === import_utils66.AST_NODE_TYPES.Identifier && MUTATING_METHODS2.has(parent.property.name))) {
|
|
12031
12314
|
return false;
|
|
12032
12315
|
}
|
|
12033
12316
|
return true;
|
|
@@ -12135,9 +12418,15 @@ var prefer_module_level_constant_default = createRule({
|
|
|
12135
12418
|
if (node.id.type !== import_utils66.AST_NODE_TYPES.Identifier || node.init === null) {
|
|
12136
12419
|
return;
|
|
12137
12420
|
}
|
|
12138
|
-
|
|
12421
|
+
const owner = enclosingFunction3(node);
|
|
12422
|
+
if (owner === null) {
|
|
12139
12423
|
return;
|
|
12140
12424
|
}
|
|
12425
|
+
let expression = owner;
|
|
12426
|
+
while (expression.parent !== void 0 && unwrap4(expression.parent) === expression) {
|
|
12427
|
+
expression = expression.parent;
|
|
12428
|
+
}
|
|
12429
|
+
if (expression.parent?.type === import_utils66.AST_NODE_TYPES.CallExpression && expression.parent.callee === expression) return;
|
|
12141
12430
|
const candidate2 = classify(node.init, checkRegex);
|
|
12142
12431
|
if (candidate2 === null) {
|
|
12143
12432
|
return;
|
|
@@ -12162,10 +12451,10 @@ var prefer_module_level_constant_default = createRule({
|
|
|
12162
12451
|
var import_utils67 = require("@typescript-eslint/utils");
|
|
12163
12452
|
var PREFER_MODULE_LEVEL_SCHEMA_DOCUMENTATION = {
|
|
12164
12453
|
summary: "Declare a Zod schema at module scope when it closes over nothing in the enclosing function",
|
|
12165
|
-
rationale: "A
|
|
12454
|
+
rationale: "A schema created inside a function is rebuilt on each call. Module scope can enable reuse across callers; local schemas already support local type inference.",
|
|
12166
12455
|
remediation: "Move the closed schema declaration to module scope and reference it from the function.",
|
|
12167
12456
|
category: "performance",
|
|
12168
|
-
limitations: ["Schemas that depend on local state or are wrapped in a recognized memoization helper are excluded."],
|
|
12457
|
+
limitations: ["Schemas that depend on local state or are wrapped in a recognized memoization helper are excluded.", "Eager calls outside the recognized Zod construction chain and new expressions are excluded. This is manual guidance, not a purity proof: review getters, callback effects, schema identity, error customization, and module initialization order before moving construction."],
|
|
12169
12458
|
examples: [
|
|
12170
12459
|
{ id: "module-schema", title: "Declare the schema once", outcome: "no-match", files: [{ path: "src/handler.ts", source: "import { z } from 'zod'; const ZBody = z.object({ id: z.string(), name: z.string() }); export function handle(raw: unknown) { return ZBody.parse(raw); }" }], focusPath: "src/handler.ts", expectedCount: 0, public: true },
|
|
12171
12460
|
{ id: "local-schema", title: "Do not rebuild a closed schema", outcome: "match", files: [{ path: "src/handler.ts", source: "import { z } from 'zod'; export function handle(raw: unknown) { const ZBody = z.object({ id: z.string(), name: z.string() }); return ZBody.parse(raw); }" }], focusPath: "src/handler.ts", expectedCount: 1, public: true }
|
|
@@ -12182,6 +12471,36 @@ var DEFAULT_FACTORIES = [
|
|
|
12182
12471
|
"union"
|
|
12183
12472
|
];
|
|
12184
12473
|
var DEFAULT_MIN_PROPERTIES = 2;
|
|
12474
|
+
var CONSTRUCTION_FACTORIES = /* @__PURE__ */ new Set([
|
|
12475
|
+
...DEFAULT_FACTORIES,
|
|
12476
|
+
"any",
|
|
12477
|
+
"array",
|
|
12478
|
+
"bigint",
|
|
12479
|
+
"boolean",
|
|
12480
|
+
"custom",
|
|
12481
|
+
"date",
|
|
12482
|
+
"enum",
|
|
12483
|
+
"instanceof",
|
|
12484
|
+
"lazy",
|
|
12485
|
+
"literal",
|
|
12486
|
+
"map",
|
|
12487
|
+
"nan",
|
|
12488
|
+
"nativeEnum",
|
|
12489
|
+
"never",
|
|
12490
|
+
"null",
|
|
12491
|
+
"nullable",
|
|
12492
|
+
"nullish",
|
|
12493
|
+
"number",
|
|
12494
|
+
"optional",
|
|
12495
|
+
"preprocess",
|
|
12496
|
+
"promise",
|
|
12497
|
+
"set",
|
|
12498
|
+
"string",
|
|
12499
|
+
"symbol",
|
|
12500
|
+
"undefined",
|
|
12501
|
+
"unknown",
|
|
12502
|
+
"void"
|
|
12503
|
+
]);
|
|
12185
12504
|
var MEMO_CALLEES = /* @__PURE__ */ new Set([
|
|
12186
12505
|
"lazy",
|
|
12187
12506
|
"memo",
|
|
@@ -12258,7 +12577,7 @@ function outermostEnclosingFunction(node) {
|
|
|
12258
12577
|
}
|
|
12259
12578
|
return outermost;
|
|
12260
12579
|
}
|
|
12261
|
-
function subtreeSome(root, predicate) {
|
|
12580
|
+
function subtreeSome(root, predicate, skipDeferredFunctions = false) {
|
|
12262
12581
|
let found = false;
|
|
12263
12582
|
const visit = (value) => {
|
|
12264
12583
|
if (found || value === null || typeof value !== "object") {
|
|
@@ -12274,6 +12593,7 @@ function subtreeSome(root, predicate) {
|
|
|
12274
12593
|
if (typeof candidate2.type !== "string") {
|
|
12275
12594
|
return;
|
|
12276
12595
|
}
|
|
12596
|
+
if (skipDeferredFunctions && FUNCTION_TYPES8.has(candidate2.type)) return;
|
|
12277
12597
|
if (predicate(candidate2)) {
|
|
12278
12598
|
found = true;
|
|
12279
12599
|
return;
|
|
@@ -12372,6 +12692,15 @@ var prefer_module_level_schema_default = createRule({
|
|
|
12372
12692
|
function isZodCall(node) {
|
|
12373
12693
|
return node.type === import_utils67.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils67.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === import_utils67.AST_NODE_TYPES.Identifier && zodNamespaces.has(node.callee.object.name);
|
|
12374
12694
|
}
|
|
12695
|
+
function isSchemaConstruction(node) {
|
|
12696
|
+
const callee = node.callee;
|
|
12697
|
+
if (callee.type !== import_utils67.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils67.AST_NODE_TYPES.Identifier || TERMINAL_METHODS.has(callee.property.name)) return false;
|
|
12698
|
+
if (callee.object.type === import_utils67.AST_NODE_TYPES.CallExpression) return isSchemaConstruction(callee.object);
|
|
12699
|
+
return isZodCall(node) && CONSTRUCTION_FACTORIES.has(callee.property.name);
|
|
12700
|
+
}
|
|
12701
|
+
function hasEagerComputation(node) {
|
|
12702
|
+
return subtreeSome(node, (inner) => inner.type === import_utils67.AST_NODE_TYPES.NewExpression || inner.type === import_utils67.AST_NODE_TYPES.TaggedTemplateExpression || inner.type === import_utils67.AST_NODE_TYPES.CallExpression && !isSchemaConstruction(inner), true);
|
|
12703
|
+
}
|
|
12375
12704
|
function isCovered(node) {
|
|
12376
12705
|
let current = node.parent ?? void 0;
|
|
12377
12706
|
while (current !== void 0) {
|
|
@@ -12501,6 +12830,7 @@ var prefer_module_level_schema_default = createRule({
|
|
|
12501
12830
|
return;
|
|
12502
12831
|
}
|
|
12503
12832
|
const outermost = outermostSchemaExpression(expression);
|
|
12833
|
+
if (hasEagerComputation(outermost)) return;
|
|
12504
12834
|
if (outermost !== expression && (readsReceiver(outermost) || buildsLocalizedText(outermost) || !closesOverNothing(outermost, enclosing))) {
|
|
12505
12835
|
return;
|
|
12506
12836
|
}
|
|
@@ -12638,7 +12968,8 @@ var PREFER_MODULE_LEVEL_REFINED_SCHEMA_DOCUMENTATION = {
|
|
|
12638
12968
|
limitations: [
|
|
12639
12969
|
"Composite object/record/tuple/union schemas are owned by prefer-module-level-schema.",
|
|
12640
12970
|
"Schemas that depend on function-local or mutable state, localized text, receiver state, lazy construction, or recognized memoization are excluded.",
|
|
12641
|
-
"Literal string z.enum domains are owned by prefer-shared-zod-enum."
|
|
12971
|
+
"Literal string z.enum domains are owned by prefer-shared-zod-enum.",
|
|
12972
|
+
"Eager calls outside recognized Zod construction chains and new expressions are excluded. This is manual guidance, not a purity proof: review getters, callback effects, error customization, schema identity, and module initialization order before moving construction."
|
|
12642
12973
|
],
|
|
12643
12974
|
examples: [
|
|
12644
12975
|
{
|
|
@@ -12680,7 +13011,7 @@ function collectReferences2(scope, output) {
|
|
|
12680
13011
|
output.push(...scope.references);
|
|
12681
13012
|
for (const child of scope.childScopes) collectReferences2(child, output);
|
|
12682
13013
|
}
|
|
12683
|
-
function subtreeSome2(root, predicate) {
|
|
13014
|
+
function subtreeSome2(root, predicate, skipDeferredFunctions = false) {
|
|
12684
13015
|
let found = false;
|
|
12685
13016
|
const visit = (value) => {
|
|
12686
13017
|
if (found || value === null || typeof value !== "object") return;
|
|
@@ -12690,6 +13021,7 @@ function subtreeSome2(root, predicate) {
|
|
|
12690
13021
|
}
|
|
12691
13022
|
const candidate2 = value;
|
|
12692
13023
|
if (typeof candidate2.type !== "string") return;
|
|
13024
|
+
if (skipDeferredFunctions && FUNCTION_TYPES9.has(candidate2.type)) return;
|
|
12693
13025
|
if (predicate(candidate2)) {
|
|
12694
13026
|
found = true;
|
|
12695
13027
|
return;
|
|
@@ -12810,6 +13142,15 @@ var prefer_module_level_refined_schema_default = createRule({
|
|
|
12810
13142
|
return names[1] ?? null;
|
|
12811
13143
|
return null;
|
|
12812
13144
|
}
|
|
13145
|
+
function isSchemaConstruction(node) {
|
|
13146
|
+
const callee = node.callee;
|
|
13147
|
+
if (callee.type !== import_utils68.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils68.AST_NODE_TYPES.Identifier || NON_SCHEMA_TERMINALS.has(callee.property.name)) return false;
|
|
13148
|
+
if (callee.object.type === import_utils68.AST_NODE_TYPES.CallExpression) return isSchemaConstruction(callee.object);
|
|
13149
|
+
return factoryName(node, FACTORIES) !== null || factoryName(node, COMPOSITE_FACTORIES) !== null;
|
|
13150
|
+
}
|
|
13151
|
+
function hasEagerComputation(node) {
|
|
13152
|
+
return subtreeSome2(node, (inner) => inner.type === import_utils68.AST_NODE_TYPES.NewExpression || inner.type === import_utils68.AST_NODE_TYPES.TaggedTemplateExpression || inner.type === import_utils68.AST_NODE_TYPES.CallExpression && !isSchemaConstruction(inner), true);
|
|
13153
|
+
}
|
|
12813
13154
|
function isSharedEnumDomain(node, factory) {
|
|
12814
13155
|
if (factory !== "enum") return false;
|
|
12815
13156
|
const [argument] = node.arguments;
|
|
@@ -12878,7 +13219,7 @@ var prefer_module_level_refined_schema_default = createRule({
|
|
|
12878
13219
|
const enclosing = outermostEnclosingFunction2(node);
|
|
12879
13220
|
if (enclosing === void 0) return;
|
|
12880
13221
|
const expression = schemaExpression2(node);
|
|
12881
|
-
if (readsReceiver2(expression) || buildsLocalizedText2(expression) || !closesOverNothing(expression, enclosing))
|
|
13222
|
+
if (hasEagerComputation(expression) || readsReceiver2(expression) || buildsLocalizedText2(expression) || !closesOverNothing(expression, enclosing))
|
|
12882
13223
|
return;
|
|
12883
13224
|
context.report({ node, messageId: "hoistRefinedSchema" });
|
|
12884
13225
|
}
|
|
@@ -12906,7 +13247,7 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
|
|
|
12906
13247
|
outcome: "no-match",
|
|
12907
13248
|
files: [{
|
|
12908
13249
|
path: "src/schema.ts",
|
|
12909
|
-
source: "import { z } from 'zod'; export const Version = z.literal([1, 2, 3]);"
|
|
13250
|
+
source: "import { z } from 'zod/v4'; export const Version = z.literal([1, 2, 3]);"
|
|
12910
13251
|
}],
|
|
12911
13252
|
focusPath: "src/schema.ts",
|
|
12912
13253
|
expectedCount: 0,
|
|
@@ -12918,7 +13259,7 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
|
|
|
12918
13259
|
outcome: "match",
|
|
12919
13260
|
files: [{
|
|
12920
13261
|
path: "src/schema.ts",
|
|
12921
|
-
source: "import { z } from 'zod'; export const Version = z.union([z.literal(1), z.literal(2), z.literal(3)]);"
|
|
13262
|
+
source: "import { z } from 'zod/v4'; export const Version = z.union([z.literal(1), z.literal(2), z.literal(3)]);"
|
|
12922
13263
|
}],
|
|
12923
13264
|
focusPath: "src/schema.ts",
|
|
12924
13265
|
expectedCount: 1,
|
|
@@ -13077,11 +13418,11 @@ var import_utils71 = require("@typescript-eslint/utils");
|
|
|
13077
13418
|
var PREFER_NAMED_COMPLEX_RETURN_TYPE_DOCUMENTATION = {
|
|
13078
13419
|
summary: "Prefer a named contract for structurally complex function return types.",
|
|
13079
13420
|
rationale: "A large inline return annotation hides a reusable domain concept and makes signatures difficult to scan.",
|
|
13080
|
-
remediation: "
|
|
13421
|
+
remediation: "Name the complex nested shape while preserving its generic wrappers and type parameters; reference the named contract from the return annotation.",
|
|
13081
13422
|
category: "maintainability",
|
|
13082
13423
|
limitations: [
|
|
13083
13424
|
"Only explicit object types with at least three members and unions with at least three object variants are reported.",
|
|
13084
|
-
"
|
|
13425
|
+
"Any single-argument generic wrapper is traversed to find nested shapes, not assumed semantically transparent. Inferred return types are outside this rule; extraction is manual and must preserve locally bound type parameters."
|
|
13085
13426
|
],
|
|
13086
13427
|
examples: [
|
|
13087
13428
|
{ id: "named-result", title: "Name a multi-state result", outcome: "no-match", files: [{ path: "src/queue.ts", source: "type ClaimResult = { state: 'idle' } | { state: 'waiting'; retryAt: number } | { state: 'claimed'; id: string }; export function claim(): ClaimResult { return { state: 'idle' }; }" }], focusPath: "src/queue.ts", expectedCount: 0, public: true },
|
|
@@ -13237,12 +13578,14 @@ var import_utils73 = require("@typescript-eslint/utils");
|
|
|
13237
13578
|
var PREFER_NODE_CRYPTO_HASH_DOCUMENTATION = {
|
|
13238
13579
|
summary: "Prefer the modern one-shot node:crypto hash API when streaming state is unnecessary.",
|
|
13239
13580
|
rationale: "A createHash-update-digest chain allocates mutable streaming state for a single in-memory value; Node's built-in hash function expresses the one-shot operation directly and can use its optimized fast path.",
|
|
13240
|
-
remediation: "
|
|
13581
|
+
remediation: "On a supported Node runtime, consider hash(algorithm, value, encoding). Preserve the output encoding explicitly: digest() returns a Buffer, while hash defaults to hex. Keep createHash for streams or multiple updates.",
|
|
13241
13582
|
category: "performance",
|
|
13242
13583
|
limitations: [
|
|
13243
13584
|
"Only bindings and inline calls with statically proven provenance from crypto or node:crypto are analyzed; arbitrary assignments and dynamic module specifiers are excluded.",
|
|
13244
|
-
"Only a literal algorithm with exactly one update call is reported; streaming and incremental hashes remain valid."
|
|
13585
|
+
"Only a literal algorithm with exactly one update call is reported; streaming and incremental hashes remain valid.",
|
|
13586
|
+
"Runtime support and output encoding require manual review; no autofix or guaranteed speedup is promised."
|
|
13245
13587
|
],
|
|
13588
|
+
references: ["https://nodejs.org/api/crypto.html#cryptohashalgorithm-data-options"],
|
|
13246
13589
|
examples: [
|
|
13247
13590
|
{ id: "one-shot-hash", title: "Use Node's one-shot hash API", outcome: "no-match", files: [{ path: "case.ts", source: "import { hash } from 'node:crypto'; export const digest = hash('sha256', 'value', 'hex');" }], focusPath: "case.ts", expectedCount: 0, public: true },
|
|
13248
13591
|
{ id: "mutable-one-shot-chain", title: "Avoid mutable state for one value", outcome: "match", files: [{ path: "case.ts", source: "import { createHash } from 'node:crypto'; export const digest = createHash('sha256').update('value').digest('hex');" }], focusPath: "case.ts", expectedCount: 1, public: true }
|
|
@@ -13371,6 +13714,7 @@ var PREFER_NODE_FS_PROMISES_DOCUMENTATION = {
|
|
|
13371
13714
|
category: "performance",
|
|
13372
13715
|
limitations: [
|
|
13373
13716
|
"Tests and generated files are excluded.",
|
|
13717
|
+
"This recommendation applies to Node-compatible runtimes. Changing a synchronous API to a promise changes its caller contract; review startup-only work and APIs that require synchronous execution rather than mechanically adding await.",
|
|
13374
13718
|
"ESLint rule implementations under src/rules are excluded because visitor creation and execution are synchronous by contract.",
|
|
13375
13719
|
"Only statically identifiable node:fs loads are inspected; filesystem objects passed through arbitrary functions or assignments require type-aware analysis."
|
|
13376
13720
|
],
|
|
@@ -13389,14 +13733,14 @@ function memberName5(node) {
|
|
|
13389
13733
|
function unwrapAwait2(node) {
|
|
13390
13734
|
return node.type === import_utils74.AST_NODE_TYPES.AwaitExpression ? node.argument : node;
|
|
13391
13735
|
}
|
|
13392
|
-
function isFsLoader(node) {
|
|
13736
|
+
function isFsLoader(node, isGlobal) {
|
|
13393
13737
|
const expression = unwrapAwait2(node);
|
|
13394
13738
|
if (expression.type === import_utils74.AST_NODE_TYPES.ImportExpression) return isFsSpecifier(expression.source);
|
|
13395
13739
|
if (expression.type !== import_utils74.AST_NODE_TYPES.CallExpression || expression.arguments.length !== 1) return false;
|
|
13396
13740
|
const [argument] = expression.arguments;
|
|
13397
13741
|
if (argument === void 0 || argument.type === import_utils74.AST_NODE_TYPES.SpreadElement || !isFsSpecifier(argument)) return false;
|
|
13398
|
-
if (expression.callee.type === import_utils74.AST_NODE_TYPES.Identifier) return expression.callee.name === "require";
|
|
13399
|
-
return expression.callee.type === import_utils74.AST_NODE_TYPES.MemberExpression && expression.callee.object.type === import_utils74.AST_NODE_TYPES.Identifier && expression.callee.object.name === "process" && memberName5(expression.callee) === "getBuiltinModule";
|
|
13742
|
+
if (expression.callee.type === import_utils74.AST_NODE_TYPES.Identifier) return expression.callee.name === "require" && isGlobal(expression.callee);
|
|
13743
|
+
return expression.callee.type === import_utils74.AST_NODE_TYPES.MemberExpression && expression.callee.object.type === import_utils74.AST_NODE_TYPES.Identifier && expression.callee.object.name === "process" && isGlobal(expression.callee.object) && memberName5(expression.callee) === "getBuiltinModule";
|
|
13400
13744
|
}
|
|
13401
13745
|
function isFsSpecifier(node) {
|
|
13402
13746
|
return node.type === import_utils74.AST_NODE_TYPES.Literal && (node.value === "node:fs" || node.value === "fs");
|
|
@@ -13423,13 +13767,26 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13423
13767
|
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text) || normalizedFilename.includes("src/rules/"))
|
|
13424
13768
|
return {};
|
|
13425
13769
|
const namespaces = /* @__PURE__ */ new Set();
|
|
13770
|
+
const bindingOf = (node) => import_utils74.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
|
|
13771
|
+
const isGlobal = (node) => {
|
|
13772
|
+
const binding = bindingOf(node);
|
|
13773
|
+
return binding === null || binding.defs.length === 0;
|
|
13774
|
+
};
|
|
13775
|
+
const isNamespace = (node) => {
|
|
13776
|
+
const binding = bindingOf(node);
|
|
13777
|
+
return binding !== null && namespaces.has(binding) && !binding.references.some((reference) => reference.isWrite() && reference.init !== true);
|
|
13778
|
+
};
|
|
13779
|
+
const recordNamespace = (node) => {
|
|
13780
|
+
const binding = bindingOf(node);
|
|
13781
|
+
if (binding !== null) namespaces.add(binding);
|
|
13782
|
+
};
|
|
13426
13783
|
return {
|
|
13427
13784
|
ImportDeclaration(node) {
|
|
13428
13785
|
if (node.source.value !== "node:fs" && node.source.value !== "fs") return;
|
|
13429
13786
|
const synchronousImports = [];
|
|
13430
13787
|
for (const specifier of node.specifiers) {
|
|
13431
13788
|
if (specifier.type === import_utils74.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils74.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
13432
|
-
|
|
13789
|
+
recordNamespace(specifier.local);
|
|
13433
13790
|
continue;
|
|
13434
13791
|
}
|
|
13435
13792
|
if (specifier.type === import_utils74.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils74.AST_NODE_TYPES.Identifier && specifier.imported.name.endsWith("Sync")) synchronousImports.push(specifier.imported.name);
|
|
@@ -13443,10 +13800,10 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13443
13800
|
}
|
|
13444
13801
|
},
|
|
13445
13802
|
VariableDeclarator(node) {
|
|
13446
|
-
if (node.init === null || !isFsLoader(node.init) && (node.init.type !== import_utils74.AST_NODE_TYPES.Identifier || !
|
|
13803
|
+
if (node.init === null || !isFsLoader(node.init, isGlobal) && (node.init.type !== import_utils74.AST_NODE_TYPES.Identifier || !isNamespace(node.init)))
|
|
13447
13804
|
return;
|
|
13448
13805
|
if (node.id.type === import_utils74.AST_NODE_TYPES.Identifier) {
|
|
13449
|
-
|
|
13806
|
+
recordNamespace(node.id);
|
|
13450
13807
|
return;
|
|
13451
13808
|
}
|
|
13452
13809
|
if (node.id.type !== import_utils74.AST_NODE_TYPES.ObjectPattern) return;
|
|
@@ -13467,7 +13824,7 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13467
13824
|
const name = memberName5(node);
|
|
13468
13825
|
if (name?.endsWith("Sync") !== true) return;
|
|
13469
13826
|
const object = unwrapAwait2(node.object);
|
|
13470
|
-
if (object.type === import_utils74.AST_NODE_TYPES.Identifier &&
|
|
13827
|
+
if (object.type === import_utils74.AST_NODE_TYPES.Identifier && isNamespace(object) || isFsLoader(object, isGlobal)) {
|
|
13471
13828
|
context.report({ node, messageId: "preferAsyncFs", data: { name } });
|
|
13472
13829
|
}
|
|
13473
13830
|
}
|
|
@@ -13478,11 +13835,11 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13478
13835
|
// src/rules/prefer-non-nullable-collection.ts
|
|
13479
13836
|
var import_utils75 = require("@typescript-eslint/utils");
|
|
13480
13837
|
var PREFER_NON_NULLABLE_COLLECTION_DOCUMENTATION = {
|
|
13481
|
-
summary: "Suggest
|
|
13838
|
+
summary: "Suggest reviewing nullish arrays that use local empty-array defaults or a shared null-or-empty guard.",
|
|
13482
13839
|
rationale: "A redundant nullish collection state spreads defaults and guards through consumers without carrying information.",
|
|
13483
13840
|
remediation: "Use a non-null collection type and normalize omitted input to an empty collection at the boundary.",
|
|
13484
13841
|
category: "maintainability",
|
|
13485
|
-
limitations: ["
|
|
13842
|
+
limitations: ["Recognized defaults and guards are manual modeling prompts, not proof of equivalence for every caller or later use. Exported wire shapes and unknown object escapes are excluded; preserve meaningful null states and boundary compatibility."],
|
|
13486
13843
|
examples: [
|
|
13487
13844
|
{ id: "non-null-array", title: "Model an always-present collection", outcome: "no-match", files: [{ path: "src/search.ts", source: "interface Input { items: string[] } function search({ items }: Input) { return items.length; }" }], focusPath: "src/search.ts", expectedCount: 0, public: true },
|
|
13488
13845
|
{ id: "defaulted-nullish-array", title: "Do not retain a redundant nullish state", outcome: "match", files: [{ path: "src/search.ts", source: "interface Input { items: string[] | undefined } function search({ items = [] }: Input) { return items.length; }" }], focusPath: "src/search.ts", expectedCount: 1, public: true }
|
|
@@ -13551,22 +13908,6 @@ function sameAccess(node, access) {
|
|
|
13551
13908
|
}
|
|
13552
13909
|
return node.type === import_utils75.AST_NODE_TYPES.MemberExpression && !node.computed && node.object.type === import_utils75.AST_NODE_TYPES.Identifier && node.object.name === access.object && node.property.type === import_utils75.AST_NODE_TYPES.Identifier && node.property.name === access.property;
|
|
13553
13910
|
}
|
|
13554
|
-
function isNullGuard(node, access) {
|
|
13555
|
-
if (node.type === import_utils75.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && (sameAccess(node.argument, access) || optionalMemberLengthOf(node.argument, access))) return true;
|
|
13556
|
-
if (node.type !== import_utils75.AST_NODE_TYPES.BinaryExpression || !["==", "==="].includes(node.operator)) {
|
|
13557
|
-
return false;
|
|
13558
|
-
}
|
|
13559
|
-
const nullish = (value) => value.type === import_utils75.AST_NODE_TYPES.Literal && value.value === null || value.type === import_utils75.AST_NODE_TYPES.Identifier && value.name === "undefined";
|
|
13560
|
-
return sameAccess(node.left, access) && nullish(node.right) || sameAccess(node.right, access) && nullish(node.left);
|
|
13561
|
-
}
|
|
13562
|
-
function isEmptyGuard(node, access) {
|
|
13563
|
-
if (node.type === import_utils75.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && memberLengthOf(node.argument, access)) return true;
|
|
13564
|
-
if (node.type !== import_utils75.AST_NODE_TYPES.BinaryExpression || !["==", "===", "<="].includes(node.operator)) {
|
|
13565
|
-
return false;
|
|
13566
|
-
}
|
|
13567
|
-
const zero = (value) => value.type === import_utils75.AST_NODE_TYPES.Literal && value.value === 0;
|
|
13568
|
-
return memberLengthOf(node.left, access) && zero(node.right) || memberLengthOf(node.right, access) && zero(node.left);
|
|
13569
|
-
}
|
|
13570
13911
|
function memberLengthOf(node, access) {
|
|
13571
13912
|
const target = node.type === import_utils75.AST_NODE_TYPES.ChainExpression ? node.expression : node;
|
|
13572
13913
|
return target.type === import_utils75.AST_NODE_TYPES.MemberExpression && !target.computed && target.property.type === import_utils75.AST_NODE_TYPES.Identifier && target.property.name === "length" && sameAccess(target.object, access);
|
|
@@ -13581,7 +13922,24 @@ function hasEquivalentLeadingGuard(fn, access, visitorKeys) {
|
|
|
13581
13922
|
const terminating = first.consequent.type === import_utils75.AST_NODE_TYPES.ReturnStatement || first.consequent.type === import_utils75.AST_NODE_TYPES.ThrowStatement || first.consequent.type === import_utils75.AST_NODE_TYPES.BlockStatement && first.consequent.body.length === 1 && (first.consequent.body[0]?.type === import_utils75.AST_NODE_TYPES.ReturnStatement || first.consequent.body[0]?.type === import_utils75.AST_NODE_TYPES.ThrowStatement);
|
|
13582
13923
|
if (!terminating) return false;
|
|
13583
13924
|
if (contains(first.consequent, visitorKeys, (node) => sameAccess(node, access))) return false;
|
|
13584
|
-
|
|
13925
|
+
if (first.test.type === import_utils75.AST_NODE_TYPES.UnaryExpression && first.test.operator === "!" && optionalMemberLengthOf(first.test.argument, access)) return true;
|
|
13926
|
+
return first.test.type === import_utils75.AST_NODE_TYPES.LogicalExpression && first.test.operator === "||" && isNullGuard(first.test.left, access) && isEmptyGuard(first.test.right, access);
|
|
13927
|
+
}
|
|
13928
|
+
function isNullGuard(node, access) {
|
|
13929
|
+
if (node.type === import_utils75.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && (sameAccess(node.argument, access) || optionalMemberLengthOf(node.argument, access))) return true;
|
|
13930
|
+
if (node.type !== import_utils75.AST_NODE_TYPES.BinaryExpression || !["==", "==="].includes(node.operator)) {
|
|
13931
|
+
return false;
|
|
13932
|
+
}
|
|
13933
|
+
const nullish = (value) => value.type === import_utils75.AST_NODE_TYPES.Literal && value.value === null || value.type === import_utils75.AST_NODE_TYPES.Identifier && value.name === "undefined";
|
|
13934
|
+
return sameAccess(node.left, access) && nullish(node.right) || sameAccess(node.right, access) && nullish(node.left);
|
|
13935
|
+
}
|
|
13936
|
+
function isEmptyGuard(node, access) {
|
|
13937
|
+
if (node.type === import_utils75.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && memberLengthOf(node.argument, access)) return true;
|
|
13938
|
+
if (node.type !== import_utils75.AST_NODE_TYPES.BinaryExpression || !["==", "===", "<="].includes(node.operator)) {
|
|
13939
|
+
return false;
|
|
13940
|
+
}
|
|
13941
|
+
const zero = (value) => value.type === import_utils75.AST_NODE_TYPES.Literal && value.value === 0;
|
|
13942
|
+
return memberLengthOf(node.left, access) && zero(node.right) || node.operator !== "<=" && memberLengthOf(node.right, access) && zero(node.left);
|
|
13585
13943
|
}
|
|
13586
13944
|
function contains(node, visitorKeys, predicate) {
|
|
13587
13945
|
if (predicate(node)) return true;
|
|
@@ -13619,7 +13977,7 @@ function memberIsOnlyCoalesced(context, object, property, fn) {
|
|
|
13619
13977
|
if (!belongsToFunction(reference.identifier, fn)) return [null];
|
|
13620
13978
|
const parent = reference.identifier.parent;
|
|
13621
13979
|
if (parent?.type === import_utils75.AST_NODE_TYPES.MemberExpression && !parent.computed && parent.object === reference.identifier && parent.property.type === import_utils75.AST_NODE_TYPES.Identifier && parent.property.name === property) return [parent];
|
|
13622
|
-
return [];
|
|
13980
|
+
return parent?.type === import_utils75.AST_NODE_TYPES.MemberExpression && parent.object === reference.identifier ? [] : [null];
|
|
13623
13981
|
});
|
|
13624
13982
|
return accesses.length > 0 && accesses.every((access) => access !== null && directlyCoalesced(access));
|
|
13625
13983
|
}
|
|
@@ -13629,11 +13987,11 @@ var prefer_non_nullable_collection_default = createRule({
|
|
|
13629
13987
|
meta: {
|
|
13630
13988
|
type: "suggestion",
|
|
13631
13989
|
docs: {
|
|
13632
|
-
description: "Suggest
|
|
13990
|
+
description: "Suggest reviewing nullish arrays that use local empty-array defaults or a shared null-or-empty guard."
|
|
13633
13991
|
},
|
|
13634
13992
|
schema: [],
|
|
13635
13993
|
messages: {
|
|
13636
|
-
preferNonNullableCollection: "`{{name}}`
|
|
13994
|
+
preferNonNullableCollection: "`{{name}}` uses an empty-array default or shared null-or-empty guard; consider a non-null array after checking that the nullish state carries no separate meaning."
|
|
13637
13995
|
}
|
|
13638
13996
|
},
|
|
13639
13997
|
defaultOptions: [],
|
|
@@ -13882,12 +14240,12 @@ var ts4 = __toESM(require("typescript"), 1);
|
|
|
13882
14240
|
var PREFER_AWAIT_IN_ASYNC_RETURN_DOCUMENTATION = {
|
|
13883
14241
|
summary: "Prefer explicit `await` when an async function directly returns one typed Promise `.then` transform.",
|
|
13884
14242
|
rationale: "Mixing a directly returned Promise callback into otherwise async control flow makes sequencing and failures harder to read.",
|
|
13885
|
-
remediation: "
|
|
14243
|
+
remediation: "Consider awaiting the Promise and returning the transformed value with ordinary async statements. Preserve catch boundaries, callback behavior, and observable scheduling when rewriting manually.",
|
|
13886
14244
|
category: "maintainability",
|
|
13887
14245
|
since: "15.6.3",
|
|
13888
14246
|
limitations: [
|
|
13889
14247
|
"Only a single directly returned `.then` call with an inline callback is checked.",
|
|
13890
|
-
"The receiver must be proven Promise-like by TypeScript;
|
|
14248
|
+
"The receiver must be proven Promise-like by TypeScript; an earlier chain can still produce that receiver. Untyped receivers, rejection handlers and named callbacks are excluded. This is not the upstream return-await policy and no scheduling equivalence is promised.",
|
|
13891
14249
|
"Direct loader callbacks passed to resolved `React.lazy` and `next/dynamic` imports are excluded because returning the module Promise is their framework contract."
|
|
13892
14250
|
],
|
|
13893
14251
|
examples: [
|
|
@@ -14028,12 +14386,17 @@ var import_utils78 = require("@typescript-eslint/utils");
|
|
|
14028
14386
|
var PREFER_SCHEMA_FOR_API_PAYLOAD_DOCUMENTATION = {
|
|
14029
14387
|
summary: "Require Zod (or similar) schema validation on `response.json()` / `JSON.parse()` results before property access.",
|
|
14030
14388
|
rationale: "External JSON is untrusted at runtime even when its expected TypeScript shape is known statically.",
|
|
14031
|
-
remediation: "
|
|
14389
|
+
remediation: "For external payloads, parse through a schema or establish runtime validation before reading fields. Review validator implementations separately rather than recursively requiring another schema.",
|
|
14032
14390
|
category: "correctness",
|
|
14033
|
-
limitations: [
|
|
14391
|
+
limitations: [
|
|
14392
|
+
"A JSON.parse call alone does not prove external input; exact native JSON stringify/parse round trips are excluded. Validator implementations and other non-network parsing can still require manual review rather than a schema rewrite.",
|
|
14393
|
+
"JSON.parse must resolve to the global JSON object. json() receivers require an unshadowed global Request/Response annotation or construction, or stable local aliases of global fetch results; imported, inferred and unknown response types are deliberately not inferred.",
|
|
14394
|
+
"Named validators remain conventions, not proof of their implementation. Their exemptions are confined to a valid branch or a preceding same-block validation statement; ignored predicate results and deferred callbacks do not validate later reads.",
|
|
14395
|
+
"Test fixtures, generated clients and recognized local-file reads are excluded. This is bounded local analysis, not a general control-flow or mutation proof."
|
|
14396
|
+
],
|
|
14034
14397
|
examples: [
|
|
14035
|
-
{ id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "async function load(response) { const body = UserSchema.parse(await response.json()); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
|
|
14036
|
-
{ id: "unvalidated-payload", title: "Do not trust response JSON directly", outcome: "match", files: [{ path: "src/client.ts", source: "async function load(response) { const body = await response.json(); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
|
|
14398
|
+
{ id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "async function load(response: Response) { const body = UserSchema.parse(await response.json()); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
|
|
14399
|
+
{ id: "unvalidated-payload", title: "Do not trust response JSON directly", outcome: "match", files: [{ path: "src/client.ts", source: "async function load(response: Response) { const body = await response.json(); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
|
|
14037
14400
|
]
|
|
14038
14401
|
};
|
|
14039
14402
|
var unwrap6 = (node) => {
|
|
@@ -14058,7 +14421,7 @@ var isSchemaParseReference = (node) => {
|
|
|
14058
14421
|
const inner = unwrap6(node);
|
|
14059
14422
|
return inner !== null && inner.type === import_utils78.AST_NODE_TYPES.MemberExpression && !inner.computed && inner.property.type === import_utils78.AST_NODE_TYPES.Identifier && (inner.property.name === "parse" || inner.property.name === "safeParse");
|
|
14060
14423
|
};
|
|
14061
|
-
var isRawPayloadSource = (node, isKnownLocalText) => {
|
|
14424
|
+
var isRawPayloadSource = (node, context, isKnownLocalText) => {
|
|
14062
14425
|
let current = unwrap6(node);
|
|
14063
14426
|
if (current === null) return false;
|
|
14064
14427
|
if (current.type === import_utils78.AST_NODE_TYPES.AwaitExpression) {
|
|
@@ -14076,13 +14439,31 @@ var isRawPayloadSource = (node, isKnownLocalText) => {
|
|
|
14076
14439
|
return false;
|
|
14077
14440
|
}
|
|
14078
14441
|
if (property.name === "json") {
|
|
14079
|
-
return
|
|
14442
|
+
return !callee.computed && current.arguments.length === 0 && isResponseSource(callee.object, context);
|
|
14080
14443
|
}
|
|
14081
14444
|
if (PROMISE_CHAIN_METHODS.has(property.name)) {
|
|
14082
|
-
return !current.arguments.some(isSchemaParseReference) && isRawPayloadSource(callee.object);
|
|
14445
|
+
return !current.arguments.some(isSchemaParseReference) && isRawPayloadSource(callee.object, context, isKnownLocalText);
|
|
14083
14446
|
}
|
|
14084
14447
|
const object = unwrap6(callee.object);
|
|
14085
|
-
|
|
14448
|
+
const input = unwrap6(current.arguments[0]);
|
|
14449
|
+
if (input?.type === import_utils78.AST_NODE_TYPES.CallExpression && input.arguments.length === 1 && input.callee.type === import_utils78.AST_NODE_TYPES.MemberExpression && !input.callee.computed && input.callee.object.type === import_utils78.AST_NODE_TYPES.Identifier && input.callee.object.name === "JSON" && input.callee.property.type === import_utils78.AST_NODE_TYPES.Identifier && input.callee.property.name === "stringify" && (import_utils78.ASTUtils.findVariable(context.sourceCode.getScope(input.callee.object), "JSON")?.defs.length ?? 0) === 0) return false;
|
|
14450
|
+
return property.name === "parse" && object !== null && object.type === import_utils78.AST_NODE_TYPES.Identifier && object.name === "JSON" && (import_utils78.ASTUtils.findVariable(context.sourceCode.getScope(object), "JSON")?.defs.length ?? 0) === 0 && !isLocalFileRead(current.arguments[0]) && isKnownLocalText?.(current.arguments[0]) !== true;
|
|
14451
|
+
};
|
|
14452
|
+
var isResponseSource = (node, context, seen = /* @__PURE__ */ new Set()) => {
|
|
14453
|
+
let current = unwrap6(node);
|
|
14454
|
+
if (current?.type === import_utils78.AST_NODE_TYPES.AwaitExpression) current = unwrap6(current.argument);
|
|
14455
|
+
if (current === null || seen.has(current)) return false;
|
|
14456
|
+
seen.add(current);
|
|
14457
|
+
const isGlobal = (identifier) => (import_utils78.ASTUtils.findVariable(context.sourceCode.getScope(identifier), identifier.name)?.defs.length ?? 0) === 0;
|
|
14458
|
+
if (current.type === import_utils78.AST_NODE_TYPES.CallExpression) return current.callee.type === import_utils78.AST_NODE_TYPES.Identifier && current.callee.name === "fetch" && isGlobal(current.callee);
|
|
14459
|
+
if (current.type === import_utils78.AST_NODE_TYPES.NewExpression) return current.callee.type === import_utils78.AST_NODE_TYPES.Identifier && ["Request", "Response"].includes(current.callee.name) && isGlobal(current.callee);
|
|
14460
|
+
if (current.type !== import_utils78.AST_NODE_TYPES.Identifier) return false;
|
|
14461
|
+
const binding = import_utils78.ASTUtils.findVariable(context.sourceCode.getScope(current), current.name);
|
|
14462
|
+
if (binding?.defs.length !== 1 || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
14463
|
+
const definition = binding.defs[0];
|
|
14464
|
+
const annotation = definition?.name.type === import_utils78.AST_NODE_TYPES.Identifier ? definition.name.typeAnnotation?.typeAnnotation : void 0;
|
|
14465
|
+
if (annotation?.type === import_utils78.AST_NODE_TYPES.TSTypeReference && annotation.typeName.type === import_utils78.AST_NODE_TYPES.Identifier && ["Request", "Response"].includes(annotation.typeName.name) && isGlobal(annotation.typeName)) return true;
|
|
14466
|
+
return definition?.type === "Variable" && definition.parent.kind === "const" && definition.node.init !== null && isResponseSource(definition.node.init, context, seen);
|
|
14086
14467
|
};
|
|
14087
14468
|
var FILE_READ_RE = /^(readFile|readFileSync|readJson|readJsonSync|readJSON)$/;
|
|
14088
14469
|
var isDirectLocalFileRead = (node) => {
|
|
@@ -14348,7 +14729,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14348
14729
|
},
|
|
14349
14730
|
schema: [],
|
|
14350
14731
|
messages: {
|
|
14351
|
-
unparsedJsonAccess: "
|
|
14732
|
+
unparsedJsonAccess: "Review property access on parsed JSON without a recognized validation boundary. For external payloads, validate before reading fields; validator implementations need manual review, not a recursive schema rewrite."
|
|
14352
14733
|
}
|
|
14353
14734
|
},
|
|
14354
14735
|
defaultOptions: [],
|
|
@@ -14359,6 +14740,40 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14359
14740
|
const unvalidatedVariables = /* @__PURE__ */ new Set();
|
|
14360
14741
|
const aliasGroups = /* @__PURE__ */ new Map();
|
|
14361
14742
|
const localFileTextVariables = /* @__PURE__ */ new Set();
|
|
14743
|
+
const namedGuards = /* @__PURE__ */ new Map();
|
|
14744
|
+
const guardDominates = (use, call) => {
|
|
14745
|
+
if (context.sourceCode.getScope(use).variableScope !== context.sourceCode.getScope(call).variableScope) return false;
|
|
14746
|
+
let guard = call;
|
|
14747
|
+
let positive = true;
|
|
14748
|
+
while (guard.parent.type === import_utils78.AST_NODE_TYPES.UnaryExpression && guard.parent.operator === "!") {
|
|
14749
|
+
positive = !positive;
|
|
14750
|
+
guard = guard.parent;
|
|
14751
|
+
}
|
|
14752
|
+
while (positive && guard.parent.type === import_utils78.AST_NODE_TYPES.LogicalExpression && guard.parent.operator === "&&") guard = guard.parent;
|
|
14753
|
+
const branch = guard.parent;
|
|
14754
|
+
if (branch.type === import_utils78.AST_NODE_TYPES.IfStatement && branch.test === guard) {
|
|
14755
|
+
if (positive && nodeWithin2(use, branch.consequent)) return true;
|
|
14756
|
+
if (!positive && branch.alternate !== null && nodeWithin2(use, branch.alternate)) return true;
|
|
14757
|
+
const terminal = branch.consequent.type === import_utils78.AST_NODE_TYPES.BlockStatement ? branch.consequent.body.at(-1) : branch.consequent;
|
|
14758
|
+
if (!positive && (terminal?.type === import_utils78.AST_NODE_TYPES.ThrowStatement || terminal?.type === import_utils78.AST_NODE_TYPES.ReturnStatement)) {
|
|
14759
|
+
let statement2 = use;
|
|
14760
|
+
while (statement2.parent !== void 0 && statement2.parent !== branch.parent && statement2.parent.type !== import_utils78.AST_NODE_TYPES.Program) {
|
|
14761
|
+
if (statement2.type === import_utils78.AST_NODE_TYPES.FunctionDeclaration || statement2.type === import_utils78.AST_NODE_TYPES.FunctionExpression || statement2.type === import_utils78.AST_NODE_TYPES.ArrowFunctionExpression) return false;
|
|
14762
|
+
statement2 = statement2.parent;
|
|
14763
|
+
}
|
|
14764
|
+
return statement2.parent === branch.parent && statement2.range[0] > branch.range[1];
|
|
14765
|
+
}
|
|
14766
|
+
}
|
|
14767
|
+
if (branch.type === import_utils78.AST_NODE_TYPES.ConditionalExpression && branch.test === guard) return nodeWithin2(use, positive ? branch.consequent : branch.alternate);
|
|
14768
|
+
if (positive && branch.type === import_utils78.AST_NODE_TYPES.WhileStatement && branch.test === guard) return nodeWithin2(use, branch.body);
|
|
14769
|
+
if (call.parent.type !== import_utils78.AST_NODE_TYPES.ExpressionStatement || call.callee.type !== import_utils78.AST_NODE_TYPES.Identifier || /^(?:is|has)[A-Z]/u.test(call.callee.name)) return false;
|
|
14770
|
+
let statement = use;
|
|
14771
|
+
while (statement.parent !== void 0 && statement.parent !== call.parent.parent && statement.parent.type !== import_utils78.AST_NODE_TYPES.Program) {
|
|
14772
|
+
if (statement.type === import_utils78.AST_NODE_TYPES.FunctionDeclaration || statement.type === import_utils78.AST_NODE_TYPES.FunctionExpression || statement.type === import_utils78.AST_NODE_TYPES.ArrowFunctionExpression) return false;
|
|
14773
|
+
statement = statement.parent;
|
|
14774
|
+
}
|
|
14775
|
+
return statement.parent === call.parent.parent && statement.range[0] > call.parent.range[1];
|
|
14776
|
+
};
|
|
14362
14777
|
const localFileTextRef = (node, scope) => {
|
|
14363
14778
|
const unwrapped = unwrap6(node);
|
|
14364
14779
|
if (unwrapped?.type !== import_utils78.AST_NODE_TYPES.Identifier) return null;
|
|
@@ -14375,6 +14790,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14375
14790
|
};
|
|
14376
14791
|
const clearBinding = (variable) => {
|
|
14377
14792
|
unvalidatedVariables.delete(variable);
|
|
14793
|
+
namedGuards.delete(variable);
|
|
14378
14794
|
const group = aliasGroups.get(variable);
|
|
14379
14795
|
aliasGroups.delete(variable);
|
|
14380
14796
|
group?.delete(variable);
|
|
@@ -14409,10 +14825,24 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14409
14825
|
};
|
|
14410
14826
|
const isFullyNarrowedPattern = (declarator) => {
|
|
14411
14827
|
const declared = context.sourceCode.getDeclaredVariables(declarator);
|
|
14828
|
+
const statement = declarator.parent;
|
|
14829
|
+
const block = statement.parent;
|
|
14830
|
+
const followsRejectingGuard = (identifier) => {
|
|
14831
|
+
if (block.type !== import_utils78.AST_NODE_TYPES.BlockStatement && block.type !== import_utils78.AST_NODE_TYPES.Program) return false;
|
|
14832
|
+
if (context.sourceCode.getScope(identifier).variableScope !== context.sourceCode.getScope(declarator).variableScope) return false;
|
|
14833
|
+
return block.body.some((candidate2) => {
|
|
14834
|
+
if (candidate2.type !== import_utils78.AST_NODE_TYPES.IfStatement || candidate2.range[1] >= identifier.range[0] || bindingValidationPolarity(candidate2.test, identifier.name) !== "valid-when-false") return false;
|
|
14835
|
+
const terminal = candidate2.consequent.type === import_utils78.AST_NODE_TYPES.BlockStatement ? candidate2.consequent.body.at(-1) : candidate2.consequent;
|
|
14836
|
+
return terminal?.type === import_utils78.AST_NODE_TYPES.ThrowStatement || terminal?.type === import_utils78.AST_NODE_TYPES.ReturnStatement;
|
|
14837
|
+
});
|
|
14838
|
+
};
|
|
14412
14839
|
return declared.length > 0 && declared.every(
|
|
14413
|
-
(variable) => variable.references.some(
|
|
14414
|
-
|
|
14415
|
-
|
|
14840
|
+
(variable) => variable.references.some((reference) => isValidationRead(reference.identifier)) && variable.references.every((reference) => {
|
|
14841
|
+
const identifier = reference.identifier;
|
|
14842
|
+
if (reference.init === true) return true;
|
|
14843
|
+
if (reference.isWrite() || identifier.type !== import_utils78.AST_NODE_TYPES.Identifier) return false;
|
|
14844
|
+
return isValidationRead(identifier) || isUseWithinValidatedBranch(identifier, identifier.name) || followsRejectingGuard(identifier);
|
|
14845
|
+
})
|
|
14416
14846
|
);
|
|
14417
14847
|
};
|
|
14418
14848
|
const trackInitializer = (declarator, scope) => {
|
|
@@ -14420,7 +14850,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14420
14850
|
const variable = declaredVars[0];
|
|
14421
14851
|
if (variable === void 0) return;
|
|
14422
14852
|
const localText = (candidate2) => localFileTextRef(candidate2, scope) !== null;
|
|
14423
|
-
if (isRawPayloadSource(declarator.init, localText)) {
|
|
14853
|
+
if (isRawPayloadSource(declarator.init, context, localText)) {
|
|
14424
14854
|
trackRawBinding(variable);
|
|
14425
14855
|
return;
|
|
14426
14856
|
}
|
|
@@ -14441,6 +14871,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14441
14871
|
if (node.id.type === import_utils78.AST_NODE_TYPES.ObjectPattern || node.id.type === import_utils78.AST_NODE_TYPES.ArrayPattern) {
|
|
14442
14872
|
if (isRawPayloadSource(
|
|
14443
14873
|
node.init,
|
|
14874
|
+
context,
|
|
14444
14875
|
(candidate2) => localFileTextRef(candidate2, scope) !== null
|
|
14445
14876
|
)) {
|
|
14446
14877
|
if (!isFullyNarrowedPattern(node)) {
|
|
@@ -14460,7 +14891,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14460
14891
|
if (variable === null) return;
|
|
14461
14892
|
const isLocalText = (candidate2) => localFileTextRef(candidate2, scope) !== null;
|
|
14462
14893
|
updateLocalFileText(variable, node.right, scope);
|
|
14463
|
-
if (isRawPayloadSource(node.right, isLocalText)) {
|
|
14894
|
+
if (isRawPayloadSource(node.right, context, isLocalText)) {
|
|
14464
14895
|
trackRawBinding(variable);
|
|
14465
14896
|
} else {
|
|
14466
14897
|
const source = unvalidatedVariableRef(node.right, scope, unvalidatedVariables);
|
|
@@ -14472,6 +14903,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14472
14903
|
if (node.left.type === import_utils78.AST_NODE_TYPES.ObjectPattern || node.left.type === import_utils78.AST_NODE_TYPES.ArrayPattern) {
|
|
14473
14904
|
if (isRawPayloadSource(
|
|
14474
14905
|
node.right,
|
|
14906
|
+
context,
|
|
14475
14907
|
(candidate2) => localFileTextRef(candidate2, scope) !== null
|
|
14476
14908
|
)) {
|
|
14477
14909
|
context.report({
|
|
@@ -14501,7 +14933,13 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14501
14933
|
continue;
|
|
14502
14934
|
}
|
|
14503
14935
|
const variable = findVariable2(scope, unwrapped.name);
|
|
14504
|
-
if (variable !== null)
|
|
14936
|
+
if (variable !== null) {
|
|
14937
|
+
for (const alias of aliasGroups.get(variable) ?? [variable]) {
|
|
14938
|
+
const guards = namedGuards.get(alias) ?? [];
|
|
14939
|
+
guards.push(node);
|
|
14940
|
+
namedGuards.set(alias, guards);
|
|
14941
|
+
}
|
|
14942
|
+
}
|
|
14505
14943
|
}
|
|
14506
14944
|
},
|
|
14507
14945
|
MemberExpression(node) {
|
|
@@ -14511,6 +14949,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14511
14949
|
const obj = unwrap6(node.object);
|
|
14512
14950
|
if (isRawPayloadSource(
|
|
14513
14951
|
obj,
|
|
14952
|
+
context,
|
|
14514
14953
|
(candidate2) => localFileTextRef(candidate2, scope) !== null
|
|
14515
14954
|
)) {
|
|
14516
14955
|
const parent = node.parent;
|
|
@@ -14522,6 +14961,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14522
14961
|
}
|
|
14523
14962
|
const variable = obj?.type === import_utils78.AST_NODE_TYPES.Identifier ? unvalidatedVariableRef(obj, scope, unvalidatedVariables) : null;
|
|
14524
14963
|
if (variable !== null && obj?.type === import_utils78.AST_NODE_TYPES.Identifier) {
|
|
14964
|
+
if (namedGuards.get(variable)?.some((call) => guardDominates(node, call))) return;
|
|
14525
14965
|
if (isUseWithinValidatedBranch(node, obj.name)) {
|
|
14526
14966
|
return;
|
|
14527
14967
|
}
|
|
@@ -14547,7 +14987,7 @@ var PREFER_SHARED_ZOD_ENUM_DOCUMENTATION = {
|
|
|
14547
14987
|
rationale: "Inline or repeated literal domains hide a reusable contract and allow equivalent fields to drift independently.",
|
|
14548
14988
|
remediation: "Declare a module-level named Zod enum schema and reuse it at each field or contract site.",
|
|
14549
14989
|
category: "maintainability",
|
|
14550
|
-
limitations: ["Only direct z.enum calls with string-literal arrays are inspected; computed domains require review."],
|
|
14990
|
+
limitations: ["Only direct z.enum calls with string-literal arrays are inspected; computed domains require review. Equal values do not prove a shared business domain: retain local schemas when ownership, error customization, or future evolution differs, and review initialization order before extraction."],
|
|
14551
14991
|
examples: [
|
|
14552
14992
|
{ id: "shared-provider", title: "Reuse a named enum schema", outcome: "no-match", files: [{ path: "src/provider.ts", source: "import { z } from 'zod'; const ProviderSchema = z.enum(['agy', 'claude', 'sol']); const JobSchema = z.object({ provider: ProviderSchema }); const StatusSchema = z.object({ provider: ProviderSchema.optional() });" }], focusPath: "src/provider.ts", expectedCount: 0, public: true },
|
|
14553
14993
|
{ id: "inline-provider", title: "Do not inline enum domains in object fields", outcome: "match", files: [{ path: "src/provider.ts", source: "import { z } from 'zod'; const JobSchema = z.object({ provider: z.enum(['agy', 'claude', 'sol']) });" }], focusPath: "src/provider.ts", expectedCount: 1, public: true }
|
|
@@ -14589,16 +15029,22 @@ var prefer_shared_zod_enum_default = createRule({
|
|
|
14589
15029
|
create(context) {
|
|
14590
15030
|
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
14591
15031
|
const zodBindings = /* @__PURE__ */ new Set();
|
|
15032
|
+
const bindingOf = (node) => import_utils79.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
|
|
14592
15033
|
const seen = /* @__PURE__ */ new Set();
|
|
14593
15034
|
return {
|
|
14594
15035
|
ImportDeclaration(node) {
|
|
14595
15036
|
if (!isZodModule(node.source.value)) return;
|
|
14596
15037
|
for (const specifier of node.specifiers) {
|
|
14597
|
-
if (specifier.type === import_utils79.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils79.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils79.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils79.AST_NODE_TYPES.Identifier && specifier.imported.name === "z")
|
|
15038
|
+
if (specifier.type === import_utils79.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils79.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils79.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils79.AST_NODE_TYPES.Identifier && specifier.imported.name === "z") {
|
|
15039
|
+
const binding = bindingOf(specifier.local);
|
|
15040
|
+
if (binding !== null) zodBindings.add(binding);
|
|
15041
|
+
}
|
|
14598
15042
|
}
|
|
14599
15043
|
},
|
|
14600
15044
|
CallExpression(node) {
|
|
14601
|
-
if (node.callee.type !== import_utils79.AST_NODE_TYPES.MemberExpression || node.callee.computed || node.callee.object.type !== import_utils79.AST_NODE_TYPES.Identifier ||
|
|
15045
|
+
if (node.callee.type !== import_utils79.AST_NODE_TYPES.MemberExpression || node.callee.computed || node.callee.object.type !== import_utils79.AST_NODE_TYPES.Identifier || node.callee.property.type !== import_utils79.AST_NODE_TYPES.Identifier || node.callee.property.name !== "enum") return;
|
|
15046
|
+
const binding = bindingOf(node.callee.object);
|
|
15047
|
+
if (binding === null || !zodBindings.has(binding)) return;
|
|
14602
15048
|
const domain = literalDomain(node);
|
|
14603
15049
|
if (domain === null) return;
|
|
14604
15050
|
const key = JSON.stringify(domain);
|
|
@@ -15576,6 +16022,13 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
15576
16022
|
return null;
|
|
15577
16023
|
}
|
|
15578
16024
|
const actual = expectCall.arguments[0];
|
|
16025
|
+
const variable = import_utils83.ASTUtils.findVariable(sourceCode.getScope(expectCall.callee), expectCall.callee.name);
|
|
16026
|
+
if (variable !== null && variable.defs.some((definition) => {
|
|
16027
|
+
if (definition.node.type !== import_utils83.AST_NODE_TYPES.ImportSpecifier) return true;
|
|
16028
|
+
const declaration = definition.node.parent;
|
|
16029
|
+
const imported = definition.node.imported;
|
|
16030
|
+
return declaration.type !== import_utils83.AST_NODE_TYPES.ImportDeclaration || !["vitest", "@jest/globals", "@playwright/test", "bun:test"].includes(String(declaration.source.value)) || (imported.type === import_utils83.AST_NODE_TYPES.Identifier ? imported.name : imported.value) !== "expect";
|
|
16031
|
+
})) return null;
|
|
15579
16032
|
if (actual === void 0 || actual.type !== import_utils83.AST_NODE_TYPES.MemberExpression || actual.optional) {
|
|
15580
16033
|
return null;
|
|
15581
16034
|
}
|
|
@@ -15737,12 +16190,12 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
15737
16190
|
// src/rules/repeated-static-call-cases.ts
|
|
15738
16191
|
var import_utils84 = require("@typescript-eslint/utils");
|
|
15739
16192
|
var REPEATED_STATIC_CALL_CASES_DOCUMENTATION = {
|
|
15740
|
-
summary: "
|
|
15741
|
-
rationale: "Copy-pasted cases obscure the input table and stop later cases from being reported
|
|
15742
|
-
remediation: "
|
|
16193
|
+
summary: "Review three or more consecutive static-input call assertions as potential named cases.",
|
|
16194
|
+
rationale: "Copy-pasted cases obscure the input table, and a thrown assertion can stop later cases from being reported.",
|
|
16195
|
+
remediation: "If the calls are independent, use the runner's named parameterized cases or subtests. Preserve ordered state-transition scenarios as one test.",
|
|
15743
16196
|
category: "testing",
|
|
15744
16197
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
15745
|
-
limitations: ["Only consecutive top-level assertions with direct calls and
|
|
16198
|
+
limitations: ["Only consecutive top-level assertions with direct calls and static inputs and expected values are checked. Test-local callees and fixtures are excluded; imported or outer functions can still be stateful, so manual independence review is required. Not every runner supports test.each."],
|
|
15746
16199
|
examples: [
|
|
15747
16200
|
{ id: "parameterized", title: "Name each case", outcome: "no-match", files: [{ path: "src/parser.test.ts", source: "test.each([['a', true], ['b', false], ['c', true]])('parses %s', (input, expected) => { expect(parse(input)).toBe(expected); });" }], focusPath: "src/parser.test.ts", expectedCount: 0, public: true },
|
|
15748
16201
|
{ id: "repeated", title: "Do not repeat literal cases", outcome: "match", files: [{ path: "src/parser.test.ts", source: "test('parses', () => { expect(parse('a')).toBe(true); expect(parse('b')).toBe(false); expect(parse('c')).toBe(true); });" }], focusPath: "src/parser.test.ts", expectedCount: 1, public: true }
|
|
@@ -15823,7 +16276,7 @@ function staticShape(node) {
|
|
|
15823
16276
|
return "dynamic";
|
|
15824
16277
|
}
|
|
15825
16278
|
}
|
|
15826
|
-
function assertionShape(statement, context) {
|
|
16279
|
+
function assertionShape(statement, context, callback) {
|
|
15827
16280
|
if (statement.type !== import_utils84.AST_NODE_TYPES.ExpressionStatement || statement.expression.type !== import_utils84.AST_NODE_TYPES.CallExpression) return null;
|
|
15828
16281
|
const matcherCall = statement.expression;
|
|
15829
16282
|
if (matcherCall.callee.type !== import_utils84.AST_NODE_TYPES.MemberExpression || matcherCall.callee.computed || matcherCall.callee.property.type !== import_utils84.AST_NODE_TYPES.Identifier || matcherCall.arguments.length !== 1) return null;
|
|
@@ -15835,6 +16288,10 @@ function assertionShape(statement, context) {
|
|
|
15835
16288
|
const expected = matcherCall.arguments[0];
|
|
15836
16289
|
if (observed?.type !== import_utils84.AST_NODE_TYPES.CallExpression || observed.callee.type !== import_utils84.AST_NODE_TYPES.Identifier || observed.arguments.length === 0 || observed.arguments.some((arg) => arg.type === import_utils84.AST_NODE_TYPES.SpreadElement || !isStatic(arg)) || expected?.type === import_utils84.AST_NODE_TYPES.SpreadElement || expected === void 0 || !isStatic(expected)) return null;
|
|
15837
16290
|
const skeleton = `${observed.callee.name}/${observed.arguments.map((item) => staticShape(item)).join(",")}/${chain.modifiers.join(".")}/${matcher}/${staticShape(expected)}`;
|
|
16291
|
+
const binding = import_utils84.ASTUtils.findVariable(context.sourceCode.getScope(observed.callee), observed.callee.name);
|
|
16292
|
+
if (binding?.defs.some(
|
|
16293
|
+
(definition) => definition.node.range[0] >= callback.range[0] && definition.node.range[1] <= callback.range[1]
|
|
16294
|
+
)) return null;
|
|
15838
16295
|
const values = [...observed.arguments, expected].map((item) => context.sourceCode.getText(item)).join("\0");
|
|
15839
16296
|
return { statement, skeleton, values };
|
|
15840
16297
|
}
|
|
@@ -15854,9 +16311,9 @@ var repeated_static_call_cases_default = createRule({
|
|
|
15854
16311
|
documentation: REPEATED_STATIC_CALL_CASES_DOCUMENTATION,
|
|
15855
16312
|
meta: {
|
|
15856
16313
|
type: "suggestion",
|
|
15857
|
-
docs: { description:
|
|
16314
|
+
docs: { description: REPEATED_STATIC_CALL_CASES_DOCUMENTATION.summary },
|
|
15858
16315
|
schema: [],
|
|
15859
|
-
messages: { repeatedStaticCallCases: "These {{count}} consecutive assertions repeat
|
|
16316
|
+
messages: { repeatedStaticCallCases: "These {{count}} consecutive assertions repeat a call with static inputs. If independent, use named parameterized cases or subtests; preserve ordered scenarios as one test." }
|
|
15860
16317
|
},
|
|
15861
16318
|
defaultOptions: [],
|
|
15862
16319
|
create(context) {
|
|
@@ -15891,7 +16348,7 @@ var repeated_static_call_cases_default = createRule({
|
|
|
15891
16348
|
run = [];
|
|
15892
16349
|
};
|
|
15893
16350
|
for (const statement of node.body.body) {
|
|
15894
|
-
const shape = assertionShape(statement, context);
|
|
16351
|
+
const shape = assertionShape(statement, context, node);
|
|
15895
16352
|
if (shape === null || run.length > 0 && run[0]?.skeleton !== shape.skeleton) flush();
|
|
15896
16353
|
if (shape !== null) run.push(shape);
|
|
15897
16354
|
}
|
|
@@ -16910,10 +17367,12 @@ var require_fetch_timeout_default = createRule({
|
|
|
16910
17367
|
var import_utils88 = require("@typescript-eslint/utils");
|
|
16911
17368
|
var REQUIRE_INTERFACE_FOR_EXPORTED_CLASS_DOCUMENTATION = {
|
|
16912
17369
|
summary: "Require exported concrete classes with public behavior to declare a contract.",
|
|
16913
|
-
rationale: "
|
|
17370
|
+
rationale: "An explicit contract names the intended public capability separately from implementation details. TypeScript already supports structural compatibility; this is an architecture policy, not a prerequisite for substitution.",
|
|
16914
17371
|
remediation: "Declare a focused interface and add an implements clause, or inherit from an intentional base contract.",
|
|
16915
17372
|
category: "architecture",
|
|
16916
17373
|
limitations: [
|
|
17374
|
+
"JavaScript files are excluded because implements is TypeScript syntax; imported framework base contracts still require manual policy review.",
|
|
17375
|
+
"An exported injected service may also receive require-port-for-service: its service-boundary error and this general exported-contract warning intentionally enforce distinct policy scopes.",
|
|
16917
17376
|
"The warning-stage rule checks module-level class declarations and direct class-expression values exported directly, through local export specifiers, or through a default identifier; re-exports and expressions wrapped in other calls require review.",
|
|
16918
17377
|
"An extends clause satisfies the contract only when its target is a locally declared abstract class; imported base-class contracts require an explicit implements clause.",
|
|
16919
17378
|
"Static factories and data-only classes without public instance methods are outside the contract requirement."
|
|
@@ -16985,7 +17444,7 @@ var require_interface_for_exported_class_default = createRule({
|
|
|
16985
17444
|
},
|
|
16986
17445
|
defaultOptions: [],
|
|
16987
17446
|
create(context) {
|
|
16988
|
-
if (isTestFile(context.filename) || isStoryFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
17447
|
+
if (/\.(?:js|jsx|mjs|cjs)$/iu.test(context.filename) || isTestFile(context.filename) || isStoryFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
16989
17448
|
return {
|
|
16990
17449
|
"Program:exit"(program) {
|
|
16991
17450
|
const classes = /* @__PURE__ */ new Map();
|
|
@@ -17646,12 +18105,13 @@ var MODEL_EXECUTION_METHODS = /* @__PURE__ */ new Set([
|
|
|
17646
18105
|
var DATABASE_NAMES = /^(?:db|database|connection|pool|prisma|query|transaction|tx)$/iu;
|
|
17647
18106
|
var REQUIRE_SQL_ACCESS_CLASS_DOCUMENTATION = {
|
|
17648
18107
|
summary: "Keep SQL reads and writes inside a class that receives its database dependency.",
|
|
17649
|
-
rationale: "
|
|
18108
|
+
rationale: "An injected repository class is the preferred ownership boundary for database access under this architectural policy; free functions can also express explicit dependencies.",
|
|
17650
18109
|
remediation: "Move the query into a repository or store class and inject the pool, connection, transaction, or typed database binding through its constructor.",
|
|
17651
18110
|
category: "architecture",
|
|
17652
18111
|
limitations: [
|
|
17653
18112
|
"The rule recognizes conventional database receiver names, Cloudflare DB bindings, direct pool.query calls, explicit query-builder terminals, and Prisma-style model delegates; unusually named or heavily aliased clients require architectural review.",
|
|
17654
18113
|
"Query construction without a recognized execution terminal is intentionally not reported.",
|
|
18114
|
+
"Stable local Map, WeakMap, and URLSearchParams instances are excluded. Other conventional receiver names are heuristics, not proof of a database API.",
|
|
17655
18115
|
"Constructor injection inherited from a base class or transformed through a wrapper is not inferred by this syntax-only rule."
|
|
17656
18116
|
],
|
|
17657
18117
|
examples: [
|
|
@@ -17857,11 +18317,23 @@ var require_sql_access_class_default = createRule({
|
|
|
17857
18317
|
create(context) {
|
|
17858
18318
|
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text))
|
|
17859
18319
|
return {};
|
|
18320
|
+
function knownNonDatabase(node, seen = /* @__PURE__ */ new Set()) {
|
|
18321
|
+
if (seen.has(node)) return false;
|
|
18322
|
+
seen.add(node);
|
|
18323
|
+
if (node.type === import_utils90.AST_NODE_TYPES.Identifier) {
|
|
18324
|
+
const binding = import_utils90.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
|
|
18325
|
+
if (binding?.defs.length !== 1 || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
18326
|
+
const definition = binding.defs[0];
|
|
18327
|
+
return definition?.type === "Variable" && definition.node.init !== null && knownNonDatabase(definition.node.init, seen);
|
|
18328
|
+
}
|
|
18329
|
+
return node.type === import_utils90.AST_NODE_TYPES.NewExpression && node.callee.type === import_utils90.AST_NODE_TYPES.Identifier && ["Map", "WeakMap", "URLSearchParams"].includes(node.callee.name) && (import_utils90.ASTUtils.findVariable(context.sourceCode.getScope(node.callee), node.callee.name)?.defs.length ?? 0) === 0;
|
|
18330
|
+
}
|
|
17860
18331
|
return {
|
|
17861
18332
|
CallExpression(node) {
|
|
17862
18333
|
if (node.callee.type !== import_utils90.AST_NODE_TYPES.MemberExpression)
|
|
17863
18334
|
return;
|
|
17864
18335
|
const method = memberName6(node.callee);
|
|
18336
|
+
if (knownNonDatabase(node.callee.object)) return;
|
|
17865
18337
|
if (method === null || !isDatabaseOperation(method, node.callee.object))
|
|
17866
18338
|
return;
|
|
17867
18339
|
const owner = owningClass2(node);
|
|
@@ -17882,6 +18354,8 @@ var REQUIRE_STATIC_NEXT_MATCHER_DOCUMENTATION = {
|
|
|
17882
18354
|
rationale: "Next.js must statically analyze matcher values at build time; computed values are ignored.",
|
|
17883
18355
|
remediation: "Write matcher strings, arrays, and object fields as literals in the exported config.",
|
|
17884
18356
|
category: "correctness",
|
|
18357
|
+
limitations: ["Only directly exported matcher configuration in middleware/proxy entry files is inspected. Literal matcher validity is left to Next.js; this rule checks static syntax, not the complete framework schema."],
|
|
18358
|
+
references: ["https://nextjs.org/docs/app/api-reference/file-conventions/proxy"],
|
|
17885
18359
|
examples: [
|
|
17886
18360
|
{ id: "literal-matcher", title: "Use a literal matcher", outcome: "no-match", files: [{ path: "src/middleware.ts", source: 'export const config = { matcher: "/api/:path*" };' }], focusPath: "src/middleware.ts", expectedCount: 0, public: true },
|
|
17887
18361
|
{ id: "computed-matcher", title: "Do not compute the matcher", outcome: "match", files: [{ path: "src/middleware.ts", source: 'const matcher = "/api/:path*"; export const config = { matcher };' }], focusPath: "src/middleware.ts", expectedCount: 1, public: true }
|
|
@@ -17929,7 +18403,7 @@ var require_static_next_matcher_default = createRule({
|
|
|
17929
18403
|
},
|
|
17930
18404
|
schema: [],
|
|
17931
18405
|
messages: {
|
|
17932
|
-
dynamicMatcher: "Next.js matcher
|
|
18406
|
+
dynamicMatcher: "Keep Next.js matcher strings, arrays and object fields literal in the exported config. Dynamic values such as variables are not supported by its build-time static analysis and can be ignored."
|
|
17933
18407
|
}
|
|
17934
18408
|
},
|
|
17935
18409
|
defaultOptions: [],
|
|
@@ -18110,7 +18584,8 @@ var REQUIRE_ZOD_FORM_VALIDATION_DOCUMENTATION = {
|
|
|
18110
18584
|
category: "security",
|
|
18111
18585
|
limitations: [
|
|
18112
18586
|
"Tests are excluded; imported schema-shaped names are trusted when their implementation is outside the linted file.",
|
|
18113
|
-
"Delayed raw-value use is accepted only after an unconditional successful parse in the same block; safeParse remains valid when the raw binding has no unvalidated consumer."
|
|
18587
|
+
"Delayed raw-value use is accepted only after an unconditional successful parse in the same block; safeParse remains valid when the raw binding has no unvalidated consumer.",
|
|
18588
|
+
"An enclosing parse does not validate an earlier call or deferred callback that consumes its input. Direct object/array construction, unshadowed Object.fromEntries and native Number/String/Boolean coercion remain supported; arbitrary preprocessing needs an explicitly reviewed boundary."
|
|
18114
18589
|
],
|
|
18115
18590
|
examples: [
|
|
18116
18591
|
{ id: "validated-form-value", title: "Validate the form value", outcome: "no-match", files: [{ path: "src/action.ts", source: "const input = UserSchema.parse({ name: formData.get('name') });" }], focusPath: "src/action.ts", expectedCount: 0, public: true },
|
|
@@ -18225,6 +18700,15 @@ var require_zod_form_validation_default = createRule({
|
|
|
18225
18700
|
let parent = node.parent;
|
|
18226
18701
|
while (parent !== null && parent !== void 0) {
|
|
18227
18702
|
if (isZodParseCall(parent)) return parent;
|
|
18703
|
+
if (parent.type === import_utils94.AST_NODE_TYPES.CallExpression && parent.callee.type === import_utils94.AST_NODE_TYPES.Identifier && ["Number", "String", "Boolean"].includes(parent.callee.name) && parent.arguments.length === 1 && (resolvedBinding(parent.callee)?.defs.length ?? 0) === 0) {
|
|
18704
|
+
parent = parent.parent;
|
|
18705
|
+
continue;
|
|
18706
|
+
}
|
|
18707
|
+
if (parent.type === import_utils94.AST_NODE_TYPES.CallExpression && parent.callee.type === import_utils94.AST_NODE_TYPES.MemberExpression && !parent.callee.computed && parent.callee.object.type === import_utils94.AST_NODE_TYPES.Identifier && parent.callee.object.name === "Object" && parent.callee.property.type === import_utils94.AST_NODE_TYPES.Identifier && parent.callee.property.name === "fromEntries" && (resolvedBinding(parent.callee.object)?.defs.length ?? 0) === 0) {
|
|
18708
|
+
parent = parent.parent;
|
|
18709
|
+
continue;
|
|
18710
|
+
}
|
|
18711
|
+
if (parent.type === import_utils94.AST_NODE_TYPES.CallExpression || parent.type === import_utils94.AST_NODE_TYPES.NewExpression || parent.type === import_utils94.AST_NODE_TYPES.TaggedTemplateExpression || parent.type === import_utils94.AST_NODE_TYPES.ArrowFunctionExpression || parent.type === import_utils94.AST_NODE_TYPES.FunctionExpression || parent.type === import_utils94.AST_NODE_TYPES.FunctionDeclaration) return null;
|
|
18228
18712
|
parent = parent.parent;
|
|
18229
18713
|
}
|
|
18230
18714
|
return null;
|
|
@@ -18404,13 +18888,14 @@ var require_zod_form_validation_default = createRule({
|
|
|
18404
18888
|
// src/rules/store-insert-requires-on-conflict.ts
|
|
18405
18889
|
var import_utils95 = require("@typescript-eslint/utils");
|
|
18406
18890
|
var STORE_INSERT_REQUIRES_ON_CONFLICT_DOCUMENTATION = {
|
|
18407
|
-
summary: "
|
|
18408
|
-
rationale: "
|
|
18409
|
-
remediation: "
|
|
18891
|
+
summary: "Review conflict handling for embedded inserts in replay-named callables.",
|
|
18892
|
+
rationale: "Names such as seed, enqueue, or upsert suggest that repeated execution deserves a conflict-policy review, but do not prove a replay contract.",
|
|
18893
|
+
remediation: "Choose conflict handling appropriate to the schema and SQL dialect, or document why this insertion must fail on a duplicate.",
|
|
18410
18894
|
category: "correctness",
|
|
18895
|
+
limitations: ["Only the nearest statically named callable is considered; top-level inserts and anonymous callbacks are excluded. Recognized conflict syntax does not prove idempotence or concurrency safety: WHERE NOT EXISTS can race, and INSERT OR REPLACE can delete an existing row. Review unique constraints and dialect semantics manually."],
|
|
18411
18896
|
examples: [
|
|
18412
|
-
{ id: "conflict-safe-insert", title: "
|
|
18413
|
-
{ id: "bare-insert", title: "
|
|
18897
|
+
{ id: "conflict-safe-insert", title: "Review the conflict policy for a replayed insert", outcome: "no-match", files: [{ path: "src/store.ts", source: "function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?) ON CONFLICT(id) DO NOTHING`).run(); }" }], focusPath: "src/store.ts", expectedCount: 0, public: true },
|
|
18898
|
+
{ id: "bare-insert", title: "Review a bare insert in a replay-named callable", outcome: "match", files: [{ path: "src/store.ts", source: "function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?)`).run(); }" }], focusPath: "src/store.ts", expectedCount: 1, public: true }
|
|
18414
18899
|
]
|
|
18415
18900
|
};
|
|
18416
18901
|
var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
|
|
@@ -18422,14 +18907,18 @@ function owningCallableName(node) {
|
|
|
18422
18907
|
return current.id?.name ?? null;
|
|
18423
18908
|
}
|
|
18424
18909
|
if (current.type === "MethodDefinition") {
|
|
18425
|
-
return current.key.type === "Identifier" ? current.key.name : null;
|
|
18910
|
+
return !current.computed && current.key.type === "Identifier" ? current.key.name : null;
|
|
18426
18911
|
}
|
|
18427
18912
|
if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "VariableDeclarator" && current.parent.id.type === "Identifier") {
|
|
18428
18913
|
return current.parent.id.name;
|
|
18429
18914
|
}
|
|
18430
|
-
if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "Property" && current.parent.key.type === "Identifier") {
|
|
18915
|
+
if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "Property" && !current.parent.computed && current.parent.key.type === "Identifier") {
|
|
18431
18916
|
return current.parent.key.name;
|
|
18432
18917
|
}
|
|
18918
|
+
if (current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") {
|
|
18919
|
+
const parent = current.parent;
|
|
18920
|
+
return parent.type === "MethodDefinition" && !parent.computed && parent.key.type === "Identifier" ? parent.key.name : null;
|
|
18921
|
+
}
|
|
18433
18922
|
}
|
|
18434
18923
|
return null;
|
|
18435
18924
|
}
|
|
@@ -18440,11 +18929,11 @@ var store_insert_requires_on_conflict_default = createRule({
|
|
|
18440
18929
|
meta: {
|
|
18441
18930
|
type: "problem",
|
|
18442
18931
|
docs: {
|
|
18443
|
-
description: "
|
|
18932
|
+
description: "Review conflict handling for embedded inserts in replay-named callables."
|
|
18444
18933
|
},
|
|
18445
18934
|
schema: [],
|
|
18446
18935
|
messages: {
|
|
18447
|
-
storeInsertRequiresOnConflict: "This INSERT is
|
|
18936
|
+
storeInsertRequiresOnConflict: "This INSERT is inside a replay-named callable without recognized conflict handling. Review duplicate execution, unique constraints, and the appropriate conflict policy for your SQL dialect."
|
|
18448
18937
|
}
|
|
18449
18938
|
},
|
|
18450
18939
|
defaultOptions: [],
|
|
@@ -18457,7 +18946,7 @@ var store_insert_requires_on_conflict_default = createRule({
|
|
|
18457
18946
|
return;
|
|
18458
18947
|
}
|
|
18459
18948
|
const owner = owningCallableName(node);
|
|
18460
|
-
if (owner
|
|
18949
|
+
if (owner === null || !REPLAY_CONTRACT_NAME.test(owner)) {
|
|
18461
18950
|
return;
|
|
18462
18951
|
}
|
|
18463
18952
|
context.report({ node, messageId: "storeInsertRequiresOnConflict" });
|
|
@@ -18470,15 +18959,13 @@ var import_utils96 = require("@typescript-eslint/utils");
|
|
|
18470
18959
|
var STEPDOWN_DOCUMENTATION = {
|
|
18471
18960
|
summary: "Place a private helper below its sole direct same-scope caller.",
|
|
18472
18961
|
rationale: "Caller-first ordering lets a reader follow the main flow before descending into implementation details.",
|
|
18473
|
-
remediation: "
|
|
18962
|
+
remediation: "Consider moving the private helper below its sole caller after reviewing initialization and reflection dependencies.",
|
|
18474
18963
|
category: "maintainability",
|
|
18475
|
-
autofix: "safe",
|
|
18476
18964
|
limitations: [
|
|
18477
18965
|
"Generated and test files, cycles, dynamic references, overload targets, and helpers with multiple callers are excluded.",
|
|
18478
|
-
"Class helpers must be private; their sole caller may be public, protected, or private
|
|
18966
|
+
"Class helpers must be private; their sole caller may be public, protected, or private.",
|
|
18479
18967
|
"Runtime class-field, static-block, computed-member, and decorator barriers are never crossed.",
|
|
18480
|
-
"
|
|
18481
|
-
"Overlapping helper chains remain report-only so ESLint never leaves a partially reordered class after exhausting its fix-pass limit."
|
|
18968
|
+
"This rule is report-only: reordering methods can change reflective property order, and moving module declarations can change initialization behavior or introduce temporal-dead-zone failures. Review module cycles and eager callers manually."
|
|
18482
18969
|
],
|
|
18483
18970
|
examples: [
|
|
18484
18971
|
{ id: "caller-before-helper", title: "Place the caller first", outcome: "no-match", files: [{ path: "src/run.ts", source: "function run() { return load(); }\nfunction load() { return 1; }" }], focusPath: "src/run.ts", expectedCount: 0, public: true },
|
|
@@ -18488,7 +18975,7 @@ var STEPDOWN_DOCUMENTATION = {
|
|
|
18488
18975
|
function isFunction(node) {
|
|
18489
18976
|
return node.type === import_utils96.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils96.AST_NODE_TYPES.FunctionDeclaration || node.type === import_utils96.AST_NODE_TYPES.FunctionExpression;
|
|
18490
18977
|
}
|
|
18491
|
-
function reportMisordered(context, candidates, scopeDefinitions, calls, pinned, canMove = () => true
|
|
18978
|
+
function reportMisordered(context, candidates, scopeDefinitions, calls, pinned, canMove = () => true) {
|
|
18492
18979
|
const byName = new Map(scopeDefinitions.map((definition) => [definition.name, definition]));
|
|
18493
18980
|
const cycles = cycleComponents(calls);
|
|
18494
18981
|
const callers = /* @__PURE__ */ new Map();
|
|
@@ -18507,12 +18994,10 @@ function reportMisordered(context, candidates, scopeDefinitions, calls, pinned,
|
|
|
18507
18994
|
if (callerName2 === void 0 || cycles.has(helper.name) && cycles.get(helper.name) === cycles.get(callerName2)) continue;
|
|
18508
18995
|
const caller = byName.get(callerName2);
|
|
18509
18996
|
if (caller === void 0 || helper.node.range[0] >= caller.node.range[0] || !canMove(helper, caller)) continue;
|
|
18510
|
-
const fix = makeFix?.(helper, caller);
|
|
18511
18997
|
context.report({
|
|
18512
18998
|
node: helper.node,
|
|
18513
18999
|
messageId: "helperAboveOnlyCaller",
|
|
18514
|
-
data: { helper: helper.name, caller: callerName2 }
|
|
18515
|
-
...fix === void 0 ? {} : { fix }
|
|
19000
|
+
data: { helper: helper.name, caller: callerName2 }
|
|
18516
19001
|
});
|
|
18517
19002
|
}
|
|
18518
19003
|
}
|
|
@@ -18839,37 +19324,7 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18839
19324
|
if (helperIndex === void 0 || callerIndex === void 0) return false;
|
|
18840
19325
|
return runtimeBarrierPrefix[callerIndex + 1] === runtimeBarrierPrefix[helperIndex + 1];
|
|
18841
19326
|
};
|
|
18842
|
-
|
|
18843
|
-
for (const [caller, callees] of calls) {
|
|
18844
|
-
for (const callee of callees) {
|
|
18845
|
-
if (callee === caller) continue;
|
|
18846
|
-
const callers = incoming.get(callee) ?? /* @__PURE__ */ new Set();
|
|
18847
|
-
callers.add(caller);
|
|
18848
|
-
incoming.set(callee, callers);
|
|
18849
|
-
}
|
|
18850
|
-
}
|
|
18851
|
-
reportMisordered(context, definitions, scopeDefinitions, calls, pinned, canMove, (helper, caller) => {
|
|
18852
|
-
if (!canMove(helper, caller)) return void 0;
|
|
18853
|
-
const helperCallsAnother = [...calls.get(helper.name) ?? []].some((callee) => callee !== helper.name);
|
|
18854
|
-
const callerIsAnotherHelper = [...incoming.get(caller.name) ?? []].some((name) => name !== helper.name);
|
|
18855
|
-
if (helperCallsAnother || callerIsAnotherHelper) return void 0;
|
|
18856
|
-
const helperMember = helper.node;
|
|
18857
|
-
const callerMember = caller.node;
|
|
18858
|
-
const helperIndex = memberIndexes.get(helperMember);
|
|
18859
|
-
if (helperIndex === void 0) return void 0;
|
|
18860
|
-
const next = node.body.body[helperIndex + 1];
|
|
18861
|
-
const suffixEnd = next?.range[0] ?? node.body.range[1] - 1;
|
|
18862
|
-
const suffix = context.sourceCode.text.slice(helperMember.range[1], suffixEnd);
|
|
18863
|
-
if (!/^\s*$/u.test(suffix)) return void 0;
|
|
18864
|
-
const previous = node.body.body[helperIndex - 1];
|
|
18865
|
-
const prefixStart = previous?.range[1] ?? node.body.range[0] + 1;
|
|
18866
|
-
if (!/^\s*$/u.test(context.sourceCode.text.slice(prefixStart, helperMember.range[0]))) return void 0;
|
|
18867
|
-
const helperText = context.sourceCode.getText(helperMember);
|
|
18868
|
-
return (fixer) => [
|
|
18869
|
-
fixer.removeRange([helperMember.range[0], suffixEnd]),
|
|
18870
|
-
fixer.insertTextAfter(callerMember, `${suffix}${helperText}`)
|
|
18871
|
-
];
|
|
18872
|
-
});
|
|
19327
|
+
reportMisordered(context, definitions, scopeDefinitions, calls, pinned, canMove);
|
|
18873
19328
|
}
|
|
18874
19329
|
function isClassRuntimeBarrier(member) {
|
|
18875
19330
|
switch (member.type) {
|
|
@@ -18891,7 +19346,6 @@ var stepdown_default = createRule({
|
|
|
18891
19346
|
type: "suggestion",
|
|
18892
19347
|
docs: { description: "Place a private helper below its sole direct same-scope caller." },
|
|
18893
19348
|
schema: [],
|
|
18894
|
-
fixable: "code",
|
|
18895
19349
|
messages: {
|
|
18896
19350
|
helperAboveOnlyCaller: "Private helper `{{helper}}` is defined above its only caller `{{caller}}`; move it below the caller."
|
|
18897
19351
|
}
|
|
@@ -18964,7 +19418,7 @@ var SOURCE_COUPLED_TEST_DOCUMENTATION = {
|
|
|
18964
19418
|
remediation: "Parse the artifact, execute its validator, or assert on another runtime contract.",
|
|
18965
19419
|
category: "testing",
|
|
18966
19420
|
limitations: [
|
|
18967
|
-
"The rule follows lexical
|
|
19421
|
+
"The rule follows stable lexical bindings, static source paths, awaited reads, and common text operations. Reassigned bindings, dynamic paths, unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.",
|
|
18968
19422
|
"When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."
|
|
18969
19423
|
],
|
|
18970
19424
|
examples: [
|
|
@@ -19003,6 +19457,11 @@ function stringValue(node) {
|
|
|
19003
19457
|
const current = unwrap7(node);
|
|
19004
19458
|
if (current.type === import_utils97.AST_NODE_TYPES.Literal && typeof current.value === "string") return current.value;
|
|
19005
19459
|
if (current.type === import_utils97.AST_NODE_TYPES.TemplateLiteral && current.expressions.length === 0) return current.quasis[0]?.value.cooked ?? null;
|
|
19460
|
+
if (current.type === import_utils97.AST_NODE_TYPES.BinaryExpression && current.operator === "+") {
|
|
19461
|
+
const left = stringValue(current.left);
|
|
19462
|
+
const right = stringValue(current.right);
|
|
19463
|
+
return left === null || right === null ? null : left + right;
|
|
19464
|
+
}
|
|
19006
19465
|
return null;
|
|
19007
19466
|
}
|
|
19008
19467
|
function importSource(node) {
|
|
@@ -19032,14 +19491,19 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19032
19491
|
const scopes = [newScope()];
|
|
19033
19492
|
const reportedOrigins = /* @__PURE__ */ new Set();
|
|
19034
19493
|
const currentScope = () => scopes.at(-1) ?? scopes[0];
|
|
19035
|
-
const
|
|
19494
|
+
const bindingOf = (node) => import_utils97.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
|
|
19495
|
+
const visible = (kind, node) => {
|
|
19496
|
+
const name2 = bindingOf(node);
|
|
19497
|
+
if (name2 === null || name2.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
19036
19498
|
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
19037
19499
|
const scope = scopes[index];
|
|
19038
19500
|
if (scope.declared.has(name2)) return scope[kind].has(name2);
|
|
19039
19501
|
}
|
|
19040
19502
|
return false;
|
|
19041
19503
|
};
|
|
19042
|
-
const visibleRawOrigins = (
|
|
19504
|
+
const visibleRawOrigins = (node) => {
|
|
19505
|
+
const name2 = bindingOf(node);
|
|
19506
|
+
if (name2 === null || name2.references.some((reference) => reference.isWrite() && reference.init !== true)) return /* @__PURE__ */ new Set();
|
|
19043
19507
|
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
19044
19508
|
const scope = scopes[index];
|
|
19045
19509
|
if (scope.declared.has(name2)) return scope.rawOrigins.get(name2) ?? /* @__PURE__ */ new Set();
|
|
@@ -19050,15 +19514,14 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19050
19514
|
const current = unwrap7(node);
|
|
19051
19515
|
const value = stringValue(current);
|
|
19052
19516
|
if (value !== null) return sourceSuffixRe.test(value);
|
|
19053
|
-
if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return visible("paths", current
|
|
19054
|
-
if (current.type === import_utils97.AST_NODE_TYPES.BinaryExpression && current.operator === "+") {
|
|
19055
|
-
return sourcePath(current.left) || sourcePath(current.right);
|
|
19056
|
-
}
|
|
19057
|
-
if (current.type === import_utils97.AST_NODE_TYPES.TemplateLiteral) return current.expressions.some(sourcePath);
|
|
19517
|
+
if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return visible("paths", current);
|
|
19058
19518
|
if (current.type === import_utils97.AST_NODE_TYPES.CallExpression || current.type === import_utils97.AST_NODE_TYPES.NewExpression) {
|
|
19059
|
-
|
|
19519
|
+
const callee = current.callee;
|
|
19520
|
+
const first = current.arguments[0];
|
|
19521
|
+
if (first === void 0 || first.type === import_utils97.AST_NODE_TYPES.SpreadElement) return false;
|
|
19522
|
+
if (current.type === import_utils97.AST_NODE_TYPES.NewExpression && callee.type === import_utils97.AST_NODE_TYPES.Identifier && callee.name === "URL" && (bindingOf(callee)?.defs.length ?? 0) === 0) return sourcePath(first);
|
|
19523
|
+
if (callee.type === import_utils97.AST_NODE_TYPES.Identifier && bindingOf(callee)?.defs.some((definition) => definition.node.type === import_utils97.AST_NODE_TYPES.ImportSpecifier && definition.node.imported.type === import_utils97.AST_NODE_TYPES.Identifier && definition.node.imported.name === "fileURLToPath" && definition.node.parent.type === import_utils97.AST_NODE_TYPES.ImportDeclaration && ["node:url", "url"].includes(String(definition.node.parent.source.value)))) return sourcePath(first);
|
|
19060
19524
|
}
|
|
19061
|
-
if (current.type === import_utils97.AST_NODE_TYPES.MemberExpression) return sourcePath(current.object);
|
|
19062
19525
|
return false;
|
|
19063
19526
|
};
|
|
19064
19527
|
const rawRead = (node) => {
|
|
@@ -19066,16 +19529,16 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19066
19529
|
if (current.type !== import_utils97.AST_NODE_TYPES.CallExpression || current.arguments.length === 0) return false;
|
|
19067
19530
|
const callee = unwrap7(current.callee);
|
|
19068
19531
|
if (callee.type === import_utils97.AST_NODE_TYPES.Identifier) {
|
|
19069
|
-
return visible("fsReaders", callee
|
|
19532
|
+
return visible("fsReaders", callee) && sourcePath(current.arguments[0]);
|
|
19070
19533
|
}
|
|
19071
19534
|
if (callee.type !== import_utils97.AST_NODE_TYPES.MemberExpression) return false;
|
|
19072
19535
|
const name2 = staticMemberName7(callee);
|
|
19073
19536
|
const object = unwrap7(callee.object);
|
|
19074
|
-
return name2 !== null && FS_READERS.has(name2) && object.type === import_utils97.AST_NODE_TYPES.Identifier && visible("fsObjects", object
|
|
19537
|
+
return name2 !== null && FS_READERS.has(name2) && object.type === import_utils97.AST_NODE_TYPES.Identifier && visible("fsObjects", object) && sourcePath(current.arguments[0]);
|
|
19075
19538
|
};
|
|
19076
19539
|
const rawOrigins = (node) => {
|
|
19077
19540
|
const current = unwrap7(node);
|
|
19078
|
-
if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return visibleRawOrigins(current
|
|
19541
|
+
if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return visibleRawOrigins(current);
|
|
19079
19542
|
if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
|
|
19080
19543
|
if (current.type === import_utils97.AST_NODE_TYPES.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
|
|
19081
19544
|
if (current.type === import_utils97.AST_NODE_TYPES.MemberExpression && staticMemberName7(current) === "length") return rawOrigins(current.object);
|
|
@@ -19116,14 +19579,9 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19116
19579
|
if (receiver.type !== import_utils97.AST_NODE_TYPES.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
|
|
19117
19580
|
return new Set(node.arguments.flatMap((argument) => argument.type === import_utils97.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
19118
19581
|
};
|
|
19119
|
-
const
|
|
19120
|
-
const
|
|
19121
|
-
if (
|
|
19122
|
-
const argument = node.arguments[0];
|
|
19123
|
-
if (argument?.type !== import_utils97.AST_NODE_TYPES.Literal || !(argument.value instanceof RegExp)) return /* @__PURE__ */ new Set();
|
|
19124
|
-
return rawOrigins(callee.object);
|
|
19125
|
-
};
|
|
19126
|
-
const declare = (name2, state) => {
|
|
19582
|
+
const declare = (node, state) => {
|
|
19583
|
+
const name2 = bindingOf(node);
|
|
19584
|
+
if (name2 === null) return;
|
|
19127
19585
|
const scope = currentScope();
|
|
19128
19586
|
scope.declared.add(name2);
|
|
19129
19587
|
scope.collections.delete(name2);
|
|
@@ -19143,18 +19601,8 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19143
19601
|
const current = unwrap7(node);
|
|
19144
19602
|
return current.type === import_utils97.AST_NODE_TYPES.ArrayExpression && current.elements.length > 0 && current.elements.every((element) => element !== null && element.type !== import_utils97.AST_NODE_TYPES.SpreadElement && sourcePath(element));
|
|
19145
19603
|
};
|
|
19146
|
-
const
|
|
19147
|
-
const current = unwrap7(node);
|
|
19148
|
-
if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return [current.name];
|
|
19149
|
-
if (current.type === import_utils97.AST_NODE_TYPES.AssignmentPattern) return declaredNames2(current.left);
|
|
19150
|
-
if (current.type === import_utils97.AST_NODE_TYPES.RestElement) return declaredNames2(current.argument);
|
|
19151
|
-
if (current.type === import_utils97.AST_NODE_TYPES.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
|
|
19152
|
-
if (current.type === import_utils97.AST_NODE_TYPES.ObjectPattern) return current.properties.flatMap((property) => property.type === import_utils97.AST_NODE_TYPES.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
|
|
19153
|
-
return [];
|
|
19154
|
-
};
|
|
19155
|
-
const enterFunction = (node) => {
|
|
19604
|
+
const enterFunction = () => {
|
|
19156
19605
|
scopes.push(newScope());
|
|
19157
|
-
for (const parameter of node.params) for (const name2 of declaredNames2(parameter)) declare(name2, {});
|
|
19158
19606
|
};
|
|
19159
19607
|
const exitFunction = () => {
|
|
19160
19608
|
scopes.pop();
|
|
@@ -19166,9 +19614,9 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19166
19614
|
for (const specifier of node.specifiers) {
|
|
19167
19615
|
if (specifier.type === import_utils97.AST_NODE_TYPES.ImportSpecifier) {
|
|
19168
19616
|
const imported = specifier.imported.type === import_utils97.AST_NODE_TYPES.Identifier ? specifier.imported.name : String(specifier.imported.value);
|
|
19169
|
-
if (FS_READERS.has(imported)) declare(specifier.local
|
|
19617
|
+
if (FS_READERS.has(imported)) declare(specifier.local, { fsReader: true });
|
|
19170
19618
|
} else {
|
|
19171
|
-
declare(specifier.local
|
|
19619
|
+
declare(specifier.local, { fsObject: true });
|
|
19172
19620
|
}
|
|
19173
19621
|
}
|
|
19174
19622
|
},
|
|
@@ -19177,35 +19625,34 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19177
19625
|
VariableDeclarator(node) {
|
|
19178
19626
|
if (node.init === null) return;
|
|
19179
19627
|
const required = requireSource(node.init);
|
|
19628
|
+
const initializer = unwrap7(node.init);
|
|
19629
|
+
if (required !== null && initializer.type === import_utils97.AST_NODE_TYPES.CallExpression && initializer.callee.type === import_utils97.AST_NODE_TYPES.Identifier && (bindingOf(initializer.callee)?.defs.length ?? 0) > 0) return;
|
|
19180
19630
|
if (required !== null && FS_MODULES.has(required) && node.id.type === import_utils97.AST_NODE_TYPES.Identifier) {
|
|
19181
|
-
declare(node.id
|
|
19631
|
+
declare(node.id, { fsObject: true });
|
|
19182
19632
|
return;
|
|
19183
19633
|
}
|
|
19184
19634
|
if (node.id.type === import_utils97.AST_NODE_TYPES.ObjectPattern && required !== null && FS_MODULES.has(required)) {
|
|
19185
19635
|
for (const property of node.id.properties) {
|
|
19186
19636
|
if (property.type !== import_utils97.AST_NODE_TYPES.Property || property.value.type !== import_utils97.AST_NODE_TYPES.Identifier) continue;
|
|
19187
19637
|
const key = property.key.type === import_utils97.AST_NODE_TYPES.Identifier ? property.key.name : property.key.type === import_utils97.AST_NODE_TYPES.Literal ? String(property.key.value) : "";
|
|
19188
|
-
if (FS_READERS.has(key)) declare(property.value
|
|
19638
|
+
if (FS_READERS.has(key)) declare(property.value, { fsReader: true });
|
|
19189
19639
|
}
|
|
19190
19640
|
return;
|
|
19191
19641
|
}
|
|
19192
19642
|
if (node.id.type !== import_utils97.AST_NODE_TYPES.Identifier) return;
|
|
19193
|
-
declare(node.id
|
|
19643
|
+
declare(node.id, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
|
|
19194
19644
|
},
|
|
19195
19645
|
AssignmentExpression(node) {
|
|
19196
|
-
if (node.left.type === import_utils97.AST_NODE_TYPES.Identifier) declare(node.left
|
|
19646
|
+
if (node.left.type === import_utils97.AST_NODE_TYPES.Identifier) declare(node.left, {});
|
|
19197
19647
|
},
|
|
19198
19648
|
ForOfStatement(node) {
|
|
19199
19649
|
const right = unwrap7(node.right);
|
|
19200
|
-
const collection = right.type === import_utils97.AST_NODE_TYPES.Identifier && visible("collections", right
|
|
19650
|
+
const collection = right.type === import_utils97.AST_NODE_TYPES.Identifier && visible("collections", right);
|
|
19201
19651
|
const left = node.left.type === import_utils97.AST_NODE_TYPES.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
|
|
19202
|
-
if (collection && left?.type === import_utils97.AST_NODE_TYPES.Identifier) declare(left
|
|
19652
|
+
if (collection && left?.type === import_utils97.AST_NODE_TYPES.Identifier) declare(left, { path: true });
|
|
19203
19653
|
},
|
|
19204
19654
|
CallExpression(node) {
|
|
19205
|
-
const origins =
|
|
19206
|
-
...rawAssertionOrigins(node),
|
|
19207
|
-
...rawRegexExtractionOrigins(node)
|
|
19208
|
-
]);
|
|
19655
|
+
const origins = rawAssertionOrigins(node);
|
|
19209
19656
|
if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
|
|
19210
19657
|
for (const origin of origins) reportedOrigins.add(origin);
|
|
19211
19658
|
context.report({ node, messageId: "rawSourceOracle" });
|
|
@@ -19348,8 +19795,8 @@ var IAC_SOURCE_COUPLED_TEST_DOCUMENTATION = {
|
|
|
19348
19795
|
remediation: "Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
|
|
19349
19796
|
category: "testing",
|
|
19350
19797
|
limitations: [
|
|
19351
|
-
"The rule follows lexical
|
|
19352
|
-
"
|
|
19798
|
+
"The rule follows stable lexical bindings and static source paths. Reassigned bindings, dynamic paths, unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.",
|
|
19799
|
+
"When raw representation is genuinely the contract, use an exact line suppression explaining that contract."
|
|
19353
19800
|
],
|
|
19354
19801
|
examples: [
|
|
19355
19802
|
{
|
|
@@ -19824,7 +20271,7 @@ var RULES = {
|
|
|
19824
20271
|
};
|
|
19825
20272
|
var meta = {
|
|
19826
20273
|
name: "@sarj/eslint-plugin",
|
|
19827
|
-
version: "15.17.
|
|
20274
|
+
version: "15.17.11"
|
|
19828
20275
|
};
|
|
19829
20276
|
var APPLICATION_ONLY_RULES = [];
|
|
19830
20277
|
var LIBRARY_IMPORT_POLICY = ["error", {
|