@sarj/eslint-plugin 15.17.9 → 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 +846 -467
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +934 -555
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -627,10 +627,11 @@ var TEST_MODULES = /* @__PURE__ */ new Set(["@jest/globals", "@playwright/test",
|
|
|
627
627
|
var DUPLICATE_TEST_BODY_DOCUMENTATION = {
|
|
628
628
|
summary: "Disallow substantial sibling tests with the same body shape; express their differing inputs as a parameterized case table.",
|
|
629
629
|
rationale: "Copy-pasted test bodies hide the cases that differ and allow equivalent assertions to drift independently.",
|
|
630
|
-
remediation: "
|
|
630
|
+
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.",
|
|
631
631
|
category: "testing",
|
|
632
632
|
limitations: [
|
|
633
|
-
"The rule compares substantial sibling tests within one suite and skips inline snapshots and materially different comments."
|
|
633
|
+
"The rule compares substantial sibling tests within one suite and skips inline snapshots and materially different comments.",
|
|
634
|
+
"Matching normalized body shapes do not prove runtime equivalence or independent setup; parameterization is a manual review, not an automatic deletion."
|
|
634
635
|
],
|
|
635
636
|
examples: [
|
|
636
637
|
{
|
|
@@ -815,6 +816,10 @@ function isDuplicateTestFrameworkIdentifier(identifier, sourceCode) {
|
|
|
815
816
|
const variable = ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
816
817
|
if (variable === null || variable.defs.length === 0) return true;
|
|
817
818
|
return variable.defs.some((definition) => {
|
|
819
|
+
if (definition.node.type === AST_NODE_TYPES2.ImportDefaultSpecifier) return definition.node.parent.source.value === "node:test";
|
|
820
|
+
if (definition.node.type !== AST_NODE_TYPES2.ImportSpecifier) return false;
|
|
821
|
+
const imported = definition.node.imported;
|
|
822
|
+
if (!TEST_CALLERS.has(imported.type === AST_NODE_TYPES2.Identifier ? imported.name : String(imported.value))) return false;
|
|
818
823
|
let current = definition.node;
|
|
819
824
|
while (current != null && current.type !== AST_NODE_TYPES2.ImportDeclaration) current = current.parent;
|
|
820
825
|
return current?.type === AST_NODE_TYPES2.ImportDeclaration && typeof current.source.value === "string" && TEST_MODULES.has(current.source.value);
|
|
@@ -2406,7 +2411,7 @@ var no_duplicate_lifecycle_refresh_listeners_default = createRule({
|
|
|
2406
2411
|
VariableDeclarator(node) {
|
|
2407
2412
|
if (node.id.type !== AST_NODE_TYPES9.Identifier) return;
|
|
2408
2413
|
const variable = ASTUtils3.findVariable(context.sourceCode.getScope(node.id), node.id.name);
|
|
2409
|
-
if (variable === null) return;
|
|
2414
|
+
if (variable === null || variable.references.some((reference) => reference.isWrite() && !reference.init)) return;
|
|
2410
2415
|
if (node.init?.type === AST_NODE_TYPES9.ArrowFunctionExpression || node.init?.type === AST_NODE_TYPES9.FunctionExpression) {
|
|
2411
2416
|
functionCallbacks.set(node.init, variable);
|
|
2412
2417
|
}
|
|
@@ -2417,7 +2422,7 @@ var no_duplicate_lifecycle_refresh_listeners_default = createRule({
|
|
|
2417
2422
|
FunctionDeclaration(node) {
|
|
2418
2423
|
if (node.id === null) return;
|
|
2419
2424
|
const variable = ASTUtils3.findVariable(context.sourceCode.getScope(node.id), node.id.name);
|
|
2420
|
-
if (variable !== null) functionCallbacks.set(node, variable);
|
|
2425
|
+
if (variable !== null && !variable.references.some((reference) => reference.isWrite() && !reference.init)) functionCallbacks.set(node, variable);
|
|
2421
2426
|
},
|
|
2422
2427
|
CallExpression(node) {
|
|
2423
2428
|
const item = registration(context.sourceCode, node);
|
|
@@ -2561,12 +2566,33 @@ import { AST_NODE_TYPES as AST_NODE_TYPES11 } from "@typescript-eslint/utils";
|
|
|
2561
2566
|
// src/rules/_sql.ts
|
|
2562
2567
|
import { AST_NODE_TYPES as AST_NODE_TYPES10 } from "@typescript-eslint/utils";
|
|
2563
2568
|
function stripSqlNoise(text) {
|
|
2564
|
-
|
|
2569
|
+
return scanSqlNoise(text);
|
|
2570
|
+
}
|
|
2571
|
+
function sqlSingleQuotedRanges(text) {
|
|
2572
|
+
const ranges = [];
|
|
2573
|
+
scanSqlNoise(text, (start, end) => ranges.push([start, end]));
|
|
2574
|
+
return ranges;
|
|
2575
|
+
}
|
|
2576
|
+
function scanSqlNoise(text, onSingleQuoted) {
|
|
2577
|
+
const out = text.split("");
|
|
2565
2578
|
const n = text.length;
|
|
2566
2579
|
let i = 0;
|
|
2567
2580
|
while (i < n) {
|
|
2568
2581
|
const ch = text[i];
|
|
2582
|
+
if (ch === "$" && !/[\w$]/u.test(text[i - 1] ?? "")) {
|
|
2583
|
+
const delimiter = /^\$(?:[A-Za-z_][A-Za-z_0-9]*)?\$/u.exec(text.slice(i))?.[0];
|
|
2584
|
+
if (delimiter !== void 0) {
|
|
2585
|
+
const closing = text.indexOf(delimiter, i + delimiter.length);
|
|
2586
|
+
const end = closing < 0 ? n : closing + delimiter.length;
|
|
2587
|
+
while (i < end) {
|
|
2588
|
+
if (text[i] !== "\n") out[i] = " ";
|
|
2589
|
+
i += 1;
|
|
2590
|
+
}
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2569
2594
|
if (ch === "'" || ch === '"') {
|
|
2595
|
+
const start = i;
|
|
2570
2596
|
out[i] = " ";
|
|
2571
2597
|
i += 1;
|
|
2572
2598
|
while (i < n) {
|
|
@@ -2580,6 +2606,7 @@ function stripSqlNoise(text) {
|
|
|
2580
2606
|
}
|
|
2581
2607
|
out[i] = " ";
|
|
2582
2608
|
i += 1;
|
|
2609
|
+
if (ch === "'") onSingleQuoted?.(start, i);
|
|
2583
2610
|
break;
|
|
2584
2611
|
}
|
|
2585
2612
|
if (c !== "\n") {
|
|
@@ -2600,17 +2627,20 @@ function stripSqlNoise(text) {
|
|
|
2600
2627
|
out[i] = " ";
|
|
2601
2628
|
out[i + 1] = " ";
|
|
2602
2629
|
i += 2;
|
|
2603
|
-
|
|
2630
|
+
let depth = 1;
|
|
2631
|
+
while (i < n && depth > 0) {
|
|
2632
|
+
if (text[i] === "/" && text[i + 1] === "*" || text[i] === "*" && text[i + 1] === "/") {
|
|
2633
|
+
depth += text[i] === "/" ? 1 : -1;
|
|
2634
|
+
out[i] = " ";
|
|
2635
|
+
out[i + 1] = " ";
|
|
2636
|
+
i += 2;
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2604
2639
|
if (text[i] !== "\n") {
|
|
2605
2640
|
out[i] = " ";
|
|
2606
2641
|
}
|
|
2607
2642
|
i += 1;
|
|
2608
2643
|
}
|
|
2609
|
-
if (i < n) {
|
|
2610
|
-
out[i] = " ";
|
|
2611
|
-
out[i + 1] = " ";
|
|
2612
|
-
i += 2;
|
|
2613
|
-
}
|
|
2614
2644
|
continue;
|
|
2615
2645
|
}
|
|
2616
2646
|
i += 1;
|
|
@@ -2708,8 +2738,9 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
|
|
|
2708
2738
|
remediation: "Use SQL placeholders and pass runtime values through the driver's binding API.",
|
|
2709
2739
|
category: "security",
|
|
2710
2740
|
limitations: [
|
|
2711
|
-
"
|
|
2712
|
-
"
|
|
2741
|
+
"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.",
|
|
2742
|
+
"Literal fragments, legacy uppercase fragment names, and parameterizing tagged templates are exempt; uppercase spelling does not prove a value is static.",
|
|
2743
|
+
"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."
|
|
2713
2744
|
],
|
|
2714
2745
|
examples: [
|
|
2715
2746
|
{
|
|
@@ -2750,15 +2781,23 @@ function isStaticFragment(expression) {
|
|
|
2750
2781
|
return false;
|
|
2751
2782
|
}
|
|
2752
2783
|
function runtimeInterpolations(template) {
|
|
2784
|
+
const parts = template.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw);
|
|
2785
|
+
const ranges = sqlSingleQuotedRanges(parts.join(RUNTIME_MARKER));
|
|
2786
|
+
let offset = 0;
|
|
2753
2787
|
return template.expressions.filter(
|
|
2754
|
-
(expression, index) =>
|
|
2788
|
+
(expression, index) => {
|
|
2789
|
+
offset += parts[index]?.length ?? 0;
|
|
2790
|
+
const inValue = ranges.some(([start, end]) => start < offset && offset < end);
|
|
2791
|
+
offset += RUNTIME_MARKER.length;
|
|
2792
|
+
return inValue && !isStaticFragment(expression) && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "");
|
|
2793
|
+
}
|
|
2755
2794
|
);
|
|
2756
2795
|
}
|
|
2757
2796
|
function endsWithSqlQuote(text) {
|
|
2758
|
-
return /
|
|
2797
|
+
return /'\s*$/u.test(text);
|
|
2759
2798
|
}
|
|
2760
2799
|
function startsWithSqlQuote(text) {
|
|
2761
|
-
return /^\s*
|
|
2800
|
+
return /^\s*'/u.test(text);
|
|
2762
2801
|
}
|
|
2763
2802
|
function staticLiteralText(node) {
|
|
2764
2803
|
if (node.type === AST_NODE_TYPES11.Literal && typeof node.value === "string") {
|
|
@@ -2780,7 +2819,13 @@ function runtimeConcatOperands(node) {
|
|
|
2780
2819
|
if (!hasStringLiteral) {
|
|
2781
2820
|
return [];
|
|
2782
2821
|
}
|
|
2822
|
+
const parts = operands.map((operand) => staticLiteralText(operand) ?? RUNTIME_MARKER);
|
|
2823
|
+
const ranges = sqlSingleQuotedRanges(parts.join(""));
|
|
2824
|
+
let offset = 0;
|
|
2783
2825
|
return operands.filter((operand, index) => {
|
|
2826
|
+
const inValue = ranges.some(([start, end]) => start < offset && offset < end);
|
|
2827
|
+
offset += parts[index]?.length ?? 0;
|
|
2828
|
+
if (!inValue) return false;
|
|
2784
2829
|
if (isStaticFragment(operand)) return false;
|
|
2785
2830
|
const before = operands[index - 1];
|
|
2786
2831
|
const after = operands[index + 1];
|
|
@@ -2873,9 +2918,11 @@ var no_dynamic_sql_default = createRule({
|
|
|
2873
2918
|
import "@typescript-eslint/utils";
|
|
2874
2919
|
var NO_ENUM_DOCUMENTATION = {
|
|
2875
2920
|
summary: "Disallow TypeScript `enum`; use string-literal unions or `as const` objects instead.",
|
|
2876
|
-
rationale: "
|
|
2877
|
-
remediation: "
|
|
2921
|
+
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.",
|
|
2922
|
+
remediation: "Use a literal union or an `as const` object after checking runtime member access, numeric reverse mappings, serialized values, and public consumers.",
|
|
2878
2923
|
category: "maintainability",
|
|
2924
|
+
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."],
|
|
2925
|
+
references: ["https://www.typescriptlang.org/docs/handbook/enums.html"],
|
|
2879
2926
|
examples: [
|
|
2880
2927
|
{
|
|
2881
2928
|
id: "string-literal-union",
|
|
@@ -2955,17 +3002,17 @@ var no_enum_default = createRule({
|
|
|
2955
3002
|
// src/rules/no-fat-try-blocks.ts
|
|
2956
3003
|
import { AST_NODE_TYPES as AST_NODE_TYPES12 } from "@typescript-eslint/utils";
|
|
2957
3004
|
var NO_FAT_TRY_BLOCKS_DOCUMENTATION = {
|
|
2958
|
-
summary: "
|
|
3005
|
+
summary: "Review try blocks exceeding the configured count of syntactically selected operations.",
|
|
2959
3006
|
rationale: "A broad `try` block obscures which operation failed and encourages one catch clause to recover from unrelated errors.",
|
|
2960
3007
|
remediation: "Keep only the operations that share one recovery policy inside the `try` block and move other work outside it.",
|
|
2961
3008
|
category: "correctness",
|
|
2962
3009
|
limitations: [
|
|
2963
|
-
"The
|
|
3010
|
+
"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."
|
|
2964
3011
|
],
|
|
2965
3012
|
examples: [
|
|
2966
3013
|
{
|
|
2967
3014
|
id: "focused-try-block",
|
|
2968
|
-
title: "
|
|
3015
|
+
title: "Three selected operations stay within the default threshold",
|
|
2969
3016
|
outcome: "no-match",
|
|
2970
3017
|
files: [{
|
|
2971
3018
|
path: "src/load.ts",
|
|
@@ -2977,7 +3024,7 @@ var NO_FAT_TRY_BLOCKS_DOCUMENTATION = {
|
|
|
2977
3024
|
},
|
|
2978
3025
|
{
|
|
2979
3026
|
id: "broad-try-block",
|
|
2980
|
-
title: "
|
|
3027
|
+
title: "Review whether four selected operations share one recovery policy",
|
|
2981
3028
|
outcome: "match",
|
|
2982
3029
|
files: [{
|
|
2983
3030
|
path: "src/load.ts",
|
|
@@ -3347,7 +3394,7 @@ var no_fat_try_blocks_default = createRule({
|
|
|
3347
3394
|
meta: {
|
|
3348
3395
|
type: "problem",
|
|
3349
3396
|
docs: {
|
|
3350
|
-
description: "
|
|
3397
|
+
description: "Review try blocks exceeding the configured count of syntactically selected operations."
|
|
3351
3398
|
},
|
|
3352
3399
|
schema: [
|
|
3353
3400
|
{
|
|
@@ -3359,7 +3406,7 @@ var no_fat_try_blocks_default = createRule({
|
|
|
3359
3406
|
}
|
|
3360
3407
|
],
|
|
3361
3408
|
messages: {
|
|
3362
|
-
fatTryBlock: "This `try` block has {{count}}
|
|
3409
|
+
fatTryBlock: "This `try` block has {{count}} syntactically selected operations (max {{max}}). Review whether they share one recovery policy; move unrelated work outside the boundary."
|
|
3363
3410
|
}
|
|
3364
3411
|
},
|
|
3365
3412
|
defaultOptions: [{ max: MAX_TRY_BODY_STATEMENTS }],
|
|
@@ -3399,14 +3446,15 @@ var no_fat_try_blocks_default = createRule({
|
|
|
3399
3446
|
});
|
|
3400
3447
|
|
|
3401
3448
|
// src/rules/no-hand-rolled-sleep.ts
|
|
3402
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES13 } from "@typescript-eslint/utils";
|
|
3449
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES13, ASTUtils as ASTUtils5 } from "@typescript-eslint/utils";
|
|
3403
3450
|
var NO_HAND_ROLLED_SLEEP_DOCUMENTATION = {
|
|
3404
3451
|
summary: "Disallow uncancellable promisified timers and timeout arms.",
|
|
3405
3452
|
rationale: "A timer that outlives an aborted operation or a lost promise race retains work and can keep the process alive until it fires.",
|
|
3406
3453
|
remediation: "Use `node:timers/promises` with an abort signal for delays, or pass `AbortSignal.timeout(...)` to the timed operation.",
|
|
3407
3454
|
category: "correctness",
|
|
3408
3455
|
limitations: [
|
|
3409
|
-
"The rule skips tests, scripts, generated files, and client modules by default, and supports explicit path exemptions."
|
|
3456
|
+
"The rule skips tests, scripts, generated files, and client modules by default, and supports explicit path exemptions.",
|
|
3457
|
+
"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."
|
|
3410
3458
|
],
|
|
3411
3459
|
examples: [
|
|
3412
3460
|
{
|
|
@@ -3550,6 +3598,21 @@ var no_hand_rolled_sleep_default = createRule({
|
|
|
3550
3598
|
return {};
|
|
3551
3599
|
}
|
|
3552
3600
|
const checkClientModules = optionsArg?.checkClientModules ?? false;
|
|
3601
|
+
const bindingOf = (identifier) => ASTUtils5.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
3602
|
+
const isGlobal = (identifier) => (bindingOf(identifier)?.defs.length ?? 0) === 0;
|
|
3603
|
+
const isBuiltinTimer = (callee) => {
|
|
3604
|
+
if (!isSetTimeoutCallee(callee)) return false;
|
|
3605
|
+
if (callee.type === AST_NODE_TYPES13.MemberExpression && callee.object.type === AST_NODE_TYPES13.Identifier) return isGlobal(callee.object);
|
|
3606
|
+
if (callee.type !== AST_NODE_TYPES13.Identifier) return false;
|
|
3607
|
+
const binding = bindingOf(callee);
|
|
3608
|
+
return binding === null || binding.defs.length === 0 || binding.defs.every((definition) => definition.node.type === AST_NODE_TYPES13.ImportSpecifier && definition.node.imported.type === AST_NODE_TYPES13.Identifier && definition.node.imported.name === "setTimeout" && definition.node.parent.type === AST_NODE_TYPES13.ImportDeclaration && ["node:timers", "timers"].includes(String(definition.node.parent.source.value)));
|
|
3609
|
+
};
|
|
3610
|
+
const settlesParameter = (callback, executor, index) => {
|
|
3611
|
+
const parameter = executor.params[index];
|
|
3612
|
+
if (parameter?.type !== AST_NODE_TYPES13.Identifier) return false;
|
|
3613
|
+
const callee = callback.type === AST_NODE_TYPES13.Identifier ? callback : callback.type === AST_NODE_TYPES13.ArrowFunctionExpression || callback.type === AST_NODE_TYPES13.FunctionExpression ? soleCall(callback)?.callee : null;
|
|
3614
|
+
return callee?.type === AST_NODE_TYPES13.Identifier && bindingOf(callee) === bindingOf(parameter);
|
|
3615
|
+
};
|
|
3553
3616
|
function isClientModule2() {
|
|
3554
3617
|
if (/\.[cm]?[jt]sx$/.test(filename)) {
|
|
3555
3618
|
return true;
|
|
@@ -3575,7 +3638,7 @@ var no_hand_rolled_sleep_default = createRule({
|
|
|
3575
3638
|
};
|
|
3576
3639
|
return {
|
|
3577
3640
|
NewExpression(node) {
|
|
3578
|
-
if (node.callee.type !== AST_NODE_TYPES13.Identifier || node.callee.name !== "Promise") {
|
|
3641
|
+
if (node.callee.type !== AST_NODE_TYPES13.Identifier || node.callee.name !== "Promise" || !isGlobal(node.callee)) {
|
|
3579
3642
|
return;
|
|
3580
3643
|
}
|
|
3581
3644
|
const executor = node.arguments[0];
|
|
@@ -3583,7 +3646,7 @@ var no_hand_rolled_sleep_default = createRule({
|
|
|
3583
3646
|
return;
|
|
3584
3647
|
}
|
|
3585
3648
|
const call = soleCall(executor);
|
|
3586
|
-
if (call === null || !
|
|
3649
|
+
if (call === null || !isBuiltinTimer(call.callee)) {
|
|
3587
3650
|
return;
|
|
3588
3651
|
}
|
|
3589
3652
|
const [callback, delay] = call.arguments;
|
|
@@ -3591,14 +3654,14 @@ var no_hand_rolled_sleep_default = createRule({
|
|
|
3591
3654
|
return;
|
|
3592
3655
|
}
|
|
3593
3656
|
const resolveName = parameterName(executor, 0);
|
|
3594
|
-
if (resolveName !== null && settlesWithoutValue(callback, resolveName)) {
|
|
3657
|
+
if (resolveName !== null && call.arguments.length === 2 && settlesWithoutValue(callback, resolveName) && settlesParameter(callback, executor, 0)) {
|
|
3595
3658
|
if (reportsSleepHere()) {
|
|
3596
3659
|
context.report({ node, messageId: "handRolledSleep" });
|
|
3597
3660
|
}
|
|
3598
3661
|
return;
|
|
3599
3662
|
}
|
|
3600
3663
|
const rejectName = parameterName(executor, 1);
|
|
3601
|
-
if (rejectName !== null && isRaceArm(node) && rejectsInCallback(callback, rejectName)) {
|
|
3664
|
+
if (rejectName !== null && isRaceArm(node) && settlesParameter(callback, executor, 1) && rejectsInCallback(callback, rejectName)) {
|
|
3602
3665
|
context.report({ node, messageId: "handRolledTimeoutRace" });
|
|
3603
3666
|
}
|
|
3604
3667
|
}
|
|
@@ -3691,13 +3754,13 @@ var no_hand_rolled_spinner_default = createRule({
|
|
|
3691
3754
|
});
|
|
3692
3755
|
|
|
3693
3756
|
// src/rules/no-insecure-random-id.ts
|
|
3694
|
-
import "@typescript-eslint/utils";
|
|
3757
|
+
import { ASTUtils as ASTUtils6 } from "@typescript-eslint/utils";
|
|
3695
3758
|
var NO_INSECURE_RANDOM_ID_DOCUMENTATION = {
|
|
3696
3759
|
summary: "Disallow using `Math.random()` to generate identifiers, tokens, or secrets; use `crypto.randomUUID()` or `crypto.getRandomValues(...)` instead.",
|
|
3697
3760
|
rationale: "Math.random is predictable and lacks the entropy required for security-sensitive values.",
|
|
3698
3761
|
remediation: "Generate the value with crypto.randomUUID or crypto.getRandomValues.",
|
|
3699
3762
|
category: "security",
|
|
3700
|
-
limitations: ["
|
|
3763
|
+
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."],
|
|
3701
3764
|
examples: [
|
|
3702
3765
|
{ 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 },
|
|
3703
3766
|
{ 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 }
|
|
@@ -3778,6 +3841,7 @@ function findEnclosingNames(node) {
|
|
|
3778
3841
|
if (directBinding && parent.id.type === "Identifier") {
|
|
3779
3842
|
names.push(parent.id.name);
|
|
3780
3843
|
}
|
|
3844
|
+
return names;
|
|
3781
3845
|
}
|
|
3782
3846
|
if (parent.type === "Property" && parent.value === current) {
|
|
3783
3847
|
const key = parent.key;
|
|
@@ -3812,7 +3876,7 @@ function findEnclosingNames(node) {
|
|
|
3812
3876
|
if (directBinding && parent.id !== null) names.push(parent.id.name);
|
|
3813
3877
|
return names;
|
|
3814
3878
|
}
|
|
3815
|
-
if (parent.type === "ExpressionStatement") {
|
|
3879
|
+
if (parent.type === "ExpressionStatement" || parent.type === "IfStatement" || parent.type === "ForStatement" || parent.type === "WhileStatement" || parent.type === "DoWhileStatement" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression") {
|
|
3816
3880
|
return names;
|
|
3817
3881
|
}
|
|
3818
3882
|
current = parent;
|
|
@@ -3904,6 +3968,7 @@ var no_insecure_random_id_default = createRule({
|
|
|
3904
3968
|
if (!isMathRandomCall(node)) {
|
|
3905
3969
|
return;
|
|
3906
3970
|
}
|
|
3971
|
+
if ((ASTUtils6.findVariable(context.sourceCode.getScope(node), "Math")?.defs.length ?? 0) > 0) return;
|
|
3907
3972
|
const names = findEnclosingNames(node);
|
|
3908
3973
|
if (names.some(isStrongSecurityName)) {
|
|
3909
3974
|
context.report({ node, messageId: "insecureRandomId" });
|
|
@@ -3921,7 +3986,7 @@ var no_insecure_random_id_default = createRule({
|
|
|
3921
3986
|
});
|
|
3922
3987
|
|
|
3923
3988
|
// src/rules/no-json-stringify-error.ts
|
|
3924
|
-
import { ASTUtils as
|
|
3989
|
+
import { ASTUtils as ASTUtils7 } from "@typescript-eslint/utils";
|
|
3925
3990
|
var NO_JSON_STRINGIFY_ERROR_DOCUMENTATION = {
|
|
3926
3991
|
summary: "Avoid generic JSON serialization that can omit native Error details.",
|
|
3927
3992
|
rationale: "Native Error details are non-enumerable, so generic JSON serialization discards diagnostic information.",
|
|
@@ -3961,7 +4026,7 @@ var BUILTIN_ERROR_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
|
3961
4026
|
"URIError"
|
|
3962
4027
|
]);
|
|
3963
4028
|
function identifierIsProvenError(identifier, scope) {
|
|
3964
|
-
const variable =
|
|
4029
|
+
const variable = ASTUtils7.findVariable(scope, identifier.name);
|
|
3965
4030
|
if (variable === null || variable.defs.length !== 1 || variable.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
3966
4031
|
const definition = variable.defs[0];
|
|
3967
4032
|
if (definition?.type === "CatchClause") return true;
|
|
@@ -3970,7 +4035,7 @@ function identifierIsProvenError(identifier, scope) {
|
|
|
3970
4035
|
return initializer?.type === "NewExpression" && initializer.callee.type === "Identifier" && BUILTIN_ERROR_CONSTRUCTORS.has(initializer.callee.name) && isGlobalIdentifier(initializer.callee.name, scope);
|
|
3971
4036
|
}
|
|
3972
4037
|
function isGlobalIdentifier(name, scope) {
|
|
3973
|
-
const binding =
|
|
4038
|
+
const binding = ASTUtils7.findVariable(scope, name);
|
|
3974
4039
|
return binding === null || binding.defs.length === 0;
|
|
3975
4040
|
}
|
|
3976
4041
|
function positiveErrorSubject(test) {
|
|
@@ -4434,105 +4499,9 @@ import * as ts from "typescript";
|
|
|
4434
4499
|
import {
|
|
4435
4500
|
AST_NODE_TYPES as AST_NODE_TYPES16
|
|
4436
4501
|
} from "@typescript-eslint/utils";
|
|
4437
|
-
function symbolAt(services, checker, node) {
|
|
4438
|
-
return checker.getSymbolAtLocation(services.esTreeNodeToTSNodeMap.get(node));
|
|
4439
|
-
}
|
|
4440
|
-
function sameSymbol(left, right) {
|
|
4441
|
-
return left !== void 0 && right !== void 0 && left === right;
|
|
4442
|
-
}
|
|
4443
|
-
function enclosingClass(node) {
|
|
4444
|
-
let current = node.parent;
|
|
4445
|
-
while (current !== void 0) {
|
|
4446
|
-
if (current.type === AST_NODE_TYPES16.ClassDeclaration || current.type === AST_NODE_TYPES16.ClassExpression) {
|
|
4447
|
-
return current;
|
|
4448
|
-
}
|
|
4449
|
-
current = current.parent;
|
|
4450
|
-
}
|
|
4451
|
-
return null;
|
|
4452
|
-
}
|
|
4453
4502
|
function memberName2(member) {
|
|
4454
4503
|
return !member.computed && member.key.type === AST_NODE_TYPES16.Identifier ? member.key.name : null;
|
|
4455
4504
|
}
|
|
4456
|
-
function privateMemberFixes(context, services, owner, members, removePrivateKeyword) {
|
|
4457
|
-
const first = members[0];
|
|
4458
|
-
const name = first === void 0 ? null : memberName2(first);
|
|
4459
|
-
if (first === void 0 || name === null || members.some((member) => member.static || member.decorators.length > 0)) {
|
|
4460
|
-
return void 0;
|
|
4461
|
-
}
|
|
4462
|
-
if (owner.body.body.some((member) => {
|
|
4463
|
-
if (member.type !== AST_NODE_TYPES16.MethodDefinition && member.type !== AST_NODE_TYPES16.PropertyDefinition && member.type !== AST_NODE_TYPES16.AccessorProperty) return false;
|
|
4464
|
-
return member.key.type === AST_NODE_TYPES16.PrivateIdentifier && member.key.name === name;
|
|
4465
|
-
})) return void 0;
|
|
4466
|
-
const selectedMembers = new Set(members);
|
|
4467
|
-
if (owner.body.body.some((member) => {
|
|
4468
|
-
if (member.type !== AST_NODE_TYPES16.MethodDefinition && member.type !== AST_NODE_TYPES16.PropertyDefinition && member.type !== AST_NODE_TYPES16.AccessorProperty) return false;
|
|
4469
|
-
return memberName2(member) === name && !selectedMembers.has(member);
|
|
4470
|
-
})) return void 0;
|
|
4471
|
-
const checker = services.program.getTypeChecker();
|
|
4472
|
-
const symbols = members.map((member) => symbolAt(services, checker, member.key)).filter(
|
|
4473
|
-
(symbol) => symbol !== void 0
|
|
4474
|
-
);
|
|
4475
|
-
if (symbols.length === 0) return void 0;
|
|
4476
|
-
const references = [];
|
|
4477
|
-
let unsafe = false;
|
|
4478
|
-
walk(context.sourceCode.ast, context.sourceCode.visitorKeys, (node) => {
|
|
4479
|
-
if (node.type === AST_NODE_TYPES16.Literal && node.value === name) {
|
|
4480
|
-
unsafe = true;
|
|
4481
|
-
return;
|
|
4482
|
-
}
|
|
4483
|
-
if (node.type !== AST_NODE_TYPES16.MemberExpression) return;
|
|
4484
|
-
const propertyName6 = node.property.type === AST_NODE_TYPES16.Identifier || node.property.type === AST_NODE_TYPES16.PrivateIdentifier ? node.property.name : node.property.type === AST_NODE_TYPES16.Literal && typeof node.property.value === "string" ? node.property.value : null;
|
|
4485
|
-
if (propertyName6 !== name) return;
|
|
4486
|
-
const propertySymbol = symbolAt(services, checker, node.property);
|
|
4487
|
-
if (node.computed || node.property.type !== AST_NODE_TYPES16.Identifier || node.object.type !== AST_NODE_TYPES16.ThisExpression || enclosingClass(node) !== owner || !symbols.some((symbol) => sameSymbol(symbol, propertySymbol))) {
|
|
4488
|
-
unsafe = true;
|
|
4489
|
-
return;
|
|
4490
|
-
}
|
|
4491
|
-
references.push(node);
|
|
4492
|
-
});
|
|
4493
|
-
if (unsafe) return void 0;
|
|
4494
|
-
const privateKeywordRanges = /* @__PURE__ */ new Map();
|
|
4495
|
-
if (removePrivateKeyword) {
|
|
4496
|
-
const comments = context.sourceCode.getAllComments();
|
|
4497
|
-
for (const member of members) {
|
|
4498
|
-
const keyword = context.sourceCode.getTokens(member).find((token) => token.value === "private");
|
|
4499
|
-
const next = keyword === void 0 ? void 0 : context.sourceCode.getTokenAfter(keyword);
|
|
4500
|
-
if (keyword === void 0 || next === null || next === void 0) return void 0;
|
|
4501
|
-
if (comments.some((comment) => comment.range[0] >= keyword.range[1] && comment.range[1] <= next.range[0])) {
|
|
4502
|
-
return void 0;
|
|
4503
|
-
}
|
|
4504
|
-
privateKeywordRanges.set(member, [keyword.range[0], next.range[0]]);
|
|
4505
|
-
}
|
|
4506
|
-
}
|
|
4507
|
-
return (fixer) => {
|
|
4508
|
-
const fixes = [];
|
|
4509
|
-
for (const member of members) {
|
|
4510
|
-
fixes.push(fixer.replaceText(member.key, `#${name}`));
|
|
4511
|
-
if (removePrivateKeyword) {
|
|
4512
|
-
const range = privateKeywordRanges.get(member);
|
|
4513
|
-
if (range === void 0) return [];
|
|
4514
|
-
fixes.push(fixer.removeRange(range));
|
|
4515
|
-
}
|
|
4516
|
-
}
|
|
4517
|
-
for (const reference of references) fixes.push(fixer.replaceText(reference.property, `#${name}`));
|
|
4518
|
-
return fixes;
|
|
4519
|
-
};
|
|
4520
|
-
}
|
|
4521
|
-
function walk(node, visitorKeys, visit) {
|
|
4522
|
-
visit(node);
|
|
4523
|
-
for (const key of visitorKeys[node.type] ?? []) {
|
|
4524
|
-
const child = node[key];
|
|
4525
|
-
if (Array.isArray(child)) {
|
|
4526
|
-
for (const item of child) {
|
|
4527
|
-
if (typeof item === "object" && item !== null && "type" in item) {
|
|
4528
|
-
walk(item, visitorKeys, visit);
|
|
4529
|
-
}
|
|
4530
|
-
}
|
|
4531
|
-
} else if (typeof child === "object" && child !== null && "type" in child) {
|
|
4532
|
-
walk(child, visitorKeys, visit);
|
|
4533
|
-
}
|
|
4534
|
-
}
|
|
4535
|
-
}
|
|
4536
4505
|
function convertibleMemberName(member) {
|
|
4537
4506
|
if (member.type !== AST_NODE_TYPES16.MethodDefinition && member.type !== AST_NODE_TYPES16.PropertyDefinition && member.type !== AST_NODE_TYPES16.AccessorProperty) return null;
|
|
4538
4507
|
return memberName2(member);
|
|
@@ -4645,7 +4614,7 @@ var interface_contract_members_private_default = createRule({
|
|
|
4645
4614
|
});
|
|
4646
4615
|
|
|
4647
4616
|
// src/rules/no-log-only-catch.ts
|
|
4648
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES18, ASTUtils as
|
|
4617
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES18, ASTUtils as ASTUtils8 } from "@typescript-eslint/utils";
|
|
4649
4618
|
|
|
4650
4619
|
// src/rules/_logging.ts
|
|
4651
4620
|
import "@typescript-eslint/utils";
|
|
@@ -4817,7 +4786,7 @@ function seededFallbackHandled(tryStatement, scope) {
|
|
|
4817
4786
|
if (previous.declarations.length !== 1 || declarator === void 0) return false;
|
|
4818
4787
|
if (declarator.id.type !== AST_NODE_TYPES18.Identifier) return false;
|
|
4819
4788
|
if (declarator.init == null || !isSeedValue(declarator.init)) return false;
|
|
4820
|
-
const variable =
|
|
4789
|
+
const variable = ASTUtils8.findVariable(scope, declarator.id.name);
|
|
4821
4790
|
if (variable === null) return false;
|
|
4822
4791
|
const [tryStart, tryEnd] = tryStatement.block.range;
|
|
4823
4792
|
let writtenInTry = false;
|
|
@@ -4871,6 +4840,39 @@ var no_log_only_catch_default = createRule({
|
|
|
4871
4840
|
const matcher = createLogMatcher(loggingOptions);
|
|
4872
4841
|
const filename = context.filename;
|
|
4873
4842
|
const sourceCode = context.sourceCode;
|
|
4843
|
+
function hasCoercionValidation(node) {
|
|
4844
|
+
const owner = node.parent;
|
|
4845
|
+
const statement = owner.block.body[0];
|
|
4846
|
+
if (node.body.body.length !== 0 || owner.finalizer !== null || owner.block.body.length !== 1 || statement?.type !== AST_NODE_TYPES18.ExpressionStatement || statement.expression.type !== AST_NODE_TYPES18.AssignmentExpression || statement.expression.operator !== "=") return false;
|
|
4847
|
+
const { left, right } = statement.expression;
|
|
4848
|
+
if (left.type !== AST_NODE_TYPES18.MemberExpression || left.computed || left.object.type !== AST_NODE_TYPES18.Identifier || right.type !== AST_NODE_TYPES18.CallExpression || right.optional || right.callee.type !== AST_NODE_TYPES18.Identifier || !["String", "Number", "Boolean", "BigInt"].includes(right.callee.name) || right.arguments.length !== 1) return false;
|
|
4849
|
+
const argument = right.arguments[0];
|
|
4850
|
+
if (argument === void 0 || sourceCode.getText(left) !== sourceCode.getText(argument)) return false;
|
|
4851
|
+
const global = ASTUtils8.findVariable(sourceCode.getScope(right.callee), right.callee.name);
|
|
4852
|
+
if (global !== null && global.defs.length > 0) return false;
|
|
4853
|
+
const root = ASTUtils8.findVariable(sourceCode.getScope(left.object), left.object.name);
|
|
4854
|
+
if (root === null || root.references.some((reference) => reference.isWrite() && !reference.init)) return false;
|
|
4855
|
+
let current = owner;
|
|
4856
|
+
let slot = statementSlot(current);
|
|
4857
|
+
while (slot === null && current.parent !== void 0 && !FUNCTION_TYPES2.has(current.parent.type)) {
|
|
4858
|
+
current = current.parent;
|
|
4859
|
+
slot = statementSlot(current);
|
|
4860
|
+
}
|
|
4861
|
+
let next = slot?.list[slot.index + 1];
|
|
4862
|
+
let target = sourceCode.getText(left);
|
|
4863
|
+
if (next?.type === AST_NODE_TYPES18.VariableDeclaration && next.kind === "const" && next.declarations.length === 1) {
|
|
4864
|
+
const alias = next.declarations[0];
|
|
4865
|
+
if (alias?.id.type !== AST_NODE_TYPES18.Identifier || alias.init === null || sourceCode.getText(alias.init) !== target) return false;
|
|
4866
|
+
target = alias.id.name;
|
|
4867
|
+
next = slot?.list[slot.index + 2];
|
|
4868
|
+
}
|
|
4869
|
+
if (next?.type !== AST_NODE_TYPES18.IfStatement) return false;
|
|
4870
|
+
let condition = next.test;
|
|
4871
|
+
while (condition.type === AST_NODE_TYPES18.LogicalExpression && condition.operator === "&&") condition = condition.left;
|
|
4872
|
+
if (condition.type !== AST_NODE_TYPES18.BinaryExpression || !["==", "==="].includes(condition.operator)) return false;
|
|
4873
|
+
const test = condition.left;
|
|
4874
|
+
return test.type === AST_NODE_TYPES18.UnaryExpression && test.operator === "typeof" && sourceCode.getText(test.argument) === target && condition.right.type === AST_NODE_TYPES18.Literal && condition.right.value === right.callee.name.toLowerCase();
|
|
4875
|
+
}
|
|
4874
4876
|
function isLoggingCallStatement(statement) {
|
|
4875
4877
|
if (statement.type !== "ExpressionStatement") {
|
|
4876
4878
|
return false;
|
|
@@ -4897,17 +4899,11 @@ var no_log_only_catch_default = createRule({
|
|
|
4897
4899
|
CatchClause(node) {
|
|
4898
4900
|
const statements = node.body.body;
|
|
4899
4901
|
const isDocumented = sourceCode.getCommentsInside(node.body).length > 0 || hasAdjacentRationale(node);
|
|
4900
|
-
if (
|
|
4901
|
-
if (isDocumented) {
|
|
4902
|
-
return;
|
|
4903
|
-
}
|
|
4904
|
-
if (fallbackFollowsTry(node.parent) || seededFallbackHandled(node.parent, sourceCode.getScope(node))) {
|
|
4905
|
-
return;
|
|
4906
|
-
}
|
|
4907
|
-
context.report({ node, messageId: "emptyCatch" });
|
|
4902
|
+
if (isDocumented || hasCoercionValidation(node) || fallbackFollowsTry(node.parent) || seededFallbackHandled(node.parent, sourceCode.getScope(node))) {
|
|
4908
4903
|
return;
|
|
4909
4904
|
}
|
|
4910
|
-
if (
|
|
4905
|
+
if (statements.length === 0) {
|
|
4906
|
+
context.report({ node, messageId: "emptyCatch" });
|
|
4911
4907
|
return;
|
|
4912
4908
|
}
|
|
4913
4909
|
const everyStatementIsLogging = statements.every(
|
|
@@ -4922,10 +4918,10 @@ var no_log_only_catch_default = createRule({
|
|
|
4922
4918
|
});
|
|
4923
4919
|
|
|
4924
4920
|
// src/rules/no-bare-return-from-test-catch.ts
|
|
4925
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES19, ASTUtils as
|
|
4921
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES19, ASTUtils as ASTUtils9 } from "@typescript-eslint/utils";
|
|
4926
4922
|
var NO_BARE_RETURN_FROM_TEST_CATCH_DOCUMENTATION = {
|
|
4927
4923
|
summary: "Disallow a bare return from a test catch block when it skips a later assertion.",
|
|
4928
|
-
rationale: "
|
|
4924
|
+
rationale: "An unasserted catch return can swallow a failure and skip later assertions; the complete test result also depends on other assertions and hooks.",
|
|
4929
4925
|
remediation: "Rethrow the error, assert on it, or use the runner's explicit skip mechanism when the capability is optional.",
|
|
4930
4926
|
category: "testing",
|
|
4931
4927
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
@@ -4947,7 +4943,7 @@ function staticMemberName2(node) {
|
|
|
4947
4943
|
return null;
|
|
4948
4944
|
}
|
|
4949
4945
|
function importedName3(identifier, context, modules) {
|
|
4950
|
-
const variable =
|
|
4946
|
+
const variable = ASTUtils9.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
4951
4947
|
if (variable === null || variable.defs.length === 0) return identifier.name;
|
|
4952
4948
|
for (const definition of variable.defs) {
|
|
4953
4949
|
if (definition.node.type !== AST_NODE_TYPES19.ImportSpecifier) continue;
|
|
@@ -5012,7 +5008,7 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
5012
5008
|
type: "problem",
|
|
5013
5009
|
docs: { description: "Disallow a bare return from a test catch block when it skips a later assertion." },
|
|
5014
5010
|
schema: [],
|
|
5015
|
-
messages: { bareReturnFromTestCatch: "This bare return
|
|
5011
|
+
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." }
|
|
5016
5012
|
},
|
|
5017
5013
|
defaultOptions: [],
|
|
5018
5014
|
create(context) {
|
|
@@ -5031,6 +5027,23 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
5031
5027
|
if (current === null || current === void 0) break;
|
|
5032
5028
|
}
|
|
5033
5029
|
if (catchClause === null || catchClause.parent.finalizer !== null) return;
|
|
5030
|
+
const parameter = catchClause.param;
|
|
5031
|
+
if (parameter?.type === AST_NODE_TYPES19.Identifier && node.parent === catchClause.body) {
|
|
5032
|
+
const errorBinding = ASTUtils9.findVariable(context.sourceCode.getScope(parameter), parameter.name);
|
|
5033
|
+
const assertedError = catchClause.body.body.some((statement) => {
|
|
5034
|
+
if (statement.range[1] >= node.range[0] || statement.type !== AST_NODE_TYPES19.ExpressionStatement) return false;
|
|
5035
|
+
const expression = statement.expression;
|
|
5036
|
+
if (expression.type !== AST_NODE_TYPES19.CallExpression || !isAssertion(expression, context)) return false;
|
|
5037
|
+
const root = rootIdentifier2(expression.callee);
|
|
5038
|
+
if (root === null) return false;
|
|
5039
|
+
const assertionName = importedName3(root, context, ASSERTION_MODULES);
|
|
5040
|
+
let operand = expression.callee.type === AST_NODE_TYPES19.MemberExpression ? expression.callee.object : null;
|
|
5041
|
+
if (operand?.type === AST_NODE_TYPES19.MemberExpression && staticMemberName2(operand) === "not") operand = operand.object;
|
|
5042
|
+
if (assertionName !== "assert" && (assertionName !== "expect" || operand?.type !== AST_NODE_TYPES19.CallExpression || operand.callee !== root)) return false;
|
|
5043
|
+
return walkOwnScope(expression, (current) => current.type === AST_NODE_TYPES19.Identifier && errorBinding?.references.some((reference) => reference.identifier === current) === true);
|
|
5044
|
+
});
|
|
5045
|
+
if (assertedError) return;
|
|
5046
|
+
}
|
|
5034
5047
|
if (walkOwnScope(catchClause.body, (current) => current.type === AST_NODE_TYPES19.ThrowStatement || isExplicitSkip(current, context))) return;
|
|
5035
5048
|
if (!walkOwnScope(owner.body, (current) => current.range[0] > node.range[1] && isAssertion(current, context))) return;
|
|
5036
5049
|
context.report({ node, messageId: "bareReturnFromTestCatch" });
|
|
@@ -5040,15 +5053,15 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
5040
5053
|
});
|
|
5041
5054
|
|
|
5042
5055
|
// src/rules/no-bespoke-api-case-conversion.ts
|
|
5043
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES20 } from "@typescript-eslint/utils";
|
|
5056
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES20, ASTUtils as ASTUtils10 } from "@typescript-eslint/utils";
|
|
5044
5057
|
var NO_BESPOKE_API_CASE_CONVERSION_DOCUMENTATION = {
|
|
5045
|
-
summary: "
|
|
5046
|
-
rationale: "
|
|
5058
|
+
summary: "Review direct snake_case/camelCase mirror mappings on explicitly API-typed adapter values.",
|
|
5059
|
+
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.",
|
|
5047
5060
|
remediation: "Move wire-name ownership and case conversion into the generated SDK/model layer; keep application adapters on the generated typed surface.",
|
|
5048
5061
|
category: "architecture",
|
|
5049
5062
|
autofix: "none",
|
|
5050
5063
|
limitations: [
|
|
5051
|
-
"Only
|
|
5064
|
+
"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.",
|
|
5052
5065
|
"Only object properties that directly translate the same identifier between snake_case and lowerCamelCase are reported.",
|
|
5053
5066
|
"Quoted/computed protocol keys, generated/vendor code, tests, fixtures, and indirect conversions are intentionally excluded."
|
|
5054
5067
|
],
|
|
@@ -5120,7 +5133,7 @@ var no_bespoke_api_case_conversion_default = createRule({
|
|
|
5120
5133
|
docs: { description: NO_BESPOKE_API_CASE_CONVERSION_DOCUMENTATION.summary },
|
|
5121
5134
|
schema: [],
|
|
5122
5135
|
messages: {
|
|
5123
|
-
noBespokeApiCaseConversion: "This API adapter
|
|
5136
|
+
noBespokeApiCaseConversion: "This API-typed adapter value mirrors `{{wireName}}` and `{{applicationName}}`. If the SDK owns application-facing names, move this conversion to its model boundary."
|
|
5124
5137
|
}
|
|
5125
5138
|
},
|
|
5126
5139
|
defaultOptions: [],
|
|
@@ -5130,16 +5143,33 @@ var no_bespoke_api_case_conversion_default = createRule({
|
|
|
5130
5143
|
if (!ADAPTER_BASENAME_RE.test(basename) || isGeneratedFile(filename, context.sourceCode.text) || isTestFile(filename, ["fixtureTree"])) {
|
|
5131
5144
|
return {};
|
|
5132
5145
|
}
|
|
5133
|
-
const
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5146
|
+
const hasApiReceiver = (value) => {
|
|
5147
|
+
let current = value;
|
|
5148
|
+
while (true) {
|
|
5149
|
+
if (current.type === AST_NODE_TYPES20.MemberExpression) current = current.object;
|
|
5150
|
+
else if (current.type === AST_NODE_TYPES20.TSAsExpression || current.type === AST_NODE_TYPES20.TSNonNullExpression || current.type === AST_NODE_TYPES20.TSTypeAssertion) current = current.expression;
|
|
5151
|
+
else break;
|
|
5152
|
+
}
|
|
5153
|
+
if (current.type !== AST_NODE_TYPES20.Identifier) return false;
|
|
5154
|
+
const binding = ASTUtils10.findVariable(context.sourceCode.getScope(current), current.name);
|
|
5155
|
+
if (binding?.defs.length !== 1 || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
5156
|
+
const identifier = binding.defs[0]?.name;
|
|
5157
|
+
if (identifier?.type !== AST_NODE_TYPES20.Identifier) return false;
|
|
5158
|
+
const annotation = identifier.typeAnnotation?.typeAnnotation;
|
|
5159
|
+
if (annotation?.type !== AST_NODE_TYPES20.TSTypeReference) return false;
|
|
5160
|
+
let typeName = annotation.typeName;
|
|
5161
|
+
while (typeName.type === AST_NODE_TYPES20.TSQualifiedName) typeName = typeName.left;
|
|
5162
|
+
if (typeName.type !== AST_NODE_TYPES20.Identifier) return false;
|
|
5163
|
+
const typeBinding = ASTUtils10.findVariable(context.sourceCode.getScope(typeName), typeName.name);
|
|
5164
|
+
return typeBinding?.defs.length === 1 && typeBinding.defs[0]?.type === "ImportBinding" && typeBinding.defs[0].parent.type === AST_NODE_TYPES20.ImportDeclaration && API_BOUNDARY_IMPORT_RE.test(typeBinding.defs[0].parent.source.value);
|
|
5165
|
+
};
|
|
5137
5166
|
return {
|
|
5138
5167
|
Property(node) {
|
|
5139
5168
|
if (node.computed || node.method || node.shorthand) return;
|
|
5140
5169
|
const key = propertyName(node.key);
|
|
5141
5170
|
const value = memberName3(node.value);
|
|
5142
5171
|
if (key === null || value === null || !isDirectCaseTranslation(key, value)) return;
|
|
5172
|
+
if (!hasApiReceiver(node.value)) return;
|
|
5143
5173
|
const wireName = SNAKE_CASE_RE.test(key) ? key : value;
|
|
5144
5174
|
const applicationName = wireName === key ? value : key;
|
|
5145
5175
|
context.report({
|
|
@@ -5262,7 +5292,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
|
|
|
5262
5292
|
limitations: [
|
|
5263
5293
|
"Only ESLint disable comments and TypeScript expect-error directives are checked.",
|
|
5264
5294
|
"The rule uses a small anchored vocabulary and does not score prose quality generally.",
|
|
5265
|
-
"Generated files and descriptions
|
|
5295
|
+
"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."
|
|
5266
5296
|
],
|
|
5267
5297
|
examples: [
|
|
5268
5298
|
{
|
|
@@ -5272,7 +5302,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
|
|
|
5272
5302
|
files: [
|
|
5273
5303
|
{
|
|
5274
5304
|
path: "src/adapter.ts",
|
|
5275
|
-
source: "// @ts-expect-error -- vendor types omit the runtime requestId field\
|
|
5305
|
+
source: "function requestId(response: object) {\n // @ts-expect-error -- vendor types omit the runtime requestId field\n return response.requestId;\n}"
|
|
5276
5306
|
}
|
|
5277
5307
|
],
|
|
5278
5308
|
focusPath: "src/adapter.ts",
|
|
@@ -5286,7 +5316,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
|
|
|
5286
5316
|
files: [
|
|
5287
5317
|
{
|
|
5288
5318
|
path: "src/adapter.ts",
|
|
5289
|
-
source: "// @ts-expect-error -- false positive\
|
|
5319
|
+
source: "function requestId(response: object) {\n // @ts-expect-error -- false positive\n return response.requestId;\n}"
|
|
5290
5320
|
}
|
|
5291
5321
|
],
|
|
5292
5322
|
focusPath: "src/adapter.ts",
|
|
@@ -5331,15 +5361,15 @@ var no_vague_suppression_description_default = createRule({
|
|
|
5331
5361
|
});
|
|
5332
5362
|
|
|
5333
5363
|
// src/rules/no-generic-single-export-module.ts
|
|
5334
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES22, ASTUtils as
|
|
5364
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES22, ASTUtils as ASTUtils11 } from "@typescript-eslint/utils";
|
|
5335
5365
|
var NO_GENERIC_SINGLE_EXPORT_MODULE_DOCUMENTATION = {
|
|
5336
5366
|
summary: "Disallow generic module stems when one runtime export already names the responsibility.",
|
|
5337
5367
|
rationale: "A generic filename hides the sole exported responsibility and makes navigation less descriptive.",
|
|
5338
5368
|
remediation: "Choose a responsibility-bearing module name or colocate the export with its domain.",
|
|
5339
5369
|
category: "maintainability",
|
|
5340
|
-
limitations: ["Only
|
|
5370
|
+
limitations: ["Only the fixed generic-stem vocabulary with exactly one public runtime export is checked; exported destructuring patterns are excluded rather than undercounted."],
|
|
5341
5371
|
examples: [
|
|
5342
|
-
{ id: "responsibility-named-module", title: "Name the module after its export", outcome: "no-match", files: [{ path: "src/order
|
|
5372
|
+
{ id: "responsibility-named-module", title: "Name the module after its export", outcome: "no-match", files: [{ path: "src/parse-order.ts", source: "export function parseOrder() { return {}; }" }], focusPath: "src/parse-order.ts", expectedCount: 0, public: true },
|
|
5343
5373
|
{ id: "generic-module-name", title: "Do not hide one export in a generic module", outcome: "match", files: [{ path: "src/utils.ts", source: "export function parseOrder() { return {}; }" }], focusPath: "src/utils.ts", expectedCount: 1, public: true }
|
|
5344
5374
|
]
|
|
5345
5375
|
};
|
|
@@ -5400,6 +5430,7 @@ function runtimeExports(program) {
|
|
|
5400
5430
|
}
|
|
5401
5431
|
if (statement.declaration !== null) {
|
|
5402
5432
|
const declaration = statement.declaration;
|
|
5433
|
+
if (declaration.type === AST_NODE_TYPES22.VariableDeclaration && declaration.declarations.some((item) => item.id.type !== AST_NODE_TYPES22.Identifier)) ambiguous = true;
|
|
5403
5434
|
exports.push(...declaredNames(declaration).map((name) => ({ key: name, name, node: declaration })));
|
|
5404
5435
|
}
|
|
5405
5436
|
for (const specifier of statement.specifiers) {
|
|
@@ -5457,7 +5488,7 @@ function typeOnlyBindings(program) {
|
|
|
5457
5488
|
return new Set([...names].filter((name) => !runtimeNames.has(name)));
|
|
5458
5489
|
}
|
|
5459
5490
|
function isGlobalIdentifier2(context, node) {
|
|
5460
|
-
const variable =
|
|
5491
|
+
const variable = ASTUtils11.findVariable(context.sourceCode.getScope(node), node.name);
|
|
5461
5492
|
return variable === null || variable.defs.length === 0;
|
|
5462
5493
|
}
|
|
5463
5494
|
function isConventionalFrameworkUtility(filename, exported) {
|
|
@@ -5518,11 +5549,11 @@ var no_generic_single_export_module_default = createRule({
|
|
|
5518
5549
|
// src/rules/no-offset-pagination.ts
|
|
5519
5550
|
import "@typescript-eslint/utils";
|
|
5520
5551
|
var NO_OFFSET_PAGINATION_DOCUMENTATION = {
|
|
5521
|
-
summary: "
|
|
5552
|
+
summary: "Prefer keyset pagination for embedded SQL queries using OFFSET.",
|
|
5522
5553
|
rationale: "Offset pagination scans skipped rows and shifts page boundaries under concurrent writes.",
|
|
5523
|
-
remediation: "
|
|
5554
|
+
remediation: "Consider a keyset cursor that preserves the query's complete ordering, tie-breakers, and filters.",
|
|
5524
5555
|
category: "performance",
|
|
5525
|
-
limitations: ["
|
|
5556
|
+
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."],
|
|
5526
5557
|
examples: [
|
|
5527
5558
|
{ 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 },
|
|
5528
5559
|
{ 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 }
|
|
@@ -5530,17 +5561,18 @@ var NO_OFFSET_PAGINATION_DOCUMENTATION = {
|
|
|
5530
5561
|
};
|
|
5531
5562
|
var OFFSET_PAGINATION = /\bOFFSET\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
|
|
5532
5563
|
var OFFSET_GATE = /offset/i;
|
|
5564
|
+
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;
|
|
5533
5565
|
var no_offset_pagination_default = createRule({
|
|
5534
5566
|
name: "no-offset-pagination",
|
|
5535
5567
|
documentation: NO_OFFSET_PAGINATION_DOCUMENTATION,
|
|
5536
5568
|
meta: {
|
|
5537
5569
|
type: "problem",
|
|
5538
5570
|
docs: {
|
|
5539
|
-
description: "
|
|
5571
|
+
description: "Prefer keyset pagination for embedded SQL queries using OFFSET."
|
|
5540
5572
|
},
|
|
5541
5573
|
schema: [],
|
|
5542
5574
|
messages: {
|
|
5543
|
-
noOffsetPagination: "OFFSET pagination
|
|
5575
|
+
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."
|
|
5544
5576
|
}
|
|
5545
5577
|
},
|
|
5546
5578
|
defaultOptions: [],
|
|
@@ -5549,7 +5581,7 @@ var no_offset_pagination_default = createRule({
|
|
|
5549
5581
|
return {};
|
|
5550
5582
|
}
|
|
5551
5583
|
return createSqlListener((sql, node) => {
|
|
5552
|
-
if (!OFFSET_PAGINATION.test(sql)) {
|
|
5584
|
+
if (!PAGINATION_CONTEXT.test(sql) || !OFFSET_PAGINATION.test(sql)) {
|
|
5553
5585
|
return;
|
|
5554
5586
|
}
|
|
5555
5587
|
context.report({ node, messageId: "noOffsetPagination" });
|
|
@@ -5558,7 +5590,7 @@ var no_offset_pagination_default = createRule({
|
|
|
5558
5590
|
});
|
|
5559
5591
|
|
|
5560
5592
|
// src/rules/no-positional-tuple-return.ts
|
|
5561
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES23 } from "@typescript-eslint/utils";
|
|
5593
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES23, ASTUtils as ASTUtils12 } from "@typescript-eslint/utils";
|
|
5562
5594
|
var NO_POSITIONAL_TUPLE_RETURN_DOCUMENTATION = {
|
|
5563
5595
|
summary: "Disallow returning a multi-field tuple from a named function; return a named object so call sites cannot mismatch slots.",
|
|
5564
5596
|
rationale: "Tuple fields are identified only by position, so reordering can preserve types while changing meaning.",
|
|
@@ -5588,7 +5620,7 @@ function tupleReturnType(node, aliases, resolving = /* @__PURE__ */ new Set()) {
|
|
|
5588
5620
|
return argument === void 0 ? null : tupleReturnType(argument, aliases, resolving);
|
|
5589
5621
|
}
|
|
5590
5622
|
if (node.type === AST_NODE_TYPES23.TSTypeReference && node.typeName.type === AST_NODE_TYPES23.Identifier && !resolving.has(node.typeName.name)) {
|
|
5591
|
-
const target = aliases.get(node.typeName
|
|
5623
|
+
const target = aliases.get(node.typeName);
|
|
5592
5624
|
if (target !== void 0) return tupleReturnType(target, aliases, /* @__PURE__ */ new Set([...resolving, node.typeName.name]));
|
|
5593
5625
|
}
|
|
5594
5626
|
if (node.type === AST_NODE_TYPES23.TSTypeOperator && node.operator === "readonly") {
|
|
@@ -5680,20 +5712,27 @@ function exportedTypeNames(program) {
|
|
|
5680
5712
|
}
|
|
5681
5713
|
return names;
|
|
5682
5714
|
}
|
|
5683
|
-
function typeAliases(
|
|
5715
|
+
function typeAliases(sourceCode) {
|
|
5684
5716
|
const aliases = /* @__PURE__ */ new Map();
|
|
5685
|
-
for (const statement of
|
|
5717
|
+
for (const statement of sourceCode.ast.body) {
|
|
5686
5718
|
const declaration = statement.type === AST_NODE_TYPES23.ExportNamedDeclaration ? statement.declaration : statement;
|
|
5687
5719
|
if (declaration?.type === AST_NODE_TYPES23.TSTypeAliasDeclaration) {
|
|
5688
|
-
aliases.set(declaration.id.name, declaration
|
|
5720
|
+
aliases.set(declaration.id.name, declaration);
|
|
5689
5721
|
}
|
|
5690
5722
|
}
|
|
5691
|
-
return
|
|
5723
|
+
return {
|
|
5724
|
+
get(identifier) {
|
|
5725
|
+
const declaration = aliases.get(identifier.name);
|
|
5726
|
+
if (declaration === void 0) return void 0;
|
|
5727
|
+
const binding = ASTUtils12.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
5728
|
+
return binding?.defs.length === 1 && binding.defs[0]?.node === declaration ? declaration.typeAnnotation : void 0;
|
|
5729
|
+
}
|
|
5730
|
+
};
|
|
5692
5731
|
}
|
|
5693
5732
|
function callableReturnType(node, aliases, resolving = /* @__PURE__ */ new Set()) {
|
|
5694
5733
|
if (node.type === AST_NODE_TYPES23.TSFunctionType) return node.returnType?.typeAnnotation ?? null;
|
|
5695
5734
|
if (node.type === AST_NODE_TYPES23.TSTypeReference && node.typeName.type === AST_NODE_TYPES23.Identifier && !resolving.has(node.typeName.name)) {
|
|
5696
|
-
const target = aliases.get(node.typeName
|
|
5735
|
+
const target = aliases.get(node.typeName);
|
|
5697
5736
|
if (target !== void 0) {
|
|
5698
5737
|
return callableReturnType(target, aliases, /* @__PURE__ */ new Set([...resolving, node.typeName.name]));
|
|
5699
5738
|
}
|
|
@@ -5822,7 +5861,7 @@ var no_positional_tuple_return_default = createRule({
|
|
|
5822
5861
|
context.sourceCode.ast,
|
|
5823
5862
|
exportedTypeNames(context.sourceCode.ast)
|
|
5824
5863
|
);
|
|
5825
|
-
const aliases = typeAliases(context.sourceCode
|
|
5864
|
+
const aliases = typeAliases(context.sourceCode);
|
|
5826
5865
|
const reportedFunctions = /* @__PURE__ */ new WeakSet();
|
|
5827
5866
|
const functionStack = [];
|
|
5828
5867
|
const report2 = (annotation, name) => {
|
|
@@ -5986,7 +6025,7 @@ var no_production_browser_source_maps_default = createRule({
|
|
|
5986
6025
|
});
|
|
5987
6026
|
|
|
5988
6027
|
// src/rules/no-raw-env.ts
|
|
5989
|
-
import { ASTUtils as
|
|
6028
|
+
import { ASTUtils as ASTUtils13 } from "@typescript-eslint/utils";
|
|
5990
6029
|
var NO_RAW_ENV_DOCUMENTATION = {
|
|
5991
6030
|
summary: "Disallow direct `process.env` and `import.meta.env` reads outside validated boundaries.",
|
|
5992
6031
|
rationale: "Raw environment reads are untyped and defer invalid configuration failures until use.",
|
|
@@ -6068,7 +6107,7 @@ var no_raw_env_default = createRule({
|
|
|
6068
6107
|
},
|
|
6069
6108
|
MemberExpression(node) {
|
|
6070
6109
|
if (isProcessEnv(node) && node.object.type === "Identifier") {
|
|
6071
|
-
const binding =
|
|
6110
|
+
const binding = ASTUtils13.findVariable(context.sourceCode.getScope(node), node.object.name);
|
|
6072
6111
|
if (binding !== null && binding.defs.length > 0 && !binding.defs.every((definition) => definition.type === "ImportBinding" && definition.parent.type === "ImportDeclaration" && ["node:process", "process"].includes(definition.parent.source.value) && ["ImportDefaultSpecifier", "ImportNamespaceSpecifier"].includes(definition.node.type))) return;
|
|
6073
6112
|
}
|
|
6074
6113
|
if ((isProcessEnv(node) || isImportMetaEnv(node)) && !isExemptVariableAccess(node) && !isWriteTarget(node) && !isWholeEnvSpread(node)) {
|
|
@@ -6084,7 +6123,7 @@ var no_raw_env_default = createRule({
|
|
|
6084
6123
|
});
|
|
6085
6124
|
|
|
6086
6125
|
// src/rules/no-raw-fetch-outside-clients.ts
|
|
6087
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES24, ASTUtils as
|
|
6126
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES24, ASTUtils as ASTUtils14 } from "@typescript-eslint/utils";
|
|
6088
6127
|
var NO_RAW_FETCH_OUTSIDE_CLIENTS_DOCUMENTATION = {
|
|
6089
6128
|
summary: "Disallow calling the global `fetch` outside the client layer; route outbound HTTP through a client module that owns retry, timeout and status handling.",
|
|
6090
6129
|
rationale: "Scattered fetch calls bypass shared transport policy and are harder to stub and observe consistently.",
|
|
@@ -6272,7 +6311,7 @@ var no_raw_fetch_outside_clients_default = createRule({
|
|
|
6272
6311
|
internalApiPrefixes.push(`${options.basePath}/api`);
|
|
6273
6312
|
}
|
|
6274
6313
|
function resolvesToGlobal(identifier) {
|
|
6275
|
-
const variable =
|
|
6314
|
+
const variable = ASTUtils14.findVariable(
|
|
6276
6315
|
context.sourceCode.getScope(identifier),
|
|
6277
6316
|
identifier.name
|
|
6278
6317
|
);
|
|
@@ -6281,7 +6320,7 @@ var no_raw_fetch_outside_clients_default = createRule({
|
|
|
6281
6320
|
function resolveNode2(node) {
|
|
6282
6321
|
if (node === void 0) return null;
|
|
6283
6322
|
if (node.type !== AST_NODE_TYPES24.Identifier) return node;
|
|
6284
|
-
const variable =
|
|
6323
|
+
const variable = ASTUtils14.findVariable(
|
|
6285
6324
|
context.sourceCode.getScope(node),
|
|
6286
6325
|
node.name
|
|
6287
6326
|
);
|
|
@@ -6360,13 +6399,13 @@ var no_raw_fetch_outside_clients_default = createRule({
|
|
|
6360
6399
|
});
|
|
6361
6400
|
|
|
6362
6401
|
// src/rules/no-restricted-library-load.ts
|
|
6363
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES25, ASTUtils as
|
|
6402
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES25, ASTUtils as ASTUtils15 } from "@typescript-eslint/utils";
|
|
6364
6403
|
var NO_RESTRICTED_LIBRARY_LOAD_DOCUMENTATION = {
|
|
6365
|
-
summary: "Apply
|
|
6366
|
-
rationale: "
|
|
6367
|
-
remediation: "
|
|
6404
|
+
summary: "Apply configured library restrictions to literal runtime loads and CommonJS resolution references.",
|
|
6405
|
+
rationale: "Dynamic imports, CommonJS loads, and package resolution checks can bypass library restrictions enforced for static imports.",
|
|
6406
|
+
remediation: "Use the configured replacement for the runtime dependency reference; resolution checks do not themselves load a module.",
|
|
6368
6407
|
category: "architecture",
|
|
6369
|
-
limitations: ["Only literal dynamic imports, unshadowed CommonJS loads, and TypeScript import-equals declarations are checked."],
|
|
6408
|
+
limitations: ["Only literal dynamic imports, unshadowed CommonJS loads/resolution calls, and runtime TypeScript import-equals declarations are checked; erased type imports are excluded. A configured restriction list is required."],
|
|
6370
6409
|
examples: [
|
|
6371
6410
|
{ id: "static-import", title: "Static imports remain the static-import rule's responsibility", outcome: "no-match", files: [{ path: "src/client.ts", source: "import axios from 'axios';" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
|
|
6372
6411
|
{ id: "runtime-load", title: "Do not load a restricted library at runtime", outcome: "match", files: [{ path: "src/client.ts", source: "const client = require('axios');" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
|
|
@@ -6384,7 +6423,7 @@ var no_restricted_library_load_default = createRule({
|
|
|
6384
6423
|
meta: {
|
|
6385
6424
|
type: "problem",
|
|
6386
6425
|
docs: {
|
|
6387
|
-
description:
|
|
6426
|
+
description: NO_RESTRICTED_LIBRARY_LOAD_DOCUMENTATION.summary
|
|
6388
6427
|
},
|
|
6389
6428
|
schema: [
|
|
6390
6429
|
{
|
|
@@ -6410,7 +6449,7 @@ var no_restricted_library_load_default = createRule({
|
|
|
6410
6449
|
}
|
|
6411
6450
|
],
|
|
6412
6451
|
messages: {
|
|
6413
|
-
restrictedLibraryLoad: "{{id}}: Replace runtime
|
|
6452
|
+
restrictedLibraryLoad: "{{id}}: Replace this runtime dependency reference to {{module}} with {{replacement}}.{{note}}"
|
|
6414
6453
|
}
|
|
6415
6454
|
},
|
|
6416
6455
|
defaultOptions: [{ libraries: [] }],
|
|
@@ -6433,7 +6472,7 @@ var no_restricted_library_load_default = createRule({
|
|
|
6433
6472
|
});
|
|
6434
6473
|
}
|
|
6435
6474
|
function isUnshadowedRequire(node) {
|
|
6436
|
-
const variable =
|
|
6475
|
+
const variable = ASTUtils15.findVariable(
|
|
6437
6476
|
context.sourceCode.getScope(node),
|
|
6438
6477
|
node.name
|
|
6439
6478
|
);
|
|
@@ -6456,6 +6495,7 @@ var no_restricted_library_load_default = createRule({
|
|
|
6456
6495
|
if (source !== null) report2(node.arguments[0], source);
|
|
6457
6496
|
},
|
|
6458
6497
|
TSImportEqualsDeclaration(node) {
|
|
6498
|
+
if (node.importKind === "type") return;
|
|
6459
6499
|
if (node.moduleReference.type !== AST_NODE_TYPES25.TSExternalModuleReference) return;
|
|
6460
6500
|
const source = literalModule(node.moduleReference.expression);
|
|
6461
6501
|
if (source !== null) report2(node.moduleReference.expression, source);
|
|
@@ -6465,7 +6505,7 @@ var no_restricted_library_load_default = createRule({
|
|
|
6465
6505
|
});
|
|
6466
6506
|
|
|
6467
6507
|
// src/rules/no-router-refresh-polling.ts
|
|
6468
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES26, ASTUtils as
|
|
6508
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES26, ASTUtils as ASTUtils16 } from "@typescript-eslint/utils";
|
|
6469
6509
|
var NO_ROUTER_REFRESH_POLLING_DOCUMENTATION = {
|
|
6470
6510
|
summary: "Do not poll by calling a Next.js router's refresh method from a timer.",
|
|
6471
6511
|
rationale: "A route refresh refetches and rerenders the whole route on every tick instead of loading the named resource that changed.",
|
|
@@ -6495,7 +6535,7 @@ function isIntervalCallee(sourceCode, node) {
|
|
|
6495
6535
|
return node.type === AST_NODE_TYPES26.Identifier && node.name === "setInterval" && isUnshadowedGlobal2(sourceCode, node) || node.type === AST_NODE_TYPES26.MemberExpression && !node.computed && node.object.type === AST_NODE_TYPES26.Identifier && (node.object.name === "window" || node.object.name === "globalThis") && isUnshadowedGlobal2(sourceCode, node.object) && node.property.type === AST_NODE_TYPES26.Identifier && node.property.name === "setInterval";
|
|
6496
6536
|
}
|
|
6497
6537
|
function isUnshadowedGlobal2(sourceCode, node) {
|
|
6498
|
-
const variable =
|
|
6538
|
+
const variable = ASTUtils16.findVariable(sourceCode.getScope(node), node.name);
|
|
6499
6539
|
return variable === null || variable.defs.length === 0;
|
|
6500
6540
|
}
|
|
6501
6541
|
var no_router_refresh_polling_default = createRule({
|
|
@@ -6518,21 +6558,21 @@ var no_router_refresh_polling_default = createRule({
|
|
|
6518
6558
|
if (node.source.value !== "next/navigation") return;
|
|
6519
6559
|
for (const specifier of node.specifiers) {
|
|
6520
6560
|
if (specifier.type === AST_NODE_TYPES26.ImportSpecifier && importedName4(specifier) === "useRouter") {
|
|
6521
|
-
const variable =
|
|
6561
|
+
const variable = ASTUtils16.findVariable(context.sourceCode.getScope(specifier.local), specifier.local.name);
|
|
6522
6562
|
if (variable !== null) routerHooks.add(variable);
|
|
6523
6563
|
}
|
|
6524
6564
|
}
|
|
6525
6565
|
},
|
|
6526
6566
|
VariableDeclarator(node) {
|
|
6527
6567
|
if (node.id.type === AST_NODE_TYPES26.Identifier && node.init?.type === AST_NODE_TYPES26.CallExpression && node.init.callee.type === AST_NODE_TYPES26.Identifier) {
|
|
6528
|
-
const hook =
|
|
6529
|
-
const router =
|
|
6568
|
+
const hook = ASTUtils16.findVariable(context.sourceCode.getScope(node.init.callee), node.init.callee.name);
|
|
6569
|
+
const router = ASTUtils16.findVariable(context.sourceCode.getScope(node.id), node.id.name);
|
|
6530
6570
|
if (hook !== null && router !== null && routerHooks.has(hook)) routers.add(router);
|
|
6531
6571
|
}
|
|
6532
6572
|
},
|
|
6533
6573
|
CallExpression(node) {
|
|
6534
6574
|
if (node.callee.type !== AST_NODE_TYPES26.MemberExpression || node.callee.computed || node.callee.object.type !== AST_NODE_TYPES26.Identifier || node.callee.property.type !== AST_NODE_TYPES26.Identifier || node.callee.property.name !== "refresh") return;
|
|
6535
|
-
const router =
|
|
6575
|
+
const router = ASTUtils16.findVariable(
|
|
6536
6576
|
context.sourceCode.getScope(node.callee.object),
|
|
6537
6577
|
node.callee.object.name
|
|
6538
6578
|
);
|
|
@@ -6572,7 +6612,7 @@ var NO_REPEATED_STRING_LITERAL_DOCUMENTATION = {
|
|
|
6572
6612
|
]
|
|
6573
6613
|
};
|
|
6574
6614
|
function isStructured(value) {
|
|
6575
|
-
return
|
|
6615
|
+
return SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value) || URL_PATH_RE.test(value);
|
|
6576
6616
|
}
|
|
6577
6617
|
function preview(value) {
|
|
6578
6618
|
const oneLine = value.replaceAll("\n", " ").trim();
|
|
@@ -6593,7 +6633,7 @@ function isScaffolding(node) {
|
|
|
6593
6633
|
}
|
|
6594
6634
|
const isNonComputedPropertyKey = (parent.type === AST_NODE_TYPES27.Property || parent.type === AST_NODE_TYPES27.PropertyDefinition || parent.type === AST_NODE_TYPES27.MethodDefinition || parent.type === AST_NODE_TYPES27.AccessorProperty) && parent.key === node && !parent.computed;
|
|
6595
6635
|
const isRequireSource = parent.type === AST_NODE_TYPES27.CallExpression && parent.callee.type === AST_NODE_TYPES27.Identifier && parent.callee.name === "require";
|
|
6596
|
-
return parent.type === AST_NODE_TYPES27.ImportDeclaration || parent.type === AST_NODE_TYPES27.ImportExpression || parent.type === AST_NODE_TYPES27.ExportNamedDeclaration || parent.type === AST_NODE_TYPES27.ExportAllDeclaration || parent.type === AST_NODE_TYPES27.TSImportType || parent.type === AST_NODE_TYPES27.JSXAttribute || parent.type === AST_NODE_TYPES27.TSLiteralType || isNonComputedPropertyKey || isRequireSource;
|
|
6636
|
+
return parent.type === AST_NODE_TYPES27.ImportDeclaration || parent.type === AST_NODE_TYPES27.ImportExpression || parent.type === AST_NODE_TYPES27.ExportNamedDeclaration || parent.type === AST_NODE_TYPES27.ExportAllDeclaration || parent.type === AST_NODE_TYPES27.TSImportType || parent.type === AST_NODE_TYPES27.JSXAttribute || parent.type === AST_NODE_TYPES27.JSXExpressionContainer && parent.parent.type === AST_NODE_TYPES27.JSXAttribute || parent.type === AST_NODE_TYPES27.TSLiteralType || isNonComputedPropertyKey || isRequireSource;
|
|
6597
6637
|
}
|
|
6598
6638
|
var no_repeated_string_literal_default = createRule({
|
|
6599
6639
|
name: "no-repeated-string-literal",
|
|
@@ -7513,11 +7553,11 @@ var no_server_env_in_client_component_default = createRule({
|
|
|
7513
7553
|
// src/rules/no-select-star.ts
|
|
7514
7554
|
import "@typescript-eslint/utils";
|
|
7515
7555
|
var NO_SELECT_STAR_DOCUMENTATION = {
|
|
7516
|
-
summary: "
|
|
7556
|
+
summary: "Prefer explicit column projections over SELECT * in embedded SQL.",
|
|
7517
7557
|
rationale: "Wildcard projections couple row shape and query cost to unrelated schema changes.",
|
|
7518
7558
|
remediation: "List every required column explicitly in the projection.",
|
|
7519
7559
|
category: "correctness",
|
|
7520
|
-
limitations: ["Only statically visible embedded SQL is checked; function arguments such as COUNT(*) and stars inside EXISTS are excluded."],
|
|
7560
|
+
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."],
|
|
7521
7561
|
examples: [
|
|
7522
7562
|
{ 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 },
|
|
7523
7563
|
{ 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 }
|
|
@@ -7568,7 +7608,7 @@ var no_select_star_default = createRule({
|
|
|
7568
7608
|
meta: {
|
|
7569
7609
|
type: "problem",
|
|
7570
7610
|
docs: {
|
|
7571
|
-
description: "
|
|
7611
|
+
description: "Prefer explicit column projections over SELECT * in embedded SQL."
|
|
7572
7612
|
},
|
|
7573
7613
|
schema: [],
|
|
7574
7614
|
messages: {
|
|
@@ -7590,13 +7630,13 @@ var no_select_star_default = createRule({
|
|
|
7590
7630
|
});
|
|
7591
7631
|
|
|
7592
7632
|
// src/rules/no-sentinel-return-on-catch.ts
|
|
7593
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES30 } from "@typescript-eslint/utils";
|
|
7633
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES30, ASTUtils as ASTUtils17 } from "@typescript-eslint/utils";
|
|
7594
7634
|
var NO_SENTINEL_RETURN_ON_CATCH_DOCUMENTATION = {
|
|
7595
7635
|
summary: "Disallow swallowing a caught error by returning an empty sentinel unless the error is handled or the sentinel is part of the function contract.",
|
|
7596
7636
|
rationale: "An unreported fallback makes operational failure indistinguishable from a legitimate empty result.",
|
|
7597
7637
|
remediation: "Rethrow, report the error before returning, or model expected absence with an explicit predicate, safe-parse, or result contract.",
|
|
7598
7638
|
category: "correctness",
|
|
7599
|
-
limitations: ["Recognized predicate, safe-parse, normal-path sentinel, deliberate parse, generated-client, and configured logging patterns are excluded."],
|
|
7639
|
+
limitations: ["Recognized predicate, safe-parse, normal-path sentinel, deliberate parse, generated-client, and configured logging patterns are excluded. Locally shadowed undefined bindings are not treated as sentinels; recognized handling patterns are not a proof that every control-flow path handles the error."],
|
|
7600
7640
|
examples: [
|
|
7601
7641
|
{ id: "reported-fallback", title: "Report an error before returning a fallback", outcome: "no-match", files: [{ path: "src/load.ts", source: "function load() { try { return read(); } catch (error) { logger.warn('load failed', error); return null; } }" }], focusPath: "src/load.ts", expectedCount: 0, public: true },
|
|
7602
7642
|
{ id: "silent-fallback", title: "Do not turn an unreported error into absence", outcome: "match", files: [{ path: "src/load.ts", source: "function load() { try { return read(); } catch { return null; } }" }], focusPath: "src/load.ts", expectedCount: 1, public: true }
|
|
@@ -8011,6 +8051,8 @@ var no_sentinel_return_on_catch_default = createRule({
|
|
|
8011
8051
|
if (!isSentinelArgument(last.argument)) {
|
|
8012
8052
|
return;
|
|
8013
8053
|
}
|
|
8054
|
+
const returned = unwrapSentinelExpression(last.argument);
|
|
8055
|
+
if (returned?.type === AST_NODE_TYPES30.Identifier && returned.name === "undefined" && (ASTUtils17.findVariable(context.sourceCode.getScope(returned), returned.name)?.defs.length ?? 0) > 0) return;
|
|
8014
8056
|
if (containsThrow(node.body)) {
|
|
8015
8057
|
return;
|
|
8016
8058
|
}
|
|
@@ -8044,13 +8086,13 @@ var no_sentinel_return_on_catch_default = createRule({
|
|
|
8044
8086
|
});
|
|
8045
8087
|
|
|
8046
8088
|
// src/rules/no-silent-promise-catch.ts
|
|
8047
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES31 } from "@typescript-eslint/utils";
|
|
8089
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES31, ASTUtils as ASTUtils18 } from "@typescript-eslint/utils";
|
|
8048
8090
|
var NO_SILENT_PROMISE_CATCH_DOCUMENTATION = {
|
|
8049
8091
|
summary: "Disallow `.catch()` and second-argument `.then()` handlers that silently swallow a rejection; log, rethrow, or handle the error.",
|
|
8050
8092
|
rationale: "A swallowed rejection hides failures and gives callers an indistinguishable fallback value.",
|
|
8051
8093
|
remediation: "Log, rethrow, or explicitly recover from the rejection; explain intentional teardown suppression.",
|
|
8052
8094
|
category: "correctness",
|
|
8053
|
-
limitations: ["Test files, teardown calls, explanatory comments, non-function handlers, and handlers that consume or report the error are excluded."],
|
|
8095
|
+
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."],
|
|
8054
8096
|
examples: [
|
|
8055
8097
|
{ 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 },
|
|
8056
8098
|
{ 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 }
|
|
@@ -8064,6 +8106,43 @@ var BODY_PARSE_METHODS = /* @__PURE__ */ new Set([
|
|
|
8064
8106
|
"json",
|
|
8065
8107
|
"text"
|
|
8066
8108
|
]);
|
|
8109
|
+
var ZOD_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
8110
|
+
"any",
|
|
8111
|
+
"array",
|
|
8112
|
+
"bigint",
|
|
8113
|
+
"boolean",
|
|
8114
|
+
"custom",
|
|
8115
|
+
"date",
|
|
8116
|
+
"enum",
|
|
8117
|
+
"literal",
|
|
8118
|
+
"map",
|
|
8119
|
+
"never",
|
|
8120
|
+
"null",
|
|
8121
|
+
"number",
|
|
8122
|
+
"object",
|
|
8123
|
+
"record",
|
|
8124
|
+
"set",
|
|
8125
|
+
"string",
|
|
8126
|
+
"tuple",
|
|
8127
|
+
"undefined",
|
|
8128
|
+
"union",
|
|
8129
|
+
"unknown"
|
|
8130
|
+
]);
|
|
8131
|
+
var ZOD_CHAIN_METHODS = /* @__PURE__ */ new Set([
|
|
8132
|
+
"array",
|
|
8133
|
+
"catch",
|
|
8134
|
+
"default",
|
|
8135
|
+
"describe",
|
|
8136
|
+
"max",
|
|
8137
|
+
"min",
|
|
8138
|
+
"nullable",
|
|
8139
|
+
"nullish",
|
|
8140
|
+
"optional",
|
|
8141
|
+
"readonly",
|
|
8142
|
+
"refine",
|
|
8143
|
+
"superRefine",
|
|
8144
|
+
"transform"
|
|
8145
|
+
]);
|
|
8067
8146
|
function isBodyParseCall(node) {
|
|
8068
8147
|
return node.type === AST_NODE_TYPES31.CallExpression && node.arguments.length === 0 && node.callee.type === AST_NODE_TYPES31.MemberExpression && !node.callee.computed && node.callee.property.type === AST_NODE_TYPES31.Identifier && BODY_PARSE_METHODS.has(node.callee.property.name);
|
|
8069
8148
|
}
|
|
@@ -8137,6 +8216,26 @@ var no_silent_promise_catch_default = createRule({
|
|
|
8137
8216
|
if (isTestFile(context.filename) || isScriptFile(context.filename)) {
|
|
8138
8217
|
return {};
|
|
8139
8218
|
}
|
|
8219
|
+
function isZodSchema(node, seen = /* @__PURE__ */ new Set()) {
|
|
8220
|
+
if (seen.has(node)) return false;
|
|
8221
|
+
seen.add(node);
|
|
8222
|
+
if (node.type === AST_NODE_TYPES31.Identifier) {
|
|
8223
|
+
const binding = ASTUtils18.findVariable(context.sourceCode.getScope(node), node.name);
|
|
8224
|
+
if (binding === null || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
8225
|
+
const [definition] = binding.defs;
|
|
8226
|
+
return binding.defs.length === 1 && definition?.node.type === AST_NODE_TYPES31.VariableDeclarator && definition.node.init !== null && isZodSchema(definition.node.init, seen);
|
|
8227
|
+
}
|
|
8228
|
+
if (node.type !== AST_NODE_TYPES31.CallExpression || node.callee.type !== AST_NODE_TYPES31.MemberExpression || node.callee.computed || node.callee.property.type !== AST_NODE_TYPES31.Identifier) return false;
|
|
8229
|
+
const { object, property } = node.callee;
|
|
8230
|
+
if (object.type === AST_NODE_TYPES31.Identifier && ZOD_CONSTRUCTORS.has(property.name)) {
|
|
8231
|
+
const binding = ASTUtils18.findVariable(context.sourceCode.getScope(object), object.name);
|
|
8232
|
+
if (binding?.defs.some((definition) => {
|
|
8233
|
+
const specifier = definition.node;
|
|
8234
|
+
return (specifier.type === AST_NODE_TYPES31.ImportNamespaceSpecifier || specifier.type === AST_NODE_TYPES31.ImportDefaultSpecifier || specifier.type === AST_NODE_TYPES31.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES31.Identifier && specifier.imported.name === "z") && specifier.parent.type === AST_NODE_TYPES31.ImportDeclaration && isZodModule(String(specifier.parent.source.value));
|
|
8235
|
+
})) return true;
|
|
8236
|
+
}
|
|
8237
|
+
return ZOD_CHAIN_METHODS.has(property.name) && isZodSchema(object, seen);
|
|
8238
|
+
}
|
|
8140
8239
|
const hasExplanatoryComment = (call, handler) => {
|
|
8141
8240
|
const sourceCode = context.sourceCode;
|
|
8142
8241
|
if (sourceCode.getCommentsInside(handler).some(isExplanatory)) {
|
|
@@ -8161,6 +8260,7 @@ var no_silent_promise_catch_default = createRule({
|
|
|
8161
8260
|
const method = node.callee.property.name;
|
|
8162
8261
|
const handlerIndex = method === "catch" ? 0 : method === "then" ? 1 : null;
|
|
8163
8262
|
if (handlerIndex === null) return;
|
|
8263
|
+
if (method === "catch" && isZodSchema(node.callee.object)) return;
|
|
8164
8264
|
if (isBodyParseCall(node.callee.object)) {
|
|
8165
8265
|
return;
|
|
8166
8266
|
}
|
|
@@ -8193,16 +8293,16 @@ var no_silent_promise_catch_default = createRule({
|
|
|
8193
8293
|
});
|
|
8194
8294
|
|
|
8195
8295
|
// src/rules/no-sleep-in-test-body.ts
|
|
8196
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES32 } from "@typescript-eslint/utils";
|
|
8296
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES32, ASTUtils as ASTUtils19 } from "@typescript-eslint/utils";
|
|
8197
8297
|
var NO_SLEEP_IN_TEST_BODY_DOCUMENTATION = {
|
|
8198
|
-
summary: "
|
|
8298
|
+
summary: "Avoid fixed timed sleeps directly in test bodies; synchronize on observable behavior or use controlled timers.",
|
|
8199
8299
|
rationale: "Wall-clock delays make test correctness depend on scheduler and machine speed.",
|
|
8200
|
-
remediation: "Await the observable signal or advance
|
|
8300
|
+
remediation: "Await the observable signal, or advance fake timers when supported and restore real timers in finally or a teardown hook.",
|
|
8201
8301
|
category: "testing",
|
|
8202
8302
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
8203
|
-
limitations: ["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked; nested fakes
|
|
8303
|
+
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."],
|
|
8204
8304
|
examples: [
|
|
8205
|
-
{ id: "fake-timer", title: "Advance time
|
|
8305
|
+
{ 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 },
|
|
8206
8306
|
{ 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 }
|
|
8207
8307
|
]
|
|
8208
8308
|
};
|
|
@@ -8221,9 +8321,6 @@ var FUNCTION_TYPES5 = /* @__PURE__ */ new Set([
|
|
|
8221
8321
|
function isNonzeroNumericLiteral(node) {
|
|
8222
8322
|
return node?.type === AST_NODE_TYPES32.Literal && typeof node.value === "number" && node.value !== 0;
|
|
8223
8323
|
}
|
|
8224
|
-
function isTimedSetTimeout(node) {
|
|
8225
|
-
return node.type === AST_NODE_TYPES32.CallExpression && node.callee.type === AST_NODE_TYPES32.Identifier && node.callee.name === "setTimeout" && node.arguments.length >= 2 && isNonzeroNumericLiteral(node.arguments[1]);
|
|
8226
|
-
}
|
|
8227
8324
|
function isPromiseSleep(node) {
|
|
8228
8325
|
if (node.callee.type !== AST_NODE_TYPES32.Identifier || node.callee.name !== "Promise") {
|
|
8229
8326
|
return false;
|
|
@@ -8233,12 +8330,14 @@ function isPromiseSleep(node) {
|
|
|
8233
8330
|
return false;
|
|
8234
8331
|
}
|
|
8235
8332
|
const body2 = executor.body;
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8333
|
+
const resolve2 = executor.params[0];
|
|
8334
|
+
if (executor.params.length !== 1 || resolve2?.type !== AST_NODE_TYPES32.Identifier || resolve2.name === "setTimeout") return false;
|
|
8335
|
+
const statement = body2.type === AST_NODE_TYPES32.BlockStatement && body2.body.length === 1 ? body2.body[0] : null;
|
|
8336
|
+
const timer = body2.type !== AST_NODE_TYPES32.BlockStatement ? body2 : statement?.type === AST_NODE_TYPES32.ExpressionStatement ? statement.expression : null;
|
|
8337
|
+
return timer?.type === AST_NODE_TYPES32.CallExpression && isTimedSetTimeout(timer) && timer.arguments[0]?.type === AST_NODE_TYPES32.Identifier && timer.arguments[0].name === resolve2.name;
|
|
8338
|
+
}
|
|
8339
|
+
function isTimedSetTimeout(node) {
|
|
8340
|
+
return node.type === AST_NODE_TYPES32.CallExpression && node.callee.type === AST_NODE_TYPES32.Identifier && node.callee.name === "setTimeout" && node.arguments.length >= 2 && isNonzeroNumericLiteral(node.arguments[1]);
|
|
8242
8341
|
}
|
|
8243
8342
|
function isHelperSleep(node) {
|
|
8244
8343
|
return node.callee.type === AST_NODE_TYPES32.Identifier && SLEEP_HELPERS.has(node.callee.name) && node.arguments.length >= 1 && isNonzeroNumericLiteral(node.arguments[0]);
|
|
@@ -8292,11 +8391,11 @@ var no_sleep_in_test_body_default = createRule({
|
|
|
8292
8391
|
meta: {
|
|
8293
8392
|
type: "problem",
|
|
8294
8393
|
docs: {
|
|
8295
|
-
description: "
|
|
8394
|
+
description: "Avoid fixed timed sleeps directly in test bodies; synchronize on observable behavior or use controlled timers."
|
|
8296
8395
|
},
|
|
8297
8396
|
schema: [],
|
|
8298
8397
|
messages: {
|
|
8299
|
-
noSleepInTestBody: "A fixed sleep
|
|
8398
|
+
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."
|
|
8300
8399
|
}
|
|
8301
8400
|
},
|
|
8302
8401
|
defaultOptions: [],
|
|
@@ -8317,11 +8416,17 @@ var no_sleep_in_test_body_default = createRule({
|
|
|
8317
8416
|
return {
|
|
8318
8417
|
NewExpression(node) {
|
|
8319
8418
|
if (isPromiseSleep(node)) {
|
|
8419
|
+
const constructor = ASTUtils19.findVariable(context.sourceCode.getScope(node), "Promise");
|
|
8420
|
+
const timer = ASTUtils19.findVariable(context.sourceCode.getScope(node), "setTimeout");
|
|
8421
|
+
if ((constructor?.defs.length ?? 0) > 0 || (timer?.defs.length ?? 0) > 0) return;
|
|
8320
8422
|
report2(node);
|
|
8321
8423
|
}
|
|
8322
8424
|
},
|
|
8323
8425
|
CallExpression(node) {
|
|
8324
8426
|
if (isHelperSleep(node)) {
|
|
8427
|
+
if (node.callee.type !== AST_NODE_TYPES32.Identifier) return;
|
|
8428
|
+
const variable = ASTUtils19.findVariable(context.sourceCode.getScope(node), node.callee.name);
|
|
8429
|
+
if (variable?.defs.some((definition) => definition.type !== "ImportBinding")) return;
|
|
8325
8430
|
report2(node);
|
|
8326
8431
|
}
|
|
8327
8432
|
}
|
|
@@ -8342,7 +8447,7 @@ var NO_STORAGE_IN_STATELESS_MODULES_DOCUMENTATION = {
|
|
|
8342
8447
|
rationale: "Private storage in a stateless workflow creates another source of truth that can silently diverge.",
|
|
8343
8448
|
remediation: "Read from the system of record or derive state from an artifact the workflow already produces.",
|
|
8344
8449
|
category: "architecture",
|
|
8345
|
-
limitations: ["
|
|
8450
|
+
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."],
|
|
8346
8451
|
examples: [
|
|
8347
8452
|
{ 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 },
|
|
8348
8453
|
{ 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 }
|
|
@@ -8376,6 +8481,17 @@ function storageMethodName(node, methods) {
|
|
|
8376
8481
|
if (name === "put" && !isStorageLikeReceiver(callee.object)) {
|
|
8377
8482
|
return null;
|
|
8378
8483
|
}
|
|
8484
|
+
if (name === "prepare") {
|
|
8485
|
+
const argument = node.arguments[0];
|
|
8486
|
+
const text = argument === void 0 ? null : sqlTextOf(argument);
|
|
8487
|
+
if (text !== null) {
|
|
8488
|
+
if (!/^\s*(?:SELECT|WITH|INSERT|UPDATE|DELETE|REPLACE|CREATE|ALTER|DROP|PRAGMA|EXPLAIN)\b/iu.test(stripSqlNoise(text))) return null;
|
|
8489
|
+
} else {
|
|
8490
|
+
const receiver = callee.object;
|
|
8491
|
+
const receiverName = receiver.type === AST_NODE_TYPES33.Identifier ? receiver.name : receiver.type === AST_NODE_TYPES33.MemberExpression && !receiver.computed && receiver.property.type === AST_NODE_TYPES33.Identifier ? receiver.property.name : "";
|
|
8492
|
+
if (!/^(?:db|database|connection)$/iu.test(receiverName)) return null;
|
|
8493
|
+
}
|
|
8494
|
+
}
|
|
8379
8495
|
return name;
|
|
8380
8496
|
}
|
|
8381
8497
|
function isStorageLikeReceiver(node) {
|
|
@@ -8460,12 +8576,13 @@ var no_storage_in_stateless_modules_default = createRule({
|
|
|
8460
8576
|
// src/rules/no-string-concat-in-loop.ts
|
|
8461
8577
|
import "@typescript-eslint/utils";
|
|
8462
8578
|
var NO_STRING_CONCAT_IN_LOOP_DOCUMENTATION = {
|
|
8463
|
-
summary: "
|
|
8579
|
+
summary: "Prefer collecting string fragments over repeatedly accumulating a growing string inside a loop.",
|
|
8464
8580
|
rationale: "Repeatedly rebuilding a growing string can copy all prior content on each iteration, making total work grow quadratically.",
|
|
8465
|
-
remediation: "
|
|
8581
|
+
remediation: "Consider collecting fragments and joining once; preserve intermediate observations and coercion timing, and measure hot paths.",
|
|
8466
8582
|
category: "performance",
|
|
8467
8583
|
limitations: [
|
|
8468
|
-
"Only local identifiers initialized with a string or template literal and accumulated in a loop body are inspected."
|
|
8584
|
+
"Only local identifiers initialized with a string or template literal and accumulated in a loop body are inspected.",
|
|
8585
|
+
"Deferred function bodies are excluded except recognized direct forEach callbacks. Syntax does not establish engine-specific string allocation complexity."
|
|
8469
8586
|
],
|
|
8470
8587
|
examples: [
|
|
8471
8588
|
{
|
|
@@ -8583,6 +8700,7 @@ function enclosingLoop(node) {
|
|
|
8583
8700
|
if ((parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") && parent.parent.type === "CallExpression" && parent.parent.arguments[0] === parent && parent.parent.callee.type === "MemberExpression" && !parent.parent.callee.computed && parent.parent.callee.property.type === "Identifier" && parent.parent.callee.property.name === "forEach") {
|
|
8584
8701
|
return parent.parent;
|
|
8585
8702
|
}
|
|
8703
|
+
if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression" || parent.type === "FunctionDeclaration") return null;
|
|
8586
8704
|
if (LOOP_NODE_TYPES.has(parent.type)) {
|
|
8587
8705
|
const loop = parent;
|
|
8588
8706
|
if (loop.body === child) {
|
|
@@ -8594,6 +8712,19 @@ function enclosingLoop(node) {
|
|
|
8594
8712
|
}
|
|
8595
8713
|
return null;
|
|
8596
8714
|
}
|
|
8715
|
+
function immediatelyExitsLoop(node, loop) {
|
|
8716
|
+
if (!LOOP_NODE_TYPES.has(loop.type) || node.parent.type !== "ExpressionStatement") return false;
|
|
8717
|
+
const statement = node.parent;
|
|
8718
|
+
const block = statement.parent;
|
|
8719
|
+
if (block.type !== "BlockStatement") return false;
|
|
8720
|
+
const next = block.body[block.body.indexOf(statement) + 1];
|
|
8721
|
+
if (next?.type !== "BreakStatement" && next?.type !== "ReturnStatement" && next?.type !== "ThrowStatement") return false;
|
|
8722
|
+
if (next.type === "BreakStatement" && next.label !== null) return false;
|
|
8723
|
+
for (let current = block; current !== void 0 && current !== loop; current = current.parent) {
|
|
8724
|
+
if (current.type === "TryStatement" || next.type === "BreakStatement" && current.type === "SwitchStatement") return false;
|
|
8725
|
+
}
|
|
8726
|
+
return true;
|
|
8727
|
+
}
|
|
8597
8728
|
function isSmallStaticForLoop(node) {
|
|
8598
8729
|
if (node.type !== "ForStatement" || node.init?.type !== "VariableDeclaration" || node.init.declarations.length !== 1 || node.test?.type !== "BinaryExpression" || node.test.operator !== "<" && node.test.operator !== "<=" || node.update?.type !== "UpdateExpression" || node.update.operator !== "++") {
|
|
8599
8730
|
return false;
|
|
@@ -8632,12 +8763,12 @@ var no_string_concat_in_loop_default = createRule({
|
|
|
8632
8763
|
meta: {
|
|
8633
8764
|
type: "suggestion",
|
|
8634
8765
|
docs: {
|
|
8635
|
-
description:
|
|
8766
|
+
description: NO_STRING_CONCAT_IN_LOOP_DOCUMENTATION.summary
|
|
8636
8767
|
},
|
|
8637
8768
|
schema: [],
|
|
8638
8769
|
messages: {
|
|
8639
|
-
noStringConcatInLoop:
|
|
8640
|
-
noStringReduce: "
|
|
8770
|
+
noStringConcatInLoop: "This loop repeatedly accumulates a growing string. Consider collecting fragments and joining once; preserve coercion timing and intermediate reads, and measure performance-sensitive paths.",
|
|
8771
|
+
noStringReduce: "This reduce repeatedly accumulates a growing string. Consider mapping fragments and joining once if coercion timing and intermediate observations are unchanged."
|
|
8641
8772
|
}
|
|
8642
8773
|
},
|
|
8643
8774
|
defaultOptions: [],
|
|
@@ -8664,6 +8795,7 @@ var no_string_concat_in_loop_default = createRule({
|
|
|
8664
8795
|
if (loop === null) {
|
|
8665
8796
|
return;
|
|
8666
8797
|
}
|
|
8798
|
+
if (immediatelyExitsLoop(node, loop)) return;
|
|
8667
8799
|
if (isSmallStaticForLoop(loop)) {
|
|
8668
8800
|
return;
|
|
8669
8801
|
}
|
|
@@ -8697,14 +8829,14 @@ var no_string_concat_in_loop_default = createRule({
|
|
|
8697
8829
|
});
|
|
8698
8830
|
|
|
8699
8831
|
// src/rules/no-tautological-expect.ts
|
|
8700
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES34 } from "@typescript-eslint/utils";
|
|
8832
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES34, ASTUtils as ASTUtils20 } from "@typescript-eslint/utils";
|
|
8701
8833
|
var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
8702
|
-
summary: "Disallow
|
|
8834
|
+
summary: "Disallow supported literal-only assertions that are statically known to pass.",
|
|
8703
8835
|
rationale: "An assertion determined entirely by literals does not observe the code under test and can keep passing after that code is removed.",
|
|
8704
8836
|
remediation: "Assert on a value produced by the behavior under test, or remove the assertion.",
|
|
8705
8837
|
category: "testing",
|
|
8706
8838
|
limitations: [
|
|
8707
|
-
"Only direct supported `expect` matcher calls in recognized test files are inspected."
|
|
8839
|
+
"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."
|
|
8708
8840
|
],
|
|
8709
8841
|
examples: [
|
|
8710
8842
|
{
|
|
@@ -8741,11 +8873,11 @@ var NUMERIC_SIGNS = /* @__PURE__ */ new Set(["-", "+"]);
|
|
|
8741
8873
|
function isLiteral(node) {
|
|
8742
8874
|
switch (node.type) {
|
|
8743
8875
|
case AST_NODE_TYPES34.Literal:
|
|
8744
|
-
return
|
|
8876
|
+
return !("regex" in node);
|
|
8745
8877
|
case AST_NODE_TYPES34.TemplateLiteral:
|
|
8746
8878
|
return node.expressions.length === 0;
|
|
8747
8879
|
case AST_NODE_TYPES34.UnaryExpression:
|
|
8748
|
-
return NUMERIC_SIGNS.has(node.operator) &&
|
|
8880
|
+
return NUMERIC_SIGNS.has(node.operator) && node.argument.type === AST_NODE_TYPES34.Literal && typeof node.argument.value === "number";
|
|
8749
8881
|
case AST_NODE_TYPES34.ArrayExpression:
|
|
8750
8882
|
return node.elements.every((element) => element !== null && isLiteral(element));
|
|
8751
8883
|
case AST_NODE_TYPES34.ObjectExpression:
|
|
@@ -8759,6 +8891,43 @@ function isLiteral(node) {
|
|
|
8759
8891
|
function isStructuralLiteral(node) {
|
|
8760
8892
|
return node.type === AST_NODE_TYPES34.ArrayExpression || node.type === AST_NODE_TYPES34.ObjectExpression;
|
|
8761
8893
|
}
|
|
8894
|
+
function passesZeroArgumentMatcher(node, matcher) {
|
|
8895
|
+
let value;
|
|
8896
|
+
switch (node.type) {
|
|
8897
|
+
case AST_NODE_TYPES34.Literal:
|
|
8898
|
+
value = node.value;
|
|
8899
|
+
break;
|
|
8900
|
+
case AST_NODE_TYPES34.TemplateLiteral:
|
|
8901
|
+
value = node.quasis[0]?.value.cooked;
|
|
8902
|
+
break;
|
|
8903
|
+
case AST_NODE_TYPES34.UnaryExpression:
|
|
8904
|
+
if (node.argument.type !== AST_NODE_TYPES34.Literal || typeof node.argument.value !== "number") return false;
|
|
8905
|
+
value = node.operator === "-" ? -node.argument.value : node.argument.value;
|
|
8906
|
+
break;
|
|
8907
|
+
case AST_NODE_TYPES34.ArrayExpression:
|
|
8908
|
+
case AST_NODE_TYPES34.ObjectExpression:
|
|
8909
|
+
value = {};
|
|
8910
|
+
break;
|
|
8911
|
+
default:
|
|
8912
|
+
return false;
|
|
8913
|
+
}
|
|
8914
|
+
switch (matcher) {
|
|
8915
|
+
case "toBeDefined":
|
|
8916
|
+
return value !== void 0;
|
|
8917
|
+
case "toBeUndefined":
|
|
8918
|
+
return value === void 0;
|
|
8919
|
+
case "toBeNull":
|
|
8920
|
+
return value === null;
|
|
8921
|
+
case "toBeTruthy":
|
|
8922
|
+
return Boolean(value);
|
|
8923
|
+
case "toBeFalsy":
|
|
8924
|
+
return !value;
|
|
8925
|
+
case "toBeNaN":
|
|
8926
|
+
return typeof value === "number" && Number.isNaN(value);
|
|
8927
|
+
default:
|
|
8928
|
+
return false;
|
|
8929
|
+
}
|
|
8930
|
+
}
|
|
8762
8931
|
function expectOperand(callee) {
|
|
8763
8932
|
const receiver = callee.object;
|
|
8764
8933
|
if (receiver.type !== AST_NODE_TYPES34.CallExpression || receiver.callee.type !== AST_NODE_TYPES34.Identifier || receiver.callee.name !== "expect" || receiver.arguments.length !== 1) {
|
|
@@ -8772,12 +8941,12 @@ var no_tautological_expect_default = createRule({
|
|
|
8772
8941
|
meta: {
|
|
8773
8942
|
type: "problem",
|
|
8774
8943
|
docs: {
|
|
8775
|
-
description: "Disallow
|
|
8944
|
+
description: "Disallow supported literal-only assertions that are statically known to pass."
|
|
8776
8945
|
},
|
|
8777
8946
|
schema: [],
|
|
8778
8947
|
messages: {
|
|
8779
|
-
tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` compares
|
|
8780
|
-
tautologicalMatcher: "`expect({{operand}}).{{matcher}}()`
|
|
8948
|
+
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.",
|
|
8949
|
+
tautologicalMatcher: "`expect({{operand}}).{{matcher}}()` is statically known to pass. Assert on a produced value or remove only the redundant assertion, preserving other coverage."
|
|
8781
8950
|
}
|
|
8782
8951
|
},
|
|
8783
8952
|
defaultOptions: [],
|
|
@@ -8799,11 +8968,20 @@ var no_tautological_expect_default = createRule({
|
|
|
8799
8968
|
return;
|
|
8800
8969
|
}
|
|
8801
8970
|
const matcher = callee.property.name;
|
|
8971
|
+
if (callee.object.type !== AST_NODE_TYPES34.CallExpression || callee.object.callee.type !== AST_NODE_TYPES34.Identifier) return;
|
|
8972
|
+
const expectIdentifier = callee.object.callee;
|
|
8973
|
+
const variable = ASTUtils20.findVariable(context.sourceCode.getScope(expectIdentifier), expectIdentifier.name);
|
|
8974
|
+
if (variable !== null && variable.defs.some((definition) => {
|
|
8975
|
+
if (definition.node.type !== AST_NODE_TYPES34.ImportSpecifier) return true;
|
|
8976
|
+
const declaration = definition.node.parent;
|
|
8977
|
+
const imported = definition.node.imported;
|
|
8978
|
+
return declaration.type !== AST_NODE_TYPES34.ImportDeclaration || !["vitest", "@jest/globals", "@playwright/test", "bun:test"].includes(String(declaration.source.value)) || (imported.type === AST_NODE_TYPES34.Identifier ? imported.name : imported.value) !== "expect";
|
|
8979
|
+
})) return;
|
|
8802
8980
|
const operand = expectOperand(callee);
|
|
8803
8981
|
if (operand === null || !isLiteral(operand)) {
|
|
8804
8982
|
return;
|
|
8805
8983
|
}
|
|
8806
|
-
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0) {
|
|
8984
|
+
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0 && passesZeroArgumentMatcher(operand, matcher)) {
|
|
8807
8985
|
context.report({
|
|
8808
8986
|
node,
|
|
8809
8987
|
messageId: "tautologicalMatcher",
|
|
@@ -10037,7 +10215,7 @@ var no_unnecessary_use_client_default = createRule({
|
|
|
10037
10215
|
// src/rules/no-unsafe-mock-casting.ts
|
|
10038
10216
|
import {
|
|
10039
10217
|
AST_NODE_TYPES as AST_NODE_TYPES40,
|
|
10040
|
-
ASTUtils as
|
|
10218
|
+
ASTUtils as ASTUtils21
|
|
10041
10219
|
} from "@typescript-eslint/utils";
|
|
10042
10220
|
var MOCK_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
10043
10221
|
"Mock",
|
|
@@ -10058,15 +10236,16 @@ var MOCK_MODULES = /* @__PURE__ */ new Set([
|
|
|
10058
10236
|
var NO_UNSAFE_MOCK_CASTING_DOCUMENTATION = {
|
|
10059
10237
|
summary: "Disallow casting to mock types like `jest.Mock` or `vi.Mock`. Use `vi.mocked()` or `jest.mocked()` instead.",
|
|
10060
10238
|
rationale: "A type assertion can claim an unmocked value is a mock and bypass checking between the original callable and the mock API.",
|
|
10061
|
-
remediation: "
|
|
10239
|
+
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.",
|
|
10062
10240
|
category: "testing",
|
|
10063
|
-
limitations: ["Only mock types imported from Vitest or Jest modules are inspected."],
|
|
10241
|
+
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."],
|
|
10242
|
+
references: ["https://vitest.dev/api/vi.html#vi-mocked"],
|
|
10064
10243
|
examples: [
|
|
10065
10244
|
{
|
|
10066
10245
|
id: "typed-mock-helper",
|
|
10067
10246
|
title: "Use the framework helper",
|
|
10068
10247
|
outcome: "no-match",
|
|
10069
|
-
files: [{ path: "src/client.test.ts", source: "const m = vi.mocked(
|
|
10248
|
+
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);" }],
|
|
10070
10249
|
focusPath: "src/client.test.ts",
|
|
10071
10250
|
expectedCount: 0,
|
|
10072
10251
|
public: true
|
|
@@ -10092,7 +10271,7 @@ var no_unsafe_mock_casting_default = createRule({
|
|
|
10092
10271
|
},
|
|
10093
10272
|
schema: [],
|
|
10094
10273
|
messages: {
|
|
10095
|
-
unsafeMockCast: "
|
|
10274
|
+
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."
|
|
10096
10275
|
}
|
|
10097
10276
|
},
|
|
10098
10277
|
defaultOptions: [],
|
|
@@ -10103,7 +10282,7 @@ var no_unsafe_mock_casting_default = createRule({
|
|
|
10103
10282
|
const directBindings = /* @__PURE__ */ new Set();
|
|
10104
10283
|
const namespaceBindings = /* @__PURE__ */ new Set();
|
|
10105
10284
|
function resolve2(identifier) {
|
|
10106
|
-
return
|
|
10285
|
+
return ASTUtils21.findVariable(
|
|
10107
10286
|
context.sourceCode.getScope(identifier),
|
|
10108
10287
|
identifier.name
|
|
10109
10288
|
);
|
|
@@ -10153,12 +10332,12 @@ var no_unsafe_mock_casting_default = createRule({
|
|
|
10153
10332
|
import {
|
|
10154
10333
|
ESLintUtils as ESLintUtils3,
|
|
10155
10334
|
AST_NODE_TYPES as AST_NODE_TYPES41,
|
|
10156
|
-
ASTUtils as
|
|
10335
|
+
ASTUtils as ASTUtils22
|
|
10157
10336
|
} from "@typescript-eslint/utils";
|
|
10158
10337
|
import * as ts2 from "typescript";
|
|
10159
10338
|
var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
|
|
10160
10339
|
summary: 'Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum(["a", "b"])` with a string-literal union instead.',
|
|
10161
|
-
rationale: "
|
|
10340
|
+
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.",
|
|
10162
10341
|
remediation: "Pass string literals directly to `z.enum` and derive the TypeScript type with `z.infer`.",
|
|
10163
10342
|
category: "maintainability",
|
|
10164
10343
|
autofix: "none",
|
|
@@ -10263,7 +10442,7 @@ var no_zod_native_enum_default = createRule({
|
|
|
10263
10442
|
const zodImportedBindings = /* @__PURE__ */ new Map();
|
|
10264
10443
|
const zodNamespaceBindings = /* @__PURE__ */ new Set();
|
|
10265
10444
|
function resolvedBinding(identifier) {
|
|
10266
|
-
return
|
|
10445
|
+
return ASTUtils22.findVariable(
|
|
10267
10446
|
sourceCode.getScope(identifier),
|
|
10268
10447
|
identifier.name
|
|
10269
10448
|
);
|
|
@@ -10329,14 +10508,14 @@ var no_zod_native_enum_default = createRule({
|
|
|
10329
10508
|
});
|
|
10330
10509
|
|
|
10331
10510
|
// src/rules/test-loops-over-literal-cases.ts
|
|
10332
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES42, ASTUtils as
|
|
10511
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES42, ASTUtils as ASTUtils23 } from "@typescript-eslint/utils";
|
|
10333
10512
|
var TEST_LOOPS_OVER_LITERAL_CASES_DOCUMENTATION = {
|
|
10334
10513
|
summary: "Disallow assertions over an inline literal case loop in a test; parameterization reports and names every case independently.",
|
|
10335
10514
|
rationale: "A loop is reported as one test, so failures hide the individual case name and may stop later cases from running.",
|
|
10336
10515
|
remediation: "Create one named parameterized test or runner-aware subtest for each literal case.",
|
|
10337
10516
|
category: "testing",
|
|
10338
10517
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**"],
|
|
10339
|
-
limitations: ["Only inline literal for-of cases containing framework assertions are reported."],
|
|
10518
|
+
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."],
|
|
10340
10519
|
examples: [
|
|
10341
10520
|
{ 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 },
|
|
10342
10521
|
{ 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 }
|
|
@@ -10483,7 +10662,7 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10483
10662
|
},
|
|
10484
10663
|
schema: [],
|
|
10485
10664
|
messages: {
|
|
10486
|
-
literalCaseLoop: "This loop asserts over {{count}} inline cases
|
|
10665
|
+
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."
|
|
10487
10666
|
}
|
|
10488
10667
|
},
|
|
10489
10668
|
defaultOptions: [],
|
|
@@ -10492,7 +10671,7 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10492
10671
|
return {};
|
|
10493
10672
|
}
|
|
10494
10673
|
const isFrameworkIdentifier = (identifier, modules) => {
|
|
10495
|
-
const variable =
|
|
10674
|
+
const variable = ASTUtils23.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
10496
10675
|
if (variable === null || variable.defs.length === 0) return true;
|
|
10497
10676
|
return variable.defs.some((definition) => {
|
|
10498
10677
|
let current = definition.node;
|
|
@@ -10508,6 +10687,9 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10508
10687
|
if (enclosing === null || !isTestBody2(enclosing, isFrameworkTest)) {
|
|
10509
10688
|
return;
|
|
10510
10689
|
}
|
|
10690
|
+
for (let current = node; current !== void 0 && current !== enclosing; current = current.parent) {
|
|
10691
|
+
if (current.parent?.type === AST_NODE_TYPES42.BlockStatement && current.parent.body.at(-1) !== current) return;
|
|
10692
|
+
}
|
|
10511
10693
|
const cases = unwrapExpression(node.right);
|
|
10512
10694
|
const callbackParameters = new Set(
|
|
10513
10695
|
enclosing.params.flatMap((parameter) => parameter.type === AST_NODE_TYPES42.Identifier ? [parameter.name] : [])
|
|
@@ -10517,6 +10699,16 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10517
10699
|
) || !walkOwnScope2(node.body, (current) => isAssertion2(current, isFrameworkAssertion)) || walkOwnScope2(node.body, (current) => opensSubtest(current, callbackParameters)) || walkOwnScope2(node.body, (current) => LOOP_CARRIED_CONTROL.has(current.type))) {
|
|
10518
10700
|
return;
|
|
10519
10701
|
}
|
|
10702
|
+
const capturesSetup = walkOwnScope2(node.body, (current) => {
|
|
10703
|
+
if (current.type !== AST_NODE_TYPES42.Identifier) return false;
|
|
10704
|
+
const variable = ASTUtils23.findVariable(context.sourceCode.getScope(current), current.name);
|
|
10705
|
+
if (variable === null || !variable.references.some((reference) => reference.identifier === current)) return false;
|
|
10706
|
+
return variable.defs.some((definition) => {
|
|
10707
|
+
const declaration = definition.name;
|
|
10708
|
+
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]);
|
|
10709
|
+
});
|
|
10710
|
+
});
|
|
10711
|
+
if (capturesSetup) return;
|
|
10520
10712
|
context.report({
|
|
10521
10713
|
node,
|
|
10522
10714
|
messageId: "literalCaseLoop",
|
|
@@ -10740,13 +10932,12 @@ import {
|
|
|
10740
10932
|
var PREFER_ECMASCRIPT_PRIVATE_MEMBERS_DOCUMENTATION = {
|
|
10741
10933
|
summary: "Prefer ECMAScript `#private` class members over TypeScript-only `private` members.",
|
|
10742
10934
|
rationale: "ECMAScript private names enforce encapsulation at runtime instead of erasing the boundary during compilation.",
|
|
10743
|
-
remediation: "
|
|
10935
|
+
remediation: "Review reflection, instance escape and framework contracts before replacing TypeScript privacy with ECMAScript private names and updating references.",
|
|
10744
10936
|
category: "maintainability",
|
|
10745
|
-
autofix: "
|
|
10937
|
+
autofix: "none",
|
|
10746
10938
|
limitations: [
|
|
10747
10939
|
"Ambient, abstract, computed, decorated, override, parameter-property, and generated declarations are excluded.",
|
|
10748
|
-
"
|
|
10749
|
-
"Overloads, modifier-adjacent comments, reflection, and any potentially cross-file or escaping class remain report-only."
|
|
10940
|
+
"Migration is report-only: type information cannot prove that instances or constructors never escape through this, or that reflection and framework serialization do not observe ordinary private properties."
|
|
10750
10941
|
],
|
|
10751
10942
|
references: ["https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements"],
|
|
10752
10943
|
examples: [
|
|
@@ -10766,19 +10957,11 @@ var PREFER_ECMASCRIPT_PRIVATE_MEMBERS_DOCUMENTATION = {
|
|
|
10766
10957
|
files: [{ path: "src/vault.ts", source: "class Vault { private read() { return 1; } open() { return this.read(); } }" }],
|
|
10767
10958
|
focusPath: "src/vault.ts",
|
|
10768
10959
|
expectedCount: 1,
|
|
10769
|
-
public: true
|
|
10770
|
-
fixedFiles: [{ path: "src/vault.ts", source: "class Vault { #read() { return 1; } open() { return this.#read(); } }" }]
|
|
10960
|
+
public: true
|
|
10771
10961
|
}
|
|
10772
10962
|
]
|
|
10773
10963
|
};
|
|
10774
|
-
function reportClass2(context,
|
|
10775
|
-
const parent = owner.parent;
|
|
10776
|
-
const directlyExported = parent.type === AST_NODE_TYPES45.ExportNamedDeclaration || parent.type === AST_NODE_TYPES45.ExportDefaultDeclaration;
|
|
10777
|
-
const locallyClosed = !directlyExported && owner.decorators.length === 0 && owner.type === AST_NODE_TYPES45.ClassDeclaration && context.sourceCode.getDeclaredVariables(owner).every(
|
|
10778
|
-
(variable) => variable.references.every(
|
|
10779
|
-
(reference) => reference.identifier.range[0] >= owner.range[0] && reference.identifier.range[1] <= owner.range[1]
|
|
10780
|
-
)
|
|
10781
|
-
);
|
|
10964
|
+
function reportClass2(context, owner) {
|
|
10782
10965
|
const groups = /* @__PURE__ */ new Map();
|
|
10783
10966
|
for (const member of owner.body.body) {
|
|
10784
10967
|
if (!isConvertible(member)) continue;
|
|
@@ -10791,18 +10974,10 @@ function reportClass2(context, services, owner) {
|
|
|
10791
10974
|
for (const [name, members] of groups) {
|
|
10792
10975
|
const first = members[0];
|
|
10793
10976
|
if (first === void 0) continue;
|
|
10794
|
-
const fix = locallyClosed ? privateMemberFixes(
|
|
10795
|
-
context,
|
|
10796
|
-
services,
|
|
10797
|
-
owner,
|
|
10798
|
-
members,
|
|
10799
|
-
true
|
|
10800
|
-
) : void 0;
|
|
10801
10977
|
context.report({
|
|
10802
10978
|
node: first.key,
|
|
10803
10979
|
messageId: "preferEcmascriptPrivate",
|
|
10804
|
-
data: { name }
|
|
10805
|
-
...fix === void 0 ? {} : { fix }
|
|
10980
|
+
data: { name }
|
|
10806
10981
|
});
|
|
10807
10982
|
}
|
|
10808
10983
|
}
|
|
@@ -10816,7 +10991,6 @@ var prefer_ecmascript_private_members_default = createRule({
|
|
|
10816
10991
|
meta: {
|
|
10817
10992
|
type: "suggestion",
|
|
10818
10993
|
docs: { description: "Prefer ECMAScript `#private` class members over TypeScript-only `private` members." },
|
|
10819
|
-
fixable: "code",
|
|
10820
10994
|
schema: [],
|
|
10821
10995
|
messages: {
|
|
10822
10996
|
preferEcmascriptPrivate: "TypeScript `private {{name}}` is erased at runtime; use the ECMAScript private name `#{{name}}`."
|
|
@@ -10833,8 +11007,8 @@ var prefer_ecmascript_private_members_default = createRule({
|
|
|
10833
11007
|
}
|
|
10834
11008
|
if (services === null) return {};
|
|
10835
11009
|
return {
|
|
10836
|
-
ClassDeclaration: (node) => reportClass2(context,
|
|
10837
|
-
ClassExpression: (node) => reportClass2(context,
|
|
11010
|
+
ClassDeclaration: (node) => reportClass2(context, node),
|
|
11011
|
+
ClassExpression: (node) => reportClass2(context, node)
|
|
10838
11012
|
};
|
|
10839
11013
|
}
|
|
10840
11014
|
});
|
|
@@ -10844,10 +11018,10 @@ import "@typescript-eslint/utils";
|
|
|
10844
11018
|
import { AST_NODE_TYPES as AST_NODE_TYPES46 } from "@typescript-eslint/utils";
|
|
10845
11019
|
var PREFER_DISCRIMINATED_UNION_DOCUMENTATION = {
|
|
10846
11020
|
summary: "Flag flat result objects with a required positive boolean status and optional success/failure payloads.",
|
|
10847
|
-
rationale: "
|
|
10848
|
-
remediation: "
|
|
11021
|
+
rationale: "When success and failure are mutually exclusive outcomes, a boolean plus optional branch data permits contradictory and incomplete states.",
|
|
11022
|
+
remediation: "If the outcomes are mutually exclusive, represent each branch as a discriminated union member with its required payload.",
|
|
10849
11023
|
category: "correctness",
|
|
10850
|
-
limitations: ["Only local object shapes with recognized
|
|
11024
|
+
limitations: ["Only local object shapes with recognized non-computed status and payload names are inspected. Names do not prove that partial-success outcomes are forbidden; review the domain before changing its representation."],
|
|
10851
11025
|
examples: [
|
|
10852
11026
|
{ id: "explicit-result-branches", title: "Use explicit result branches", outcome: "no-match", files: [{ path: "src/result.ts", source: "type Result = { ok: true; data: string } | { ok: false; error: string };" }], focusPath: "src/result.ts", expectedCount: 0, public: true },
|
|
10853
11027
|
{ id: "optional-result-payloads", title: "Do not make both result payloads optional", outcome: "match", files: [{ path: "src/result.ts", source: "type Result = { ok: boolean; data?: string; error?: string };" }], focusPath: "src/result.ts", expectedCount: 1, public: true }
|
|
@@ -10911,7 +11085,7 @@ function looksLikeMutuallyExclusiveState(typeLiteral) {
|
|
|
10911
11085
|
return statusMemberCount === REQUIRED_STATUS_MEMBER_COUNT && hasFailurePayload && (hasSuccessPayload || !hasUnrecognizedMember);
|
|
10912
11086
|
}
|
|
10913
11087
|
function getMemberName(member) {
|
|
10914
|
-
if (member.type !== AST_NODE_TYPES46.TSPropertySignature) {
|
|
11088
|
+
if (member.type !== AST_NODE_TYPES46.TSPropertySignature || member.computed) {
|
|
10915
11089
|
return null;
|
|
10916
11090
|
}
|
|
10917
11091
|
const { key } = member;
|
|
@@ -10947,7 +11121,7 @@ var prefer_discriminated_union_default = createRule({
|
|
|
10947
11121
|
},
|
|
10948
11122
|
schema: [],
|
|
10949
11123
|
messages: {
|
|
10950
|
-
preferDiscriminatedUnion: "This object type
|
|
11124
|
+
preferDiscriminatedUnion: "This object type combines a boolean status with optional payloads. If success and failure are mutually exclusive, consider a discriminated union such as `{ ok: true; data: T } | { ok: false; error: E }`."
|
|
10951
11125
|
}
|
|
10952
11126
|
},
|
|
10953
11127
|
defaultOptions: [],
|
|
@@ -10987,7 +11161,7 @@ var prefer_discriminated_union_default = createRule({
|
|
|
10987
11161
|
});
|
|
10988
11162
|
|
|
10989
11163
|
// src/rules/prefer-input-group-search.ts
|
|
10990
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES47, ASTUtils as
|
|
11164
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES47, ASTUtils as ASTUtils24 } from "@typescript-eslint/utils";
|
|
10991
11165
|
var PREFER_INPUT_GROUP_SEARCH_DOCUMENTATION = {
|
|
10992
11166
|
summary: "Require search icons and shared Input controls in the same visual wrapper to use InputGroup.",
|
|
10993
11167
|
rationale: "The shared compound control provides consistent spacing, focus behavior, and accessible composition.",
|
|
@@ -11111,7 +11285,7 @@ var prefer_input_group_search_default = createRule({
|
|
|
11111
11285
|
JSXOpeningElement(node) {
|
|
11112
11286
|
const name = elementName(node);
|
|
11113
11287
|
if (name === null) return;
|
|
11114
|
-
const binding =
|
|
11288
|
+
const binding = ASTUtils24.findVariable(context.sourceCode.getScope(node), name);
|
|
11115
11289
|
if (binding?.defs.length !== 1 || binding.defs[0]?.node.type !== AST_NODE_TYPES47.ImportSpecifier || binding.defs[0].node.importKind === "type") return;
|
|
11116
11290
|
const occurrence = {
|
|
11117
11291
|
ancestors: context.sourceCode.getAncestors(node),
|
|
@@ -11151,7 +11325,7 @@ var prefer_input_group_search_default = createRule({
|
|
|
11151
11325
|
|
|
11152
11326
|
// src/rules/prefer-millisecond-control-duration-schema.ts
|
|
11153
11327
|
import {
|
|
11154
|
-
ASTUtils as
|
|
11328
|
+
ASTUtils as ASTUtils25,
|
|
11155
11329
|
AST_NODE_TYPES as AST_NODE_TYPES48
|
|
11156
11330
|
} from "@typescript-eslint/utils";
|
|
11157
11331
|
var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
@@ -11161,7 +11335,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
|
11161
11335
|
category: "correctness",
|
|
11162
11336
|
autofix: "none",
|
|
11163
11337
|
limitations: [
|
|
11164
|
-
"Only direct identifier keys in application-owned z.object/z.strictObject schemas are checked.",
|
|
11338
|
+
"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.",
|
|
11165
11339
|
"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.",
|
|
11166
11340
|
"Quoted/computed protocol keys, generated/vendor code, tests, fixtures, and non-Zod schemas are excluded."
|
|
11167
11341
|
],
|
|
@@ -11173,7 +11347,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
|
11173
11347
|
files: [
|
|
11174
11348
|
{
|
|
11175
11349
|
path: "src/request.ts",
|
|
11176
|
-
source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeoutMs: z.number().int().min(
|
|
11350
|
+
source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeoutMs: z.number().int().min(1000).max(300000).default(30000) });"
|
|
11177
11351
|
}
|
|
11178
11352
|
],
|
|
11179
11353
|
focusPath: "src/request.ts",
|
|
@@ -11187,7 +11361,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
|
11187
11361
|
files: [
|
|
11188
11362
|
{
|
|
11189
11363
|
path: "src/request.ts",
|
|
11190
|
-
source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeout_seconds: z.number().int().min(1) });"
|
|
11364
|
+
source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeout_seconds: z.number().int().min(1).max(300).default(30) });"
|
|
11191
11365
|
}
|
|
11192
11366
|
],
|
|
11193
11367
|
focusPath: "src/request.ts",
|
|
@@ -11219,8 +11393,9 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11219
11393
|
}
|
|
11220
11394
|
const zodNamespaces = /* @__PURE__ */ new Set();
|
|
11221
11395
|
const objectFactories = /* @__PURE__ */ new Set();
|
|
11396
|
+
const numberFactories = /* @__PURE__ */ new Set();
|
|
11222
11397
|
function binding(identifier) {
|
|
11223
|
-
return
|
|
11398
|
+
return ASTUtils25.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
11224
11399
|
}
|
|
11225
11400
|
function record(target, identifier) {
|
|
11226
11401
|
const variable = binding(identifier);
|
|
@@ -11238,10 +11413,27 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11238
11413
|
const variable = binding(callee.object);
|
|
11239
11414
|
return variable !== null && zodNamespaces.has(variable);
|
|
11240
11415
|
}
|
|
11416
|
+
function isNumericSchema(node) {
|
|
11417
|
+
if (node.type !== AST_NODE_TYPES48.CallExpression) return false;
|
|
11418
|
+
const callee = node.callee;
|
|
11419
|
+
if (callee.type === AST_NODE_TYPES48.Identifier) {
|
|
11420
|
+
const variable = binding(callee);
|
|
11421
|
+
return variable !== null && numberFactories.has(variable);
|
|
11422
|
+
}
|
|
11423
|
+
if (callee.type !== AST_NODE_TYPES48.MemberExpression || callee.computed || callee.property.type !== AST_NODE_TYPES48.Identifier) return false;
|
|
11424
|
+
if (callee.object.type === AST_NODE_TYPES48.Identifier) {
|
|
11425
|
+
const variable = binding(callee.object);
|
|
11426
|
+
return callee.property.name === "number" && variable !== null && zodNamespaces.has(variable);
|
|
11427
|
+
}
|
|
11428
|
+
return ["int", "min", "max", "positive", "nonnegative", "finite", "multipleOf", "optional", "nullable", "nullish", "default", "describe", "brand", "readonly"].includes(callee.property.name) && isNumericSchema(callee.object);
|
|
11429
|
+
}
|
|
11241
11430
|
return {
|
|
11242
11431
|
ImportDeclaration(node) {
|
|
11243
11432
|
if (!isZodModule(node.source.value)) return;
|
|
11244
11433
|
for (const specifier of node.specifiers) {
|
|
11434
|
+
if (specifier.type === AST_NODE_TYPES48.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES48.Identifier && specifier.imported.name === "number") {
|
|
11435
|
+
record(numberFactories, specifier.local);
|
|
11436
|
+
}
|
|
11245
11437
|
if (specifier.type === AST_NODE_TYPES48.ImportNamespaceSpecifier || specifier.type === AST_NODE_TYPES48.ImportDefaultSpecifier || specifier.type === AST_NODE_TYPES48.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES48.Identifier && specifier.imported.name === "z") {
|
|
11246
11438
|
record(zodNamespaces, specifier.local);
|
|
11247
11439
|
} else if (specifier.type === AST_NODE_TYPES48.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES48.Identifier && (specifier.imported.name === "object" || specifier.imported.name === "strictObject")) {
|
|
@@ -11254,7 +11446,7 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11254
11446
|
const shape = node.arguments[0];
|
|
11255
11447
|
if (shape?.type !== AST_NODE_TYPES48.ObjectExpression) return;
|
|
11256
11448
|
for (const member of shape.properties) {
|
|
11257
|
-
if (member.type !== AST_NODE_TYPES48.Property) continue;
|
|
11449
|
+
if (member.type !== AST_NODE_TYPES48.Property || !isNumericSchema(member.value)) continue;
|
|
11258
11450
|
const key = directIdentifierKey(member);
|
|
11259
11451
|
if (key === null || !CONTROL_SECONDS_RE.test(key.name) && !CONTROL_SECONDS_CAMEL_RE.test(key.name)) {
|
|
11260
11452
|
continue;
|
|
@@ -11267,14 +11459,14 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11267
11459
|
});
|
|
11268
11460
|
|
|
11269
11461
|
// src/rules/prefer-immutable-module-constant.ts
|
|
11270
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES49, ASTUtils as
|
|
11462
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES49, ASTUtils as ASTUtils26 } from "@typescript-eslint/utils";
|
|
11271
11463
|
var PREFER_IMMUTABLE_MODULE_CONSTANT_DOCUMENTATION = {
|
|
11272
11464
|
summary: "Require module-level constant collections to expose readonly state.",
|
|
11273
11465
|
rationale: "A const binding prevents reassignment but does not stop callers from mutating its array, object, Set, or Map contents.",
|
|
11274
11466
|
remediation: "Expose literals with `as const` or a readonly type, and expose Set or Map values through ReadonlySet or ReadonlyMap.",
|
|
11275
11467
|
category: "correctness",
|
|
11276
11468
|
limitations: [
|
|
11277
|
-
"
|
|
11469
|
+
"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."
|
|
11278
11470
|
],
|
|
11279
11471
|
examples: [
|
|
11280
11472
|
{
|
|
@@ -11420,7 +11612,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11420
11612
|
create(context) {
|
|
11421
11613
|
const sourceCode = context.sourceCode;
|
|
11422
11614
|
const isUnshadowedGlobal3 = (identifier) => {
|
|
11423
|
-
const variable =
|
|
11615
|
+
const variable = ASTUtils26.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
11424
11616
|
return variable === null || variable.defs.length === 0;
|
|
11425
11617
|
};
|
|
11426
11618
|
if (JAVASCRIPT_FILE_RE.test(context.filename) || isTestFile(context.filename) || isGeneratedFile(context.filename, sourceCode.getText())) {
|
|
@@ -11428,7 +11620,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11428
11620
|
}
|
|
11429
11621
|
const exportedNames2 = /* @__PURE__ */ new Set();
|
|
11430
11622
|
const typeAliases2 = /* @__PURE__ */ new Map();
|
|
11431
|
-
const
|
|
11623
|
+
const mutatesThroughAlias = (root) => {
|
|
11432
11624
|
const pending = [root];
|
|
11433
11625
|
const seen = /* @__PURE__ */ new Set();
|
|
11434
11626
|
while (pending.length > 0) {
|
|
@@ -11440,7 +11632,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11440
11632
|
if (identifier.type !== AST_NODE_TYPES49.Identifier) continue;
|
|
11441
11633
|
if (referenceMutates(identifier, isUnshadowedGlobal3)) return true;
|
|
11442
11634
|
const declarator = identifier.parent;
|
|
11443
|
-
if (declarator.type !== AST_NODE_TYPES49.VariableDeclarator || declarator.init !== identifier || declarator.id.type !== AST_NODE_TYPES49.Identifier || declarator.parent.type !== AST_NODE_TYPES49.VariableDeclaration
|
|
11635
|
+
if (declarator.type !== AST_NODE_TYPES49.VariableDeclarator || declarator.init !== identifier || declarator.id.type !== AST_NODE_TYPES49.Identifier || declarator.parent.type !== AST_NODE_TYPES49.VariableDeclaration) {
|
|
11444
11636
|
continue;
|
|
11445
11637
|
}
|
|
11446
11638
|
const alias = sourceCode.getDeclaredVariables(declarator)[0];
|
|
@@ -11489,7 +11681,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11489
11681
|
return;
|
|
11490
11682
|
}
|
|
11491
11683
|
const variable = sourceCode.getDeclaredVariables(node)[0];
|
|
11492
|
-
if (!directlyExported && !exportedNames2.has(node.id.name) && variable !== void 0 &&
|
|
11684
|
+
if (!directlyExported && !exportedNames2.has(node.id.name) && variable !== void 0 && mutatesThroughAlias(variable)) {
|
|
11493
11685
|
return;
|
|
11494
11686
|
}
|
|
11495
11687
|
context.report({
|
|
@@ -11916,7 +12108,7 @@ var PREFER_MODULE_LEVEL_CONSTANT_DOCUMENTATION = {
|
|
|
11916
12108
|
rationale: "Recreating immutable lookup data on every call wastes allocations and obscures its constant nature.",
|
|
11917
12109
|
remediation: "Declare immutable literal collections and non-stateful regular expressions once at module scope.",
|
|
11918
12110
|
category: "performance",
|
|
11919
|
-
limitations: ["
|
|
12111
|
+
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."],
|
|
11920
12112
|
examples: [
|
|
11921
12113
|
{ 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 },
|
|
11922
12114
|
{ 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 }
|
|
@@ -12071,12 +12263,16 @@ var NON_RETAINING_BUILTINS = /* @__PURE__ */ new Map(
|
|
|
12071
12263
|
]
|
|
12072
12264
|
);
|
|
12073
12265
|
function isSafeRead(identifier) {
|
|
12074
|
-
|
|
12266
|
+
let parent = identifier.parent;
|
|
12075
12267
|
if (parent.type === AST_NODE_TYPES51.MemberExpression) {
|
|
12076
12268
|
if (parent.object !== identifier) {
|
|
12077
12269
|
return true;
|
|
12078
12270
|
}
|
|
12271
|
+
while (parent.parent.type === AST_NODE_TYPES51.MemberExpression && parent.parent.object === parent) {
|
|
12272
|
+
parent = parent.parent;
|
|
12273
|
+
}
|
|
12079
12274
|
const grandparent = parent.parent;
|
|
12275
|
+
if (grandparent.type === AST_NODE_TYPES51.VariableDeclarator || grandparent.type === AST_NODE_TYPES51.SpreadElement) return false;
|
|
12080
12276
|
if (grandparent.type === AST_NODE_TYPES51.AssignmentExpression && grandparent.left === parent) {
|
|
12081
12277
|
return false;
|
|
12082
12278
|
}
|
|
@@ -12086,7 +12282,7 @@ function isSafeRead(identifier) {
|
|
|
12086
12282
|
if (grandparent.type === AST_NODE_TYPES51.UnaryExpression && grandparent.operator === "delete") {
|
|
12087
12283
|
return false;
|
|
12088
12284
|
}
|
|
12089
|
-
if (
|
|
12285
|
+
if (grandparent.type === AST_NODE_TYPES51.CallExpression && grandparent.callee === parent && (parent.computed ? parent.property.type !== AST_NODE_TYPES51.Literal || typeof parent.property.value !== "string" || MUTATING_METHODS2.has(parent.property.value) : parent.property.type === AST_NODE_TYPES51.Identifier && MUTATING_METHODS2.has(parent.property.name))) {
|
|
12090
12286
|
return false;
|
|
12091
12287
|
}
|
|
12092
12288
|
return true;
|
|
@@ -12194,9 +12390,15 @@ var prefer_module_level_constant_default = createRule({
|
|
|
12194
12390
|
if (node.id.type !== AST_NODE_TYPES51.Identifier || node.init === null) {
|
|
12195
12391
|
return;
|
|
12196
12392
|
}
|
|
12197
|
-
|
|
12393
|
+
const owner = enclosingFunction3(node);
|
|
12394
|
+
if (owner === null) {
|
|
12198
12395
|
return;
|
|
12199
12396
|
}
|
|
12397
|
+
let expression = owner;
|
|
12398
|
+
while (expression.parent !== void 0 && unwrap4(expression.parent) === expression) {
|
|
12399
|
+
expression = expression.parent;
|
|
12400
|
+
}
|
|
12401
|
+
if (expression.parent?.type === AST_NODE_TYPES51.CallExpression && expression.parent.callee === expression) return;
|
|
12200
12402
|
const candidate2 = classify(node.init, checkRegex);
|
|
12201
12403
|
if (candidate2 === null) {
|
|
12202
12404
|
return;
|
|
@@ -12221,10 +12423,10 @@ var prefer_module_level_constant_default = createRule({
|
|
|
12221
12423
|
import { AST_NODE_TYPES as AST_NODE_TYPES52 } from "@typescript-eslint/utils";
|
|
12222
12424
|
var PREFER_MODULE_LEVEL_SCHEMA_DOCUMENTATION = {
|
|
12223
12425
|
summary: "Declare a Zod schema at module scope when it closes over nothing in the enclosing function",
|
|
12224
|
-
rationale: "A
|
|
12426
|
+
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.",
|
|
12225
12427
|
remediation: "Move the closed schema declaration to module scope and reference it from the function.",
|
|
12226
12428
|
category: "performance",
|
|
12227
|
-
limitations: ["Schemas that depend on local state or are wrapped in a recognized memoization helper are excluded."],
|
|
12429
|
+
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."],
|
|
12228
12430
|
examples: [
|
|
12229
12431
|
{ 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 },
|
|
12230
12432
|
{ 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 }
|
|
@@ -12241,6 +12443,36 @@ var DEFAULT_FACTORIES = [
|
|
|
12241
12443
|
"union"
|
|
12242
12444
|
];
|
|
12243
12445
|
var DEFAULT_MIN_PROPERTIES = 2;
|
|
12446
|
+
var CONSTRUCTION_FACTORIES = /* @__PURE__ */ new Set([
|
|
12447
|
+
...DEFAULT_FACTORIES,
|
|
12448
|
+
"any",
|
|
12449
|
+
"array",
|
|
12450
|
+
"bigint",
|
|
12451
|
+
"boolean",
|
|
12452
|
+
"custom",
|
|
12453
|
+
"date",
|
|
12454
|
+
"enum",
|
|
12455
|
+
"instanceof",
|
|
12456
|
+
"lazy",
|
|
12457
|
+
"literal",
|
|
12458
|
+
"map",
|
|
12459
|
+
"nan",
|
|
12460
|
+
"nativeEnum",
|
|
12461
|
+
"never",
|
|
12462
|
+
"null",
|
|
12463
|
+
"nullable",
|
|
12464
|
+
"nullish",
|
|
12465
|
+
"number",
|
|
12466
|
+
"optional",
|
|
12467
|
+
"preprocess",
|
|
12468
|
+
"promise",
|
|
12469
|
+
"set",
|
|
12470
|
+
"string",
|
|
12471
|
+
"symbol",
|
|
12472
|
+
"undefined",
|
|
12473
|
+
"unknown",
|
|
12474
|
+
"void"
|
|
12475
|
+
]);
|
|
12244
12476
|
var MEMO_CALLEES = /* @__PURE__ */ new Set([
|
|
12245
12477
|
"lazy",
|
|
12246
12478
|
"memo",
|
|
@@ -12317,7 +12549,7 @@ function outermostEnclosingFunction(node) {
|
|
|
12317
12549
|
}
|
|
12318
12550
|
return outermost;
|
|
12319
12551
|
}
|
|
12320
|
-
function subtreeSome(root, predicate) {
|
|
12552
|
+
function subtreeSome(root, predicate, skipDeferredFunctions = false) {
|
|
12321
12553
|
let found = false;
|
|
12322
12554
|
const visit = (value) => {
|
|
12323
12555
|
if (found || value === null || typeof value !== "object") {
|
|
@@ -12333,6 +12565,7 @@ function subtreeSome(root, predicate) {
|
|
|
12333
12565
|
if (typeof candidate2.type !== "string") {
|
|
12334
12566
|
return;
|
|
12335
12567
|
}
|
|
12568
|
+
if (skipDeferredFunctions && FUNCTION_TYPES8.has(candidate2.type)) return;
|
|
12336
12569
|
if (predicate(candidate2)) {
|
|
12337
12570
|
found = true;
|
|
12338
12571
|
return;
|
|
@@ -12431,6 +12664,15 @@ var prefer_module_level_schema_default = createRule({
|
|
|
12431
12664
|
function isZodCall(node) {
|
|
12432
12665
|
return node.type === AST_NODE_TYPES52.CallExpression && node.callee.type === AST_NODE_TYPES52.MemberExpression && !node.callee.computed && node.callee.object.type === AST_NODE_TYPES52.Identifier && zodNamespaces.has(node.callee.object.name);
|
|
12433
12666
|
}
|
|
12667
|
+
function isSchemaConstruction(node) {
|
|
12668
|
+
const callee = node.callee;
|
|
12669
|
+
if (callee.type !== AST_NODE_TYPES52.MemberExpression || callee.computed || callee.property.type !== AST_NODE_TYPES52.Identifier || TERMINAL_METHODS.has(callee.property.name)) return false;
|
|
12670
|
+
if (callee.object.type === AST_NODE_TYPES52.CallExpression) return isSchemaConstruction(callee.object);
|
|
12671
|
+
return isZodCall(node) && CONSTRUCTION_FACTORIES.has(callee.property.name);
|
|
12672
|
+
}
|
|
12673
|
+
function hasEagerComputation(node) {
|
|
12674
|
+
return subtreeSome(node, (inner) => inner.type === AST_NODE_TYPES52.NewExpression || inner.type === AST_NODE_TYPES52.TaggedTemplateExpression || inner.type === AST_NODE_TYPES52.CallExpression && !isSchemaConstruction(inner), true);
|
|
12675
|
+
}
|
|
12434
12676
|
function isCovered(node) {
|
|
12435
12677
|
let current = node.parent ?? void 0;
|
|
12436
12678
|
while (current !== void 0) {
|
|
@@ -12560,6 +12802,7 @@ var prefer_module_level_schema_default = createRule({
|
|
|
12560
12802
|
return;
|
|
12561
12803
|
}
|
|
12562
12804
|
const outermost = outermostSchemaExpression(expression);
|
|
12805
|
+
if (hasEagerComputation(outermost)) return;
|
|
12563
12806
|
if (outermost !== expression && (readsReceiver(outermost) || buildsLocalizedText(outermost) || !closesOverNothing(outermost, enclosing))) {
|
|
12564
12807
|
return;
|
|
12565
12808
|
}
|
|
@@ -12579,7 +12822,7 @@ var prefer_module_level_schema_default = createRule({
|
|
|
12579
12822
|
// src/rules/prefer-module-level-refined-schema.ts
|
|
12580
12823
|
import {
|
|
12581
12824
|
AST_NODE_TYPES as AST_NODE_TYPES53,
|
|
12582
|
-
ASTUtils as
|
|
12825
|
+
ASTUtils as ASTUtils27
|
|
12583
12826
|
} from "@typescript-eslint/utils";
|
|
12584
12827
|
var BENCHMARK_PATH_RE = /(^|[/\\])(?:benchmarks?|bench)[/\\]/;
|
|
12585
12828
|
var FACTORIES = /* @__PURE__ */ new Set([
|
|
@@ -12700,7 +12943,8 @@ var PREFER_MODULE_LEVEL_REFINED_SCHEMA_DOCUMENTATION = {
|
|
|
12700
12943
|
limitations: [
|
|
12701
12944
|
"Composite object/record/tuple/union schemas are owned by prefer-module-level-schema.",
|
|
12702
12945
|
"Schemas that depend on function-local or mutable state, localized text, receiver state, lazy construction, or recognized memoization are excluded.",
|
|
12703
|
-
"Literal string z.enum domains are owned by prefer-shared-zod-enum."
|
|
12946
|
+
"Literal string z.enum domains are owned by prefer-shared-zod-enum.",
|
|
12947
|
+
"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."
|
|
12704
12948
|
],
|
|
12705
12949
|
examples: [
|
|
12706
12950
|
{
|
|
@@ -12742,7 +12986,7 @@ function collectReferences2(scope, output) {
|
|
|
12742
12986
|
output.push(...scope.references);
|
|
12743
12987
|
for (const child of scope.childScopes) collectReferences2(child, output);
|
|
12744
12988
|
}
|
|
12745
|
-
function subtreeSome2(root, predicate) {
|
|
12989
|
+
function subtreeSome2(root, predicate, skipDeferredFunctions = false) {
|
|
12746
12990
|
let found = false;
|
|
12747
12991
|
const visit = (value) => {
|
|
12748
12992
|
if (found || value === null || typeof value !== "object") return;
|
|
@@ -12752,6 +12996,7 @@ function subtreeSome2(root, predicate) {
|
|
|
12752
12996
|
}
|
|
12753
12997
|
const candidate2 = value;
|
|
12754
12998
|
if (typeof candidate2.type !== "string") return;
|
|
12999
|
+
if (skipDeferredFunctions && FUNCTION_TYPES9.has(candidate2.type)) return;
|
|
12755
13000
|
if (predicate(candidate2)) {
|
|
12756
13001
|
found = true;
|
|
12757
13002
|
return;
|
|
@@ -12850,7 +13095,7 @@ var prefer_module_level_refined_schema_default = createRule({
|
|
|
12850
13095
|
return {};
|
|
12851
13096
|
const zodBindings = /* @__PURE__ */ new Set();
|
|
12852
13097
|
function resolvedBinding(identifier) {
|
|
12853
|
-
return
|
|
13098
|
+
return ASTUtils27.findVariable(
|
|
12854
13099
|
context.sourceCode.getScope(identifier),
|
|
12855
13100
|
identifier.name
|
|
12856
13101
|
);
|
|
@@ -12872,6 +13117,15 @@ var prefer_module_level_refined_schema_default = createRule({
|
|
|
12872
13117
|
return names[1] ?? null;
|
|
12873
13118
|
return null;
|
|
12874
13119
|
}
|
|
13120
|
+
function isSchemaConstruction(node) {
|
|
13121
|
+
const callee = node.callee;
|
|
13122
|
+
if (callee.type !== AST_NODE_TYPES53.MemberExpression || callee.computed || callee.property.type !== AST_NODE_TYPES53.Identifier || NON_SCHEMA_TERMINALS.has(callee.property.name)) return false;
|
|
13123
|
+
if (callee.object.type === AST_NODE_TYPES53.CallExpression) return isSchemaConstruction(callee.object);
|
|
13124
|
+
return factoryName(node, FACTORIES) !== null || factoryName(node, COMPOSITE_FACTORIES) !== null;
|
|
13125
|
+
}
|
|
13126
|
+
function hasEagerComputation(node) {
|
|
13127
|
+
return subtreeSome2(node, (inner) => inner.type === AST_NODE_TYPES53.NewExpression || inner.type === AST_NODE_TYPES53.TaggedTemplateExpression || inner.type === AST_NODE_TYPES53.CallExpression && !isSchemaConstruction(inner), true);
|
|
13128
|
+
}
|
|
12875
13129
|
function isSharedEnumDomain(node, factory) {
|
|
12876
13130
|
if (factory !== "enum") return false;
|
|
12877
13131
|
const [argument] = node.arguments;
|
|
@@ -12940,7 +13194,7 @@ var prefer_module_level_refined_schema_default = createRule({
|
|
|
12940
13194
|
const enclosing = outermostEnclosingFunction2(node);
|
|
12941
13195
|
if (enclosing === void 0) return;
|
|
12942
13196
|
const expression = schemaExpression2(node);
|
|
12943
|
-
if (readsReceiver2(expression) || buildsLocalizedText2(expression) || !closesOverNothing(expression, enclosing))
|
|
13197
|
+
if (hasEagerComputation(expression) || readsReceiver2(expression) || buildsLocalizedText2(expression) || !closesOverNothing(expression, enclosing))
|
|
12944
13198
|
return;
|
|
12945
13199
|
context.report({ node, messageId: "hoistRefinedSchema" });
|
|
12946
13200
|
}
|
|
@@ -12951,7 +13205,7 @@ var prefer_module_level_refined_schema_default = createRule({
|
|
|
12951
13205
|
// src/rules/prefer-multi-value-zod-literal.ts
|
|
12952
13206
|
import {
|
|
12953
13207
|
AST_NODE_TYPES as AST_NODE_TYPES54,
|
|
12954
|
-
ASTUtils as
|
|
13208
|
+
ASTUtils as ASTUtils28
|
|
12955
13209
|
} from "@typescript-eslint/utils";
|
|
12956
13210
|
var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
|
|
12957
13211
|
summary: "Use the Zod 4 multi-value literal API instead of a union of literal schemas.",
|
|
@@ -12971,7 +13225,7 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
|
|
|
12971
13225
|
outcome: "no-match",
|
|
12972
13226
|
files: [{
|
|
12973
13227
|
path: "src/schema.ts",
|
|
12974
|
-
source: "import { z } from 'zod'; export const Version = z.literal([1, 2, 3]);"
|
|
13228
|
+
source: "import { z } from 'zod/v4'; export const Version = z.literal([1, 2, 3]);"
|
|
12975
13229
|
}],
|
|
12976
13230
|
focusPath: "src/schema.ts",
|
|
12977
13231
|
expectedCount: 0,
|
|
@@ -12983,7 +13237,7 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
|
|
|
12983
13237
|
outcome: "match",
|
|
12984
13238
|
files: [{
|
|
12985
13239
|
path: "src/schema.ts",
|
|
12986
|
-
source: "import { z } from 'zod'; export const Version = z.union([z.literal(1), z.literal(2), z.literal(3)]);"
|
|
13240
|
+
source: "import { z } from 'zod/v4'; export const Version = z.union([z.literal(1), z.literal(2), z.literal(3)]);"
|
|
12987
13241
|
}],
|
|
12988
13242
|
focusPath: "src/schema.ts",
|
|
12989
13243
|
expectedCount: 1,
|
|
@@ -12998,7 +13252,7 @@ function isStaticPrimitive(node, context) {
|
|
|
12998
13252
|
if (node.type === AST_NODE_TYPES54.TemplateLiteral && node.expressions.length === 0)
|
|
12999
13253
|
return true;
|
|
13000
13254
|
if (node.type === AST_NODE_TYPES54.Identifier && node.name === "undefined") {
|
|
13001
|
-
const binding =
|
|
13255
|
+
const binding = ASTUtils28.findVariable(
|
|
13002
13256
|
context.sourceCode.getScope(node),
|
|
13003
13257
|
node.name
|
|
13004
13258
|
);
|
|
@@ -13035,7 +13289,7 @@ var prefer_multi_value_zod_literal_default = createRule({
|
|
|
13035
13289
|
const zodBindings = /* @__PURE__ */ new Set();
|
|
13036
13290
|
const zod4Bindings = /* @__PURE__ */ new Set();
|
|
13037
13291
|
function resolvedBinding(identifier) {
|
|
13038
|
-
return
|
|
13292
|
+
return ASTUtils28.findVariable(
|
|
13039
13293
|
context.sourceCode.getScope(identifier),
|
|
13040
13294
|
identifier.name
|
|
13041
13295
|
);
|
|
@@ -13106,7 +13360,7 @@ function isLiteralUnion(node) {
|
|
|
13106
13360
|
function exportedContract(node) {
|
|
13107
13361
|
let current = node;
|
|
13108
13362
|
while (current !== void 0) {
|
|
13109
|
-
if (current.type === AST_NODE_TYPES55.
|
|
13363
|
+
if (current.type === AST_NODE_TYPES55.TSTypeAliasDeclaration || current.type === AST_NODE_TYPES55.TSInterfaceDeclaration) return current.parent.type === AST_NODE_TYPES55.ExportNamedDeclaration;
|
|
13110
13364
|
if (current.type === AST_NODE_TYPES55.Program) return false;
|
|
13111
13365
|
current = current.parent ?? void 0;
|
|
13112
13366
|
}
|
|
@@ -13142,11 +13396,11 @@ import { AST_NODE_TYPES as AST_NODE_TYPES56 } from "@typescript-eslint/utils";
|
|
|
13142
13396
|
var PREFER_NAMED_COMPLEX_RETURN_TYPE_DOCUMENTATION = {
|
|
13143
13397
|
summary: "Prefer a named contract for structurally complex function return types.",
|
|
13144
13398
|
rationale: "A large inline return annotation hides a reusable domain concept and makes signatures difficult to scan.",
|
|
13145
|
-
remediation: "
|
|
13399
|
+
remediation: "Name the complex nested shape while preserving its generic wrappers and type parameters; reference the named contract from the return annotation.",
|
|
13146
13400
|
category: "maintainability",
|
|
13147
13401
|
limitations: [
|
|
13148
13402
|
"Only explicit object types with at least three members and unions with at least three object variants are reported.",
|
|
13149
|
-
"
|
|
13403
|
+
"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."
|
|
13150
13404
|
],
|
|
13151
13405
|
examples: [
|
|
13152
13406
|
{ 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 },
|
|
@@ -13197,14 +13451,14 @@ var prefer_named_complex_return_type_default = createRule({
|
|
|
13197
13451
|
});
|
|
13198
13452
|
|
|
13199
13453
|
// src/rules/prefer-native-random-uuid.ts
|
|
13200
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES57, ASTUtils as
|
|
13454
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES57, ASTUtils as ASTUtils29 } from "@typescript-eslint/utils";
|
|
13201
13455
|
var PREFER_NATIVE_RANDOM_UUID_DOCUMENTATION = {
|
|
13202
13456
|
summary: "Prefer `globalThis.crypto.randomUUID()` over resolved zero-argument UUID v4 bindings from the `uuid` package.",
|
|
13203
13457
|
rationale: "The platform implementation avoids an unnecessary dependency for standard random UUID generation.",
|
|
13204
13458
|
remediation: "Call `globalThis.crypto.randomUUID()` and remove the unused `uuid` v4 import when possible.",
|
|
13205
13459
|
category: "maintainability",
|
|
13206
13460
|
autofix: "suggestion",
|
|
13207
|
-
limitations: ["Only resolved zero-argument UUID v4 calls are reported; customized and other UUID versions are excluded."],
|
|
13461
|
+
limitations: ["Only resolved zero-argument UUID v4 calls are reported; customized and other UUID versions are excluded. Suggestions require unshadowed globalThis and comment-free calls; verify native randomUUID availability in the deployment runtime."],
|
|
13208
13462
|
examples: [
|
|
13209
13463
|
{ id: "native-random-uuid", title: "Use the platform UUID generator", outcome: "no-match", files: [{ path: "src/id.ts", source: "const id = globalThis.crypto.randomUUID();" }], focusPath: "src/id.ts", expectedCount: 0, public: true },
|
|
13210
13464
|
{ id: "uuid-v4-package", title: "Do not call uuid v4 without options", outcome: "match", files: [{ path: "src/id.ts", source: "import { v4 } from 'uuid'; const id = v4();" }], focusPath: "src/id.ts", expectedCount: 1, public: true }
|
|
@@ -13224,7 +13478,7 @@ var prefer_native_random_uuid_default = createRule({
|
|
|
13224
13478
|
hasSuggestions: true,
|
|
13225
13479
|
schema: [],
|
|
13226
13480
|
messages: {
|
|
13227
|
-
preferNative: "
|
|
13481
|
+
preferNative: "Where supported by the deployment runtime, prefer native `globalThis.crypto.randomUUID()` over the `uuid` package for UUID v4.",
|
|
13228
13482
|
replaceWithNative: "Replace this UUID v4 call with the native implementation."
|
|
13229
13483
|
}
|
|
13230
13484
|
},
|
|
@@ -13233,22 +13487,24 @@ var prefer_native_random_uuid_default = createRule({
|
|
|
13233
13487
|
const directBindings = /* @__PURE__ */ new Set();
|
|
13234
13488
|
const namespaceBindings = /* @__PURE__ */ new Set();
|
|
13235
13489
|
function resolve2(identifier) {
|
|
13236
|
-
return
|
|
13490
|
+
return ASTUtils29.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
13237
13491
|
}
|
|
13238
13492
|
function record(identifier, destination) {
|
|
13239
13493
|
const variable = resolve2(identifier);
|
|
13240
13494
|
if (variable !== null) destination.add(variable);
|
|
13241
13495
|
}
|
|
13242
13496
|
function report2(node) {
|
|
13497
|
+
const globalBinding = ASTUtils29.findVariable(context.sourceCode.getScope(node), "globalThis");
|
|
13498
|
+
const canSuggest = (globalBinding?.defs.length ?? 0) === 0 && context.sourceCode.getCommentsInside(node).length === 0;
|
|
13243
13499
|
context.report({
|
|
13244
13500
|
node,
|
|
13245
13501
|
messageId: "preferNative",
|
|
13246
|
-
suggest: [
|
|
13502
|
+
suggest: canSuggest ? [
|
|
13247
13503
|
{
|
|
13248
13504
|
messageId: "replaceWithNative",
|
|
13249
13505
|
fix: (fixer) => fixer.replaceText(node, "globalThis.crypto.randomUUID()")
|
|
13250
13506
|
}
|
|
13251
|
-
]
|
|
13507
|
+
] : []
|
|
13252
13508
|
});
|
|
13253
13509
|
}
|
|
13254
13510
|
return {
|
|
@@ -13296,16 +13552,18 @@ var prefer_native_random_uuid_default = createRule({
|
|
|
13296
13552
|
});
|
|
13297
13553
|
|
|
13298
13554
|
// src/rules/prefer-node-crypto-hash.ts
|
|
13299
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES58, ASTUtils as
|
|
13555
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES58, ASTUtils as ASTUtils30 } from "@typescript-eslint/utils";
|
|
13300
13556
|
var PREFER_NODE_CRYPTO_HASH_DOCUMENTATION = {
|
|
13301
13557
|
summary: "Prefer the modern one-shot node:crypto hash API when streaming state is unnecessary.",
|
|
13302
13558
|
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.",
|
|
13303
|
-
remediation: "
|
|
13559
|
+
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.",
|
|
13304
13560
|
category: "performance",
|
|
13305
13561
|
limitations: [
|
|
13306
13562
|
"Only bindings and inline calls with statically proven provenance from crypto or node:crypto are analyzed; arbitrary assignments and dynamic module specifiers are excluded.",
|
|
13307
|
-
"Only a literal algorithm with exactly one update call is reported; streaming and incremental hashes remain valid."
|
|
13563
|
+
"Only a literal algorithm with exactly one update call is reported; streaming and incremental hashes remain valid.",
|
|
13564
|
+
"Runtime support and output encoding require manual review; no autofix or guaranteed speedup is promised."
|
|
13308
13565
|
],
|
|
13566
|
+
references: ["https://nodejs.org/api/crypto.html#cryptohashalgorithm-data-options"],
|
|
13309
13567
|
examples: [
|
|
13310
13568
|
{ 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 },
|
|
13311
13569
|
{ 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 }
|
|
@@ -13352,7 +13610,7 @@ var prefer_node_crypto_hash_default = createRule({
|
|
|
13352
13610
|
const directBindings = /* @__PURE__ */ new Set();
|
|
13353
13611
|
const namespaceBindings = /* @__PURE__ */ new Set();
|
|
13354
13612
|
function resolve2(identifier) {
|
|
13355
|
-
return
|
|
13613
|
+
return ASTUtils30.findVariable(
|
|
13356
13614
|
context.sourceCode.getScope(identifier),
|
|
13357
13615
|
identifier.name
|
|
13358
13616
|
);
|
|
@@ -13426,7 +13684,7 @@ function isCreateHashCall(node, directBindings, namespaceBindings, resolve2) {
|
|
|
13426
13684
|
}
|
|
13427
13685
|
|
|
13428
13686
|
// src/rules/prefer-node-fs-promises.ts
|
|
13429
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES59 } from "@typescript-eslint/utils";
|
|
13687
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES59, ASTUtils as ASTUtils31 } from "@typescript-eslint/utils";
|
|
13430
13688
|
var PREFER_NODE_FS_PROMISES_DOCUMENTATION = {
|
|
13431
13689
|
summary: "Prefer promise-based Node.js filesystem APIs over synchronous calls in production modules.",
|
|
13432
13690
|
rationale: "Synchronous filesystem work blocks the event loop and can stall unrelated daemon, server, and worker tasks.",
|
|
@@ -13434,6 +13692,7 @@ var PREFER_NODE_FS_PROMISES_DOCUMENTATION = {
|
|
|
13434
13692
|
category: "performance",
|
|
13435
13693
|
limitations: [
|
|
13436
13694
|
"Tests and generated files are excluded.",
|
|
13695
|
+
"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.",
|
|
13437
13696
|
"ESLint rule implementations under src/rules are excluded because visitor creation and execution are synchronous by contract.",
|
|
13438
13697
|
"Only statically identifiable node:fs loads are inspected; filesystem objects passed through arbitrary functions or assignments require type-aware analysis."
|
|
13439
13698
|
],
|
|
@@ -13452,14 +13711,14 @@ function memberName5(node) {
|
|
|
13452
13711
|
function unwrapAwait2(node) {
|
|
13453
13712
|
return node.type === AST_NODE_TYPES59.AwaitExpression ? node.argument : node;
|
|
13454
13713
|
}
|
|
13455
|
-
function isFsLoader(node) {
|
|
13714
|
+
function isFsLoader(node, isGlobal) {
|
|
13456
13715
|
const expression = unwrapAwait2(node);
|
|
13457
13716
|
if (expression.type === AST_NODE_TYPES59.ImportExpression) return isFsSpecifier(expression.source);
|
|
13458
13717
|
if (expression.type !== AST_NODE_TYPES59.CallExpression || expression.arguments.length !== 1) return false;
|
|
13459
13718
|
const [argument] = expression.arguments;
|
|
13460
13719
|
if (argument === void 0 || argument.type === AST_NODE_TYPES59.SpreadElement || !isFsSpecifier(argument)) return false;
|
|
13461
|
-
if (expression.callee.type === AST_NODE_TYPES59.Identifier) return expression.callee.name === "require";
|
|
13462
|
-
return expression.callee.type === AST_NODE_TYPES59.MemberExpression && expression.callee.object.type === AST_NODE_TYPES59.Identifier && expression.callee.object.name === "process" && memberName5(expression.callee) === "getBuiltinModule";
|
|
13720
|
+
if (expression.callee.type === AST_NODE_TYPES59.Identifier) return expression.callee.name === "require" && isGlobal(expression.callee);
|
|
13721
|
+
return expression.callee.type === AST_NODE_TYPES59.MemberExpression && expression.callee.object.type === AST_NODE_TYPES59.Identifier && expression.callee.object.name === "process" && isGlobal(expression.callee.object) && memberName5(expression.callee) === "getBuiltinModule";
|
|
13463
13722
|
}
|
|
13464
13723
|
function isFsSpecifier(node) {
|
|
13465
13724
|
return node.type === AST_NODE_TYPES59.Literal && (node.value === "node:fs" || node.value === "fs");
|
|
@@ -13486,13 +13745,26 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13486
13745
|
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text) || normalizedFilename.includes("src/rules/"))
|
|
13487
13746
|
return {};
|
|
13488
13747
|
const namespaces = /* @__PURE__ */ new Set();
|
|
13748
|
+
const bindingOf = (node) => ASTUtils31.findVariable(context.sourceCode.getScope(node), node.name);
|
|
13749
|
+
const isGlobal = (node) => {
|
|
13750
|
+
const binding = bindingOf(node);
|
|
13751
|
+
return binding === null || binding.defs.length === 0;
|
|
13752
|
+
};
|
|
13753
|
+
const isNamespace = (node) => {
|
|
13754
|
+
const binding = bindingOf(node);
|
|
13755
|
+
return binding !== null && namespaces.has(binding) && !binding.references.some((reference) => reference.isWrite() && reference.init !== true);
|
|
13756
|
+
};
|
|
13757
|
+
const recordNamespace = (node) => {
|
|
13758
|
+
const binding = bindingOf(node);
|
|
13759
|
+
if (binding !== null) namespaces.add(binding);
|
|
13760
|
+
};
|
|
13489
13761
|
return {
|
|
13490
13762
|
ImportDeclaration(node) {
|
|
13491
13763
|
if (node.source.value !== "node:fs" && node.source.value !== "fs") return;
|
|
13492
13764
|
const synchronousImports = [];
|
|
13493
13765
|
for (const specifier of node.specifiers) {
|
|
13494
13766
|
if (specifier.type === AST_NODE_TYPES59.ImportNamespaceSpecifier || specifier.type === AST_NODE_TYPES59.ImportDefaultSpecifier) {
|
|
13495
|
-
|
|
13767
|
+
recordNamespace(specifier.local);
|
|
13496
13768
|
continue;
|
|
13497
13769
|
}
|
|
13498
13770
|
if (specifier.type === AST_NODE_TYPES59.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES59.Identifier && specifier.imported.name.endsWith("Sync")) synchronousImports.push(specifier.imported.name);
|
|
@@ -13506,10 +13778,10 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13506
13778
|
}
|
|
13507
13779
|
},
|
|
13508
13780
|
VariableDeclarator(node) {
|
|
13509
|
-
if (node.init === null || !isFsLoader(node.init) && (node.init.type !== AST_NODE_TYPES59.Identifier || !
|
|
13781
|
+
if (node.init === null || !isFsLoader(node.init, isGlobal) && (node.init.type !== AST_NODE_TYPES59.Identifier || !isNamespace(node.init)))
|
|
13510
13782
|
return;
|
|
13511
13783
|
if (node.id.type === AST_NODE_TYPES59.Identifier) {
|
|
13512
|
-
|
|
13784
|
+
recordNamespace(node.id);
|
|
13513
13785
|
return;
|
|
13514
13786
|
}
|
|
13515
13787
|
if (node.id.type !== AST_NODE_TYPES59.ObjectPattern) return;
|
|
@@ -13530,7 +13802,7 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13530
13802
|
const name = memberName5(node);
|
|
13531
13803
|
if (name?.endsWith("Sync") !== true) return;
|
|
13532
13804
|
const object = unwrapAwait2(node.object);
|
|
13533
|
-
if (object.type === AST_NODE_TYPES59.Identifier &&
|
|
13805
|
+
if (object.type === AST_NODE_TYPES59.Identifier && isNamespace(object) || isFsLoader(object, isGlobal)) {
|
|
13534
13806
|
context.report({ node, messageId: "preferAsyncFs", data: { name } });
|
|
13535
13807
|
}
|
|
13536
13808
|
}
|
|
@@ -13539,13 +13811,13 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13539
13811
|
});
|
|
13540
13812
|
|
|
13541
13813
|
// src/rules/prefer-non-nullable-collection.ts
|
|
13542
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES60, ASTUtils as
|
|
13814
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES60, ASTUtils as ASTUtils32 } from "@typescript-eslint/utils";
|
|
13543
13815
|
var PREFER_NON_NULLABLE_COLLECTION_DOCUMENTATION = {
|
|
13544
|
-
summary: "Suggest
|
|
13816
|
+
summary: "Suggest reviewing nullish arrays that use local empty-array defaults or a shared null-or-empty guard.",
|
|
13545
13817
|
rationale: "A redundant nullish collection state spreads defaults and guards through consumers without carrying information.",
|
|
13546
13818
|
remediation: "Use a non-null collection type and normalize omitted input to an empty collection at the boundary.",
|
|
13547
13819
|
category: "maintainability",
|
|
13548
|
-
limitations: ["
|
|
13820
|
+
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."],
|
|
13549
13821
|
examples: [
|
|
13550
13822
|
{ 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 },
|
|
13551
13823
|
{ 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 }
|
|
@@ -13614,22 +13886,6 @@ function sameAccess(node, access) {
|
|
|
13614
13886
|
}
|
|
13615
13887
|
return node.type === AST_NODE_TYPES60.MemberExpression && !node.computed && node.object.type === AST_NODE_TYPES60.Identifier && node.object.name === access.object && node.property.type === AST_NODE_TYPES60.Identifier && node.property.name === access.property;
|
|
13616
13888
|
}
|
|
13617
|
-
function isNullGuard(node, access) {
|
|
13618
|
-
if (node.type === AST_NODE_TYPES60.UnaryExpression && node.operator === "!" && (sameAccess(node.argument, access) || optionalMemberLengthOf(node.argument, access))) return true;
|
|
13619
|
-
if (node.type !== AST_NODE_TYPES60.BinaryExpression || !["==", "==="].includes(node.operator)) {
|
|
13620
|
-
return false;
|
|
13621
|
-
}
|
|
13622
|
-
const nullish = (value) => value.type === AST_NODE_TYPES60.Literal && value.value === null || value.type === AST_NODE_TYPES60.Identifier && value.name === "undefined";
|
|
13623
|
-
return sameAccess(node.left, access) && nullish(node.right) || sameAccess(node.right, access) && nullish(node.left);
|
|
13624
|
-
}
|
|
13625
|
-
function isEmptyGuard(node, access) {
|
|
13626
|
-
if (node.type === AST_NODE_TYPES60.UnaryExpression && node.operator === "!" && memberLengthOf(node.argument, access)) return true;
|
|
13627
|
-
if (node.type !== AST_NODE_TYPES60.BinaryExpression || !["==", "===", "<="].includes(node.operator)) {
|
|
13628
|
-
return false;
|
|
13629
|
-
}
|
|
13630
|
-
const zero = (value) => value.type === AST_NODE_TYPES60.Literal && value.value === 0;
|
|
13631
|
-
return memberLengthOf(node.left, access) && zero(node.right) || memberLengthOf(node.right, access) && zero(node.left);
|
|
13632
|
-
}
|
|
13633
13889
|
function memberLengthOf(node, access) {
|
|
13634
13890
|
const target = node.type === AST_NODE_TYPES60.ChainExpression ? node.expression : node;
|
|
13635
13891
|
return target.type === AST_NODE_TYPES60.MemberExpression && !target.computed && target.property.type === AST_NODE_TYPES60.Identifier && target.property.name === "length" && sameAccess(target.object, access);
|
|
@@ -13644,7 +13900,24 @@ function hasEquivalentLeadingGuard(fn, access, visitorKeys) {
|
|
|
13644
13900
|
const terminating = first.consequent.type === AST_NODE_TYPES60.ReturnStatement || first.consequent.type === AST_NODE_TYPES60.ThrowStatement || first.consequent.type === AST_NODE_TYPES60.BlockStatement && first.consequent.body.length === 1 && (first.consequent.body[0]?.type === AST_NODE_TYPES60.ReturnStatement || first.consequent.body[0]?.type === AST_NODE_TYPES60.ThrowStatement);
|
|
13645
13901
|
if (!terminating) return false;
|
|
13646
13902
|
if (contains(first.consequent, visitorKeys, (node) => sameAccess(node, access))) return false;
|
|
13647
|
-
|
|
13903
|
+
if (first.test.type === AST_NODE_TYPES60.UnaryExpression && first.test.operator === "!" && optionalMemberLengthOf(first.test.argument, access)) return true;
|
|
13904
|
+
return first.test.type === AST_NODE_TYPES60.LogicalExpression && first.test.operator === "||" && isNullGuard(first.test.left, access) && isEmptyGuard(first.test.right, access);
|
|
13905
|
+
}
|
|
13906
|
+
function isNullGuard(node, access) {
|
|
13907
|
+
if (node.type === AST_NODE_TYPES60.UnaryExpression && node.operator === "!" && (sameAccess(node.argument, access) || optionalMemberLengthOf(node.argument, access))) return true;
|
|
13908
|
+
if (node.type !== AST_NODE_TYPES60.BinaryExpression || !["==", "==="].includes(node.operator)) {
|
|
13909
|
+
return false;
|
|
13910
|
+
}
|
|
13911
|
+
const nullish = (value) => value.type === AST_NODE_TYPES60.Literal && value.value === null || value.type === AST_NODE_TYPES60.Identifier && value.name === "undefined";
|
|
13912
|
+
return sameAccess(node.left, access) && nullish(node.right) || sameAccess(node.right, access) && nullish(node.left);
|
|
13913
|
+
}
|
|
13914
|
+
function isEmptyGuard(node, access) {
|
|
13915
|
+
if (node.type === AST_NODE_TYPES60.UnaryExpression && node.operator === "!" && memberLengthOf(node.argument, access)) return true;
|
|
13916
|
+
if (node.type !== AST_NODE_TYPES60.BinaryExpression || !["==", "===", "<="].includes(node.operator)) {
|
|
13917
|
+
return false;
|
|
13918
|
+
}
|
|
13919
|
+
const zero = (value) => value.type === AST_NODE_TYPES60.Literal && value.value === 0;
|
|
13920
|
+
return memberLengthOf(node.left, access) && zero(node.right) || node.operator !== "<=" && memberLengthOf(node.right, access) && zero(node.left);
|
|
13648
13921
|
}
|
|
13649
13922
|
function contains(node, visitorKeys, predicate) {
|
|
13650
13923
|
if (predicate(node)) return true;
|
|
@@ -13669,20 +13942,20 @@ function directlyCoalesced(node) {
|
|
|
13669
13942
|
return parent?.type === AST_NODE_TYPES60.LogicalExpression && parent.left === node && (parent.operator === "??" || parent.operator === "||") && emptyArray(parent.right);
|
|
13670
13943
|
}
|
|
13671
13944
|
function identifierIsOnlyCoalesced(context, binding, fn) {
|
|
13672
|
-
const variable =
|
|
13945
|
+
const variable = ASTUtils32.findVariable(context.sourceCode.getScope(binding), binding.name);
|
|
13673
13946
|
if (variable === null || variable.references.length === 0) return false;
|
|
13674
13947
|
return variable.references.every(
|
|
13675
13948
|
(reference) => belongsToFunction(reference.identifier, fn) && directlyCoalesced(reference.identifier)
|
|
13676
13949
|
);
|
|
13677
13950
|
}
|
|
13678
13951
|
function memberIsOnlyCoalesced(context, object, property, fn) {
|
|
13679
|
-
const variable =
|
|
13952
|
+
const variable = ASTUtils32.findVariable(context.sourceCode.getScope(object), object.name);
|
|
13680
13953
|
if (variable === null) return false;
|
|
13681
13954
|
const accesses = variable.references.flatMap((reference) => {
|
|
13682
13955
|
if (!belongsToFunction(reference.identifier, fn)) return [null];
|
|
13683
13956
|
const parent = reference.identifier.parent;
|
|
13684
13957
|
if (parent?.type === AST_NODE_TYPES60.MemberExpression && !parent.computed && parent.object === reference.identifier && parent.property.type === AST_NODE_TYPES60.Identifier && parent.property.name === property) return [parent];
|
|
13685
|
-
return [];
|
|
13958
|
+
return parent?.type === AST_NODE_TYPES60.MemberExpression && parent.object === reference.identifier ? [] : [null];
|
|
13686
13959
|
});
|
|
13687
13960
|
return accesses.length > 0 && accesses.every((access) => access !== null && directlyCoalesced(access));
|
|
13688
13961
|
}
|
|
@@ -13692,11 +13965,11 @@ var prefer_non_nullable_collection_default = createRule({
|
|
|
13692
13965
|
meta: {
|
|
13693
13966
|
type: "suggestion",
|
|
13694
13967
|
docs: {
|
|
13695
|
-
description: "Suggest
|
|
13968
|
+
description: "Suggest reviewing nullish arrays that use local empty-array defaults or a shared null-or-empty guard."
|
|
13696
13969
|
},
|
|
13697
13970
|
schema: [],
|
|
13698
13971
|
messages: {
|
|
13699
|
-
preferNonNullableCollection: "`{{name}}`
|
|
13972
|
+
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."
|
|
13700
13973
|
}
|
|
13701
13974
|
},
|
|
13702
13975
|
defaultOptions: [],
|
|
@@ -13786,7 +14059,7 @@ var prefer_non_nullable_collection_default = createRule({
|
|
|
13786
14059
|
// src/rules/prefer-nullish-filter-predicate.ts
|
|
13787
14060
|
import {
|
|
13788
14061
|
AST_NODE_TYPES as AST_NODE_TYPES61,
|
|
13789
|
-
ASTUtils as
|
|
14062
|
+
ASTUtils as ASTUtils33,
|
|
13790
14063
|
ESLintUtils as ESLintUtils5
|
|
13791
14064
|
} from "@typescript-eslint/utils";
|
|
13792
14065
|
import ts3 from "typescript";
|
|
@@ -13832,7 +14105,7 @@ var PREFER_NULLISH_FILTER_PREDICATE_DOCUMENTATION = {
|
|
|
13832
14105
|
]
|
|
13833
14106
|
};
|
|
13834
14107
|
function isUnshadowedBoolean(node, context) {
|
|
13835
|
-
const variable =
|
|
14108
|
+
const variable = ASTUtils33.findVariable(context.sourceCode.getScope(node), node.name);
|
|
13836
14109
|
return variable === null || variable.defs.length === 0;
|
|
13837
14110
|
}
|
|
13838
14111
|
function isBuiltinArrayFilter(node, services) {
|
|
@@ -13891,7 +14164,7 @@ function isProvablyTruthy(type, checker) {
|
|
|
13891
14164
|
}
|
|
13892
14165
|
function availableParameterName(node, context) {
|
|
13893
14166
|
for (const name of ["value", "item", "element", "candidate"]) {
|
|
13894
|
-
if (
|
|
14167
|
+
if (ASTUtils33.findVariable(context.sourceCode.getScope(node), name) === null) return name;
|
|
13895
14168
|
}
|
|
13896
14169
|
return null;
|
|
13897
14170
|
}
|
|
@@ -13945,7 +14218,7 @@ var prefer_nullish_filter_predicate_default = createRule({
|
|
|
13945
14218
|
|
|
13946
14219
|
// src/rules/prefer-await-in-async-return.ts
|
|
13947
14220
|
import {
|
|
13948
|
-
ASTUtils as
|
|
14221
|
+
ASTUtils as ASTUtils34,
|
|
13949
14222
|
ESLintUtils as ESLintUtils6,
|
|
13950
14223
|
AST_NODE_TYPES as AST_NODE_TYPES62
|
|
13951
14224
|
} from "@typescript-eslint/utils";
|
|
@@ -13953,12 +14226,12 @@ import * as ts4 from "typescript";
|
|
|
13953
14226
|
var PREFER_AWAIT_IN_ASYNC_RETURN_DOCUMENTATION = {
|
|
13954
14227
|
summary: "Prefer explicit `await` when an async function directly returns one typed Promise `.then` transform.",
|
|
13955
14228
|
rationale: "Mixing a directly returned Promise callback into otherwise async control flow makes sequencing and failures harder to read.",
|
|
13956
|
-
remediation: "
|
|
14229
|
+
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.",
|
|
13957
14230
|
category: "maintainability",
|
|
13958
14231
|
since: "15.6.3",
|
|
13959
14232
|
limitations: [
|
|
13960
14233
|
"Only a single directly returned `.then` call with an inline callback is checked.",
|
|
13961
|
-
"The receiver must be proven Promise-like by TypeScript;
|
|
14234
|
+
"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.",
|
|
13962
14235
|
"Direct loader callbacks passed to resolved `React.lazy` and `next/dynamic` imports are excluded because returning the module Promise is their framework contract."
|
|
13963
14236
|
],
|
|
13964
14237
|
examples: [
|
|
@@ -14059,13 +14332,13 @@ var prefer_await_in_async_return_default = createRule({
|
|
|
14059
14332
|
if (services === null) return {};
|
|
14060
14333
|
const frameworkLoaders = /* @__PURE__ */ new Set();
|
|
14061
14334
|
const rememberFrameworkLoader = (identifier) => {
|
|
14062
|
-
const variable =
|
|
14335
|
+
const variable = ASTUtils34.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
14063
14336
|
if (variable !== null) frameworkLoaders.add(variable);
|
|
14064
14337
|
};
|
|
14065
14338
|
const isFrameworkLoaderCallback = (owner) => {
|
|
14066
14339
|
const parent = owner.parent;
|
|
14067
14340
|
if (parent.type !== AST_NODE_TYPES62.CallExpression || parent.arguments[0] !== owner || parent.callee.type !== AST_NODE_TYPES62.Identifier) return false;
|
|
14068
|
-
const variable =
|
|
14341
|
+
const variable = ASTUtils34.findVariable(context.sourceCode.getScope(parent.callee), parent.callee.name);
|
|
14069
14342
|
return variable !== null && frameworkLoaders.has(variable);
|
|
14070
14343
|
};
|
|
14071
14344
|
return {
|
|
@@ -14095,16 +14368,21 @@ var prefer_await_in_async_return_default = createRule({
|
|
|
14095
14368
|
});
|
|
14096
14369
|
|
|
14097
14370
|
// src/rules/prefer-schema-for-api-payload.ts
|
|
14098
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES63 } from "@typescript-eslint/utils";
|
|
14371
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES63, ASTUtils as ASTUtils35 } from "@typescript-eslint/utils";
|
|
14099
14372
|
var PREFER_SCHEMA_FOR_API_PAYLOAD_DOCUMENTATION = {
|
|
14100
14373
|
summary: "Require Zod (or similar) schema validation on `response.json()` / `JSON.parse()` results before property access.",
|
|
14101
14374
|
rationale: "External JSON is untrusted at runtime even when its expected TypeScript shape is known statically.",
|
|
14102
|
-
remediation: "
|
|
14375
|
+
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.",
|
|
14103
14376
|
category: "correctness",
|
|
14104
|
-
limitations: [
|
|
14377
|
+
limitations: [
|
|
14378
|
+
"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.",
|
|
14379
|
+
"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.",
|
|
14380
|
+
"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.",
|
|
14381
|
+
"Test fixtures, generated clients and recognized local-file reads are excluded. This is bounded local analysis, not a general control-flow or mutation proof."
|
|
14382
|
+
],
|
|
14105
14383
|
examples: [
|
|
14106
|
-
{ 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 },
|
|
14107
|
-
{ 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 }
|
|
14384
|
+
{ 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 },
|
|
14385
|
+
{ 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 }
|
|
14108
14386
|
]
|
|
14109
14387
|
};
|
|
14110
14388
|
var unwrap6 = (node) => {
|
|
@@ -14129,7 +14407,7 @@ var isSchemaParseReference = (node) => {
|
|
|
14129
14407
|
const inner = unwrap6(node);
|
|
14130
14408
|
return inner !== null && inner.type === AST_NODE_TYPES63.MemberExpression && !inner.computed && inner.property.type === AST_NODE_TYPES63.Identifier && (inner.property.name === "parse" || inner.property.name === "safeParse");
|
|
14131
14409
|
};
|
|
14132
|
-
var isRawPayloadSource = (node, isKnownLocalText) => {
|
|
14410
|
+
var isRawPayloadSource = (node, context, isKnownLocalText) => {
|
|
14133
14411
|
let current = unwrap6(node);
|
|
14134
14412
|
if (current === null) return false;
|
|
14135
14413
|
if (current.type === AST_NODE_TYPES63.AwaitExpression) {
|
|
@@ -14147,13 +14425,31 @@ var isRawPayloadSource = (node, isKnownLocalText) => {
|
|
|
14147
14425
|
return false;
|
|
14148
14426
|
}
|
|
14149
14427
|
if (property.name === "json") {
|
|
14150
|
-
return
|
|
14428
|
+
return !callee.computed && current.arguments.length === 0 && isResponseSource(callee.object, context);
|
|
14151
14429
|
}
|
|
14152
14430
|
if (PROMISE_CHAIN_METHODS.has(property.name)) {
|
|
14153
|
-
return !current.arguments.some(isSchemaParseReference) && isRawPayloadSource(callee.object);
|
|
14431
|
+
return !current.arguments.some(isSchemaParseReference) && isRawPayloadSource(callee.object, context, isKnownLocalText);
|
|
14154
14432
|
}
|
|
14155
14433
|
const object = unwrap6(callee.object);
|
|
14156
|
-
|
|
14434
|
+
const input = unwrap6(current.arguments[0]);
|
|
14435
|
+
if (input?.type === AST_NODE_TYPES63.CallExpression && input.arguments.length === 1 && input.callee.type === AST_NODE_TYPES63.MemberExpression && !input.callee.computed && input.callee.object.type === AST_NODE_TYPES63.Identifier && input.callee.object.name === "JSON" && input.callee.property.type === AST_NODE_TYPES63.Identifier && input.callee.property.name === "stringify" && (ASTUtils35.findVariable(context.sourceCode.getScope(input.callee.object), "JSON")?.defs.length ?? 0) === 0) return false;
|
|
14436
|
+
return property.name === "parse" && object !== null && object.type === AST_NODE_TYPES63.Identifier && object.name === "JSON" && (ASTUtils35.findVariable(context.sourceCode.getScope(object), "JSON")?.defs.length ?? 0) === 0 && !isLocalFileRead(current.arguments[0]) && isKnownLocalText?.(current.arguments[0]) !== true;
|
|
14437
|
+
};
|
|
14438
|
+
var isResponseSource = (node, context, seen = /* @__PURE__ */ new Set()) => {
|
|
14439
|
+
let current = unwrap6(node);
|
|
14440
|
+
if (current?.type === AST_NODE_TYPES63.AwaitExpression) current = unwrap6(current.argument);
|
|
14441
|
+
if (current === null || seen.has(current)) return false;
|
|
14442
|
+
seen.add(current);
|
|
14443
|
+
const isGlobal = (identifier) => (ASTUtils35.findVariable(context.sourceCode.getScope(identifier), identifier.name)?.defs.length ?? 0) === 0;
|
|
14444
|
+
if (current.type === AST_NODE_TYPES63.CallExpression) return current.callee.type === AST_NODE_TYPES63.Identifier && current.callee.name === "fetch" && isGlobal(current.callee);
|
|
14445
|
+
if (current.type === AST_NODE_TYPES63.NewExpression) return current.callee.type === AST_NODE_TYPES63.Identifier && ["Request", "Response"].includes(current.callee.name) && isGlobal(current.callee);
|
|
14446
|
+
if (current.type !== AST_NODE_TYPES63.Identifier) return false;
|
|
14447
|
+
const binding = ASTUtils35.findVariable(context.sourceCode.getScope(current), current.name);
|
|
14448
|
+
if (binding?.defs.length !== 1 || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
14449
|
+
const definition = binding.defs[0];
|
|
14450
|
+
const annotation = definition?.name.type === AST_NODE_TYPES63.Identifier ? definition.name.typeAnnotation?.typeAnnotation : void 0;
|
|
14451
|
+
if (annotation?.type === AST_NODE_TYPES63.TSTypeReference && annotation.typeName.type === AST_NODE_TYPES63.Identifier && ["Request", "Response"].includes(annotation.typeName.name) && isGlobal(annotation.typeName)) return true;
|
|
14452
|
+
return definition?.type === "Variable" && definition.parent.kind === "const" && definition.node.init !== null && isResponseSource(definition.node.init, context, seen);
|
|
14157
14453
|
};
|
|
14158
14454
|
var FILE_READ_RE = /^(readFile|readFileSync|readJson|readJsonSync|readJSON)$/;
|
|
14159
14455
|
var isDirectLocalFileRead = (node) => {
|
|
@@ -14419,7 +14715,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14419
14715
|
},
|
|
14420
14716
|
schema: [],
|
|
14421
14717
|
messages: {
|
|
14422
|
-
unparsedJsonAccess: "
|
|
14718
|
+
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."
|
|
14423
14719
|
}
|
|
14424
14720
|
},
|
|
14425
14721
|
defaultOptions: [],
|
|
@@ -14430,6 +14726,40 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14430
14726
|
const unvalidatedVariables = /* @__PURE__ */ new Set();
|
|
14431
14727
|
const aliasGroups = /* @__PURE__ */ new Map();
|
|
14432
14728
|
const localFileTextVariables = /* @__PURE__ */ new Set();
|
|
14729
|
+
const namedGuards = /* @__PURE__ */ new Map();
|
|
14730
|
+
const guardDominates = (use, call) => {
|
|
14731
|
+
if (context.sourceCode.getScope(use).variableScope !== context.sourceCode.getScope(call).variableScope) return false;
|
|
14732
|
+
let guard = call;
|
|
14733
|
+
let positive = true;
|
|
14734
|
+
while (guard.parent.type === AST_NODE_TYPES63.UnaryExpression && guard.parent.operator === "!") {
|
|
14735
|
+
positive = !positive;
|
|
14736
|
+
guard = guard.parent;
|
|
14737
|
+
}
|
|
14738
|
+
while (positive && guard.parent.type === AST_NODE_TYPES63.LogicalExpression && guard.parent.operator === "&&") guard = guard.parent;
|
|
14739
|
+
const branch = guard.parent;
|
|
14740
|
+
if (branch.type === AST_NODE_TYPES63.IfStatement && branch.test === guard) {
|
|
14741
|
+
if (positive && nodeWithin2(use, branch.consequent)) return true;
|
|
14742
|
+
if (!positive && branch.alternate !== null && nodeWithin2(use, branch.alternate)) return true;
|
|
14743
|
+
const terminal = branch.consequent.type === AST_NODE_TYPES63.BlockStatement ? branch.consequent.body.at(-1) : branch.consequent;
|
|
14744
|
+
if (!positive && (terminal?.type === AST_NODE_TYPES63.ThrowStatement || terminal?.type === AST_NODE_TYPES63.ReturnStatement)) {
|
|
14745
|
+
let statement2 = use;
|
|
14746
|
+
while (statement2.parent !== void 0 && statement2.parent !== branch.parent && statement2.parent.type !== AST_NODE_TYPES63.Program) {
|
|
14747
|
+
if (statement2.type === AST_NODE_TYPES63.FunctionDeclaration || statement2.type === AST_NODE_TYPES63.FunctionExpression || statement2.type === AST_NODE_TYPES63.ArrowFunctionExpression) return false;
|
|
14748
|
+
statement2 = statement2.parent;
|
|
14749
|
+
}
|
|
14750
|
+
return statement2.parent === branch.parent && statement2.range[0] > branch.range[1];
|
|
14751
|
+
}
|
|
14752
|
+
}
|
|
14753
|
+
if (branch.type === AST_NODE_TYPES63.ConditionalExpression && branch.test === guard) return nodeWithin2(use, positive ? branch.consequent : branch.alternate);
|
|
14754
|
+
if (positive && branch.type === AST_NODE_TYPES63.WhileStatement && branch.test === guard) return nodeWithin2(use, branch.body);
|
|
14755
|
+
if (call.parent.type !== AST_NODE_TYPES63.ExpressionStatement || call.callee.type !== AST_NODE_TYPES63.Identifier || /^(?:is|has)[A-Z]/u.test(call.callee.name)) return false;
|
|
14756
|
+
let statement = use;
|
|
14757
|
+
while (statement.parent !== void 0 && statement.parent !== call.parent.parent && statement.parent.type !== AST_NODE_TYPES63.Program) {
|
|
14758
|
+
if (statement.type === AST_NODE_TYPES63.FunctionDeclaration || statement.type === AST_NODE_TYPES63.FunctionExpression || statement.type === AST_NODE_TYPES63.ArrowFunctionExpression) return false;
|
|
14759
|
+
statement = statement.parent;
|
|
14760
|
+
}
|
|
14761
|
+
return statement.parent === call.parent.parent && statement.range[0] > call.parent.range[1];
|
|
14762
|
+
};
|
|
14433
14763
|
const localFileTextRef = (node, scope) => {
|
|
14434
14764
|
const unwrapped = unwrap6(node);
|
|
14435
14765
|
if (unwrapped?.type !== AST_NODE_TYPES63.Identifier) return null;
|
|
@@ -14446,6 +14776,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14446
14776
|
};
|
|
14447
14777
|
const clearBinding = (variable) => {
|
|
14448
14778
|
unvalidatedVariables.delete(variable);
|
|
14779
|
+
namedGuards.delete(variable);
|
|
14449
14780
|
const group = aliasGroups.get(variable);
|
|
14450
14781
|
aliasGroups.delete(variable);
|
|
14451
14782
|
group?.delete(variable);
|
|
@@ -14480,10 +14811,24 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14480
14811
|
};
|
|
14481
14812
|
const isFullyNarrowedPattern = (declarator) => {
|
|
14482
14813
|
const declared = context.sourceCode.getDeclaredVariables(declarator);
|
|
14814
|
+
const statement = declarator.parent;
|
|
14815
|
+
const block = statement.parent;
|
|
14816
|
+
const followsRejectingGuard = (identifier) => {
|
|
14817
|
+
if (block.type !== AST_NODE_TYPES63.BlockStatement && block.type !== AST_NODE_TYPES63.Program) return false;
|
|
14818
|
+
if (context.sourceCode.getScope(identifier).variableScope !== context.sourceCode.getScope(declarator).variableScope) return false;
|
|
14819
|
+
return block.body.some((candidate2) => {
|
|
14820
|
+
if (candidate2.type !== AST_NODE_TYPES63.IfStatement || candidate2.range[1] >= identifier.range[0] || bindingValidationPolarity(candidate2.test, identifier.name) !== "valid-when-false") return false;
|
|
14821
|
+
const terminal = candidate2.consequent.type === AST_NODE_TYPES63.BlockStatement ? candidate2.consequent.body.at(-1) : candidate2.consequent;
|
|
14822
|
+
return terminal?.type === AST_NODE_TYPES63.ThrowStatement || terminal?.type === AST_NODE_TYPES63.ReturnStatement;
|
|
14823
|
+
});
|
|
14824
|
+
};
|
|
14483
14825
|
return declared.length > 0 && declared.every(
|
|
14484
|
-
(variable) => variable.references.some(
|
|
14485
|
-
|
|
14486
|
-
|
|
14826
|
+
(variable) => variable.references.some((reference) => isValidationRead(reference.identifier)) && variable.references.every((reference) => {
|
|
14827
|
+
const identifier = reference.identifier;
|
|
14828
|
+
if (reference.init === true) return true;
|
|
14829
|
+
if (reference.isWrite() || identifier.type !== AST_NODE_TYPES63.Identifier) return false;
|
|
14830
|
+
return isValidationRead(identifier) || isUseWithinValidatedBranch(identifier, identifier.name) || followsRejectingGuard(identifier);
|
|
14831
|
+
})
|
|
14487
14832
|
);
|
|
14488
14833
|
};
|
|
14489
14834
|
const trackInitializer = (declarator, scope) => {
|
|
@@ -14491,7 +14836,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14491
14836
|
const variable = declaredVars[0];
|
|
14492
14837
|
if (variable === void 0) return;
|
|
14493
14838
|
const localText = (candidate2) => localFileTextRef(candidate2, scope) !== null;
|
|
14494
|
-
if (isRawPayloadSource(declarator.init, localText)) {
|
|
14839
|
+
if (isRawPayloadSource(declarator.init, context, localText)) {
|
|
14495
14840
|
trackRawBinding(variable);
|
|
14496
14841
|
return;
|
|
14497
14842
|
}
|
|
@@ -14512,6 +14857,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14512
14857
|
if (node.id.type === AST_NODE_TYPES63.ObjectPattern || node.id.type === AST_NODE_TYPES63.ArrayPattern) {
|
|
14513
14858
|
if (isRawPayloadSource(
|
|
14514
14859
|
node.init,
|
|
14860
|
+
context,
|
|
14515
14861
|
(candidate2) => localFileTextRef(candidate2, scope) !== null
|
|
14516
14862
|
)) {
|
|
14517
14863
|
if (!isFullyNarrowedPattern(node)) {
|
|
@@ -14531,7 +14877,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14531
14877
|
if (variable === null) return;
|
|
14532
14878
|
const isLocalText = (candidate2) => localFileTextRef(candidate2, scope) !== null;
|
|
14533
14879
|
updateLocalFileText(variable, node.right, scope);
|
|
14534
|
-
if (isRawPayloadSource(node.right, isLocalText)) {
|
|
14880
|
+
if (isRawPayloadSource(node.right, context, isLocalText)) {
|
|
14535
14881
|
trackRawBinding(variable);
|
|
14536
14882
|
} else {
|
|
14537
14883
|
const source = unvalidatedVariableRef(node.right, scope, unvalidatedVariables);
|
|
@@ -14543,6 +14889,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14543
14889
|
if (node.left.type === AST_NODE_TYPES63.ObjectPattern || node.left.type === AST_NODE_TYPES63.ArrayPattern) {
|
|
14544
14890
|
if (isRawPayloadSource(
|
|
14545
14891
|
node.right,
|
|
14892
|
+
context,
|
|
14546
14893
|
(candidate2) => localFileTextRef(candidate2, scope) !== null
|
|
14547
14894
|
)) {
|
|
14548
14895
|
context.report({
|
|
@@ -14572,7 +14919,13 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14572
14919
|
continue;
|
|
14573
14920
|
}
|
|
14574
14921
|
const variable = findVariable2(scope, unwrapped.name);
|
|
14575
|
-
if (variable !== null)
|
|
14922
|
+
if (variable !== null) {
|
|
14923
|
+
for (const alias of aliasGroups.get(variable) ?? [variable]) {
|
|
14924
|
+
const guards = namedGuards.get(alias) ?? [];
|
|
14925
|
+
guards.push(node);
|
|
14926
|
+
namedGuards.set(alias, guards);
|
|
14927
|
+
}
|
|
14928
|
+
}
|
|
14576
14929
|
}
|
|
14577
14930
|
},
|
|
14578
14931
|
MemberExpression(node) {
|
|
@@ -14582,6 +14935,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14582
14935
|
const obj = unwrap6(node.object);
|
|
14583
14936
|
if (isRawPayloadSource(
|
|
14584
14937
|
obj,
|
|
14938
|
+
context,
|
|
14585
14939
|
(candidate2) => localFileTextRef(candidate2, scope) !== null
|
|
14586
14940
|
)) {
|
|
14587
14941
|
const parent = node.parent;
|
|
@@ -14593,6 +14947,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14593
14947
|
}
|
|
14594
14948
|
const variable = obj?.type === AST_NODE_TYPES63.Identifier ? unvalidatedVariableRef(obj, scope, unvalidatedVariables) : null;
|
|
14595
14949
|
if (variable !== null && obj?.type === AST_NODE_TYPES63.Identifier) {
|
|
14950
|
+
if (namedGuards.get(variable)?.some((call) => guardDominates(node, call))) return;
|
|
14596
14951
|
if (isUseWithinValidatedBranch(node, obj.name)) {
|
|
14597
14952
|
return;
|
|
14598
14953
|
}
|
|
@@ -14612,13 +14967,13 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14612
14967
|
});
|
|
14613
14968
|
|
|
14614
14969
|
// src/rules/prefer-shared-zod-enum.ts
|
|
14615
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES64 } from "@typescript-eslint/utils";
|
|
14970
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES64, ASTUtils as ASTUtils36 } from "@typescript-eslint/utils";
|
|
14616
14971
|
var PREFER_SHARED_ZOD_ENUM_DOCUMENTATION = {
|
|
14617
14972
|
summary: "Give literal Zod enum domains one reusable module-level schema.",
|
|
14618
14973
|
rationale: "Inline or repeated literal domains hide a reusable contract and allow equivalent fields to drift independently.",
|
|
14619
14974
|
remediation: "Declare a module-level named Zod enum schema and reuse it at each field or contract site.",
|
|
14620
14975
|
category: "maintainability",
|
|
14621
|
-
limitations: ["Only direct z.enum calls with string-literal arrays are inspected; computed domains require review."],
|
|
14976
|
+
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."],
|
|
14622
14977
|
examples: [
|
|
14623
14978
|
{ 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 },
|
|
14624
14979
|
{ 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 }
|
|
@@ -14660,16 +15015,22 @@ var prefer_shared_zod_enum_default = createRule({
|
|
|
14660
15015
|
create(context) {
|
|
14661
15016
|
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
14662
15017
|
const zodBindings = /* @__PURE__ */ new Set();
|
|
15018
|
+
const bindingOf = (node) => ASTUtils36.findVariable(context.sourceCode.getScope(node), node.name);
|
|
14663
15019
|
const seen = /* @__PURE__ */ new Set();
|
|
14664
15020
|
return {
|
|
14665
15021
|
ImportDeclaration(node) {
|
|
14666
15022
|
if (!isZodModule(node.source.value)) return;
|
|
14667
15023
|
for (const specifier of node.specifiers) {
|
|
14668
|
-
if (specifier.type === AST_NODE_TYPES64.ImportNamespaceSpecifier || specifier.type === AST_NODE_TYPES64.ImportDefaultSpecifier || specifier.type === AST_NODE_TYPES64.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES64.Identifier && specifier.imported.name === "z")
|
|
15024
|
+
if (specifier.type === AST_NODE_TYPES64.ImportNamespaceSpecifier || specifier.type === AST_NODE_TYPES64.ImportDefaultSpecifier || specifier.type === AST_NODE_TYPES64.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES64.Identifier && specifier.imported.name === "z") {
|
|
15025
|
+
const binding = bindingOf(specifier.local);
|
|
15026
|
+
if (binding !== null) zodBindings.add(binding);
|
|
15027
|
+
}
|
|
14669
15028
|
}
|
|
14670
15029
|
},
|
|
14671
15030
|
CallExpression(node) {
|
|
14672
|
-
if (node.callee.type !== AST_NODE_TYPES64.MemberExpression || node.callee.computed || node.callee.object.type !== AST_NODE_TYPES64.Identifier ||
|
|
15031
|
+
if (node.callee.type !== AST_NODE_TYPES64.MemberExpression || node.callee.computed || node.callee.object.type !== AST_NODE_TYPES64.Identifier || node.callee.property.type !== AST_NODE_TYPES64.Identifier || node.callee.property.name !== "enum") return;
|
|
15032
|
+
const binding = bindingOf(node.callee.object);
|
|
15033
|
+
if (binding === null || !zodBindings.has(binding)) return;
|
|
14673
15034
|
const domain = literalDomain(node);
|
|
14674
15035
|
if (domain === null) return;
|
|
14675
15036
|
const key = JSON.stringify(domain);
|
|
@@ -14691,6 +15052,7 @@ var PREFER_SWITCH_FOR_REPEATED_EQUALITY_DOCUMENTATION = {
|
|
|
14691
15052
|
category: "maintainability",
|
|
14692
15053
|
limitations: [
|
|
14693
15054
|
"Only direct if/else-if chains with at least three strict-equality tests are reported.",
|
|
15055
|
+
"The discriminant must be a bare identifier; calls, getters, indexed reads and other repeatedly evaluated expressions are excluded. Review case-value effects, selector mutation, branch scoping, and break/continue targets when converting manually; this is not an equivalence proof.",
|
|
14694
15056
|
"Case values may be literals, enum-like member references, or upper-case named constants; dynamic expressions are excluded.",
|
|
14695
15057
|
"The rule deliberately ignores compound predicates, loose equality, ranges, and chains that compare different discriminants."
|
|
14696
15058
|
],
|
|
@@ -14704,7 +15066,8 @@ function discriminantText(sourceCode, test) {
|
|
|
14704
15066
|
const leftIsCase = isCaseValue(test.left);
|
|
14705
15067
|
const rightIsCase = isCaseValue(test.right);
|
|
14706
15068
|
if (leftIsCase === rightIsCase) return null;
|
|
14707
|
-
|
|
15069
|
+
const discriminant = leftIsCase ? test.right : test.left;
|
|
15070
|
+
return discriminant.type === AST_NODE_TYPES65.Identifier ? sourceCode.getText(discriminant) : null;
|
|
14708
15071
|
}
|
|
14709
15072
|
function isCaseValue(node) {
|
|
14710
15073
|
if (node.type === AST_NODE_TYPES65.Literal) return true;
|
|
@@ -15294,7 +15657,7 @@ var prefer_semantic_colors_default = createRule({
|
|
|
15294
15657
|
});
|
|
15295
15658
|
|
|
15296
15659
|
// src/rules/prefer-server-actions.ts
|
|
15297
|
-
import { ASTUtils as
|
|
15660
|
+
import { ASTUtils as ASTUtils37 } from "@typescript-eslint/utils";
|
|
15298
15661
|
var PREFER_SERVER_ACTIONS_DOCUMENTATION = {
|
|
15299
15662
|
summary: "Prefer Next.js Server Actions over same-origin API mutations.",
|
|
15300
15663
|
rationale: "Server Actions can remove a hand-written internal API wrapper while retaining typed application calls. Client invocations still cross a network and serialization boundary.",
|
|
@@ -15326,7 +15689,7 @@ function resolvesToGlobalFetch(context, identifier) {
|
|
|
15326
15689
|
function resolveNode(node, context) {
|
|
15327
15690
|
if (!node) return null;
|
|
15328
15691
|
if (node.type !== "Identifier") return node;
|
|
15329
|
-
const variable =
|
|
15692
|
+
const variable = ASTUtils37.findVariable(getScope(context, node), node.name);
|
|
15330
15693
|
const definition = variable?.defs.length === 1 ? variable.defs[0] : void 0;
|
|
15331
15694
|
if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init === null || variable?.references.some((reference) => reference.isWrite() && reference.init !== true)) return node;
|
|
15332
15695
|
if (definition.node.init.type === "ObjectExpression" && variable?.references.some((reference) => reference.identifier !== node && reference.init !== true)) return node;
|
|
@@ -15335,7 +15698,7 @@ function resolveNode(node, context) {
|
|
|
15335
15698
|
function isAxiosClient(node, context, seen = /* @__PURE__ */ new Set()) {
|
|
15336
15699
|
if (node.type !== "Identifier" || seen.has(node)) return false;
|
|
15337
15700
|
seen.add(node);
|
|
15338
|
-
const variable =
|
|
15701
|
+
const variable = ASTUtils37.findVariable(getScope(context, node), node.name);
|
|
15339
15702
|
const definition = variable?.defs.length === 1 ? variable.defs[0] : void 0;
|
|
15340
15703
|
if (definition === void 0 || variable?.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
15341
15704
|
if (variable?.references.some((reference) => {
|
|
@@ -15538,7 +15901,7 @@ var prefer_server_actions_default = createRule({
|
|
|
15538
15901
|
});
|
|
15539
15902
|
|
|
15540
15903
|
// src/rules/prefer-whole-object-assertion.ts
|
|
15541
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES67 } from "@typescript-eslint/utils";
|
|
15904
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES67, ASTUtils as ASTUtils38 } from "@typescript-eslint/utils";
|
|
15542
15905
|
var MERGEABLE_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
|
|
15543
15906
|
var SYNTHETIC_LITERAL_MATCHERS = /* @__PURE__ */ new Map([
|
|
15544
15907
|
["toBeNull", "null"],
|
|
@@ -15645,6 +16008,13 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
15645
16008
|
return null;
|
|
15646
16009
|
}
|
|
15647
16010
|
const actual = expectCall.arguments[0];
|
|
16011
|
+
const variable = ASTUtils38.findVariable(sourceCode.getScope(expectCall.callee), expectCall.callee.name);
|
|
16012
|
+
if (variable !== null && variable.defs.some((definition) => {
|
|
16013
|
+
if (definition.node.type !== AST_NODE_TYPES67.ImportSpecifier) return true;
|
|
16014
|
+
const declaration = definition.node.parent;
|
|
16015
|
+
const imported = definition.node.imported;
|
|
16016
|
+
return declaration.type !== AST_NODE_TYPES67.ImportDeclaration || !["vitest", "@jest/globals", "@playwright/test", "bun:test"].includes(String(declaration.source.value)) || (imported.type === AST_NODE_TYPES67.Identifier ? imported.name : imported.value) !== "expect";
|
|
16017
|
+
})) return null;
|
|
15648
16018
|
if (actual === void 0 || actual.type !== AST_NODE_TYPES67.MemberExpression || actual.optional) {
|
|
15649
16019
|
return null;
|
|
15650
16020
|
}
|
|
@@ -15804,14 +16174,14 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
15804
16174
|
});
|
|
15805
16175
|
|
|
15806
16176
|
// src/rules/repeated-static-call-cases.ts
|
|
15807
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES68, ASTUtils as
|
|
16177
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES68, ASTUtils as ASTUtils39 } from "@typescript-eslint/utils";
|
|
15808
16178
|
var REPEATED_STATIC_CALL_CASES_DOCUMENTATION = {
|
|
15809
|
-
summary: "
|
|
15810
|
-
rationale: "Copy-pasted cases obscure the input table and stop later cases from being reported
|
|
15811
|
-
remediation: "
|
|
16179
|
+
summary: "Review three or more consecutive static-input call assertions as potential named cases.",
|
|
16180
|
+
rationale: "Copy-pasted cases obscure the input table, and a thrown assertion can stop later cases from being reported.",
|
|
16181
|
+
remediation: "If the calls are independent, use the runner's named parameterized cases or subtests. Preserve ordered state-transition scenarios as one test.",
|
|
15812
16182
|
category: "testing",
|
|
15813
16183
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
15814
|
-
limitations: ["Only consecutive top-level assertions with direct calls and
|
|
16184
|
+
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."],
|
|
15815
16185
|
examples: [
|
|
15816
16186
|
{ 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 },
|
|
15817
16187
|
{ 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 }
|
|
@@ -15830,7 +16200,7 @@ function staticMemberName5(node) {
|
|
|
15830
16200
|
return null;
|
|
15831
16201
|
}
|
|
15832
16202
|
function importedName6(identifier, context, modules) {
|
|
15833
|
-
const variable =
|
|
16203
|
+
const variable = ASTUtils39.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
15834
16204
|
if (variable === null || variable.defs.length === 0) return identifier.name;
|
|
15835
16205
|
for (const definition of variable.defs) {
|
|
15836
16206
|
if (definition.node.type !== AST_NODE_TYPES68.ImportSpecifier) continue;
|
|
@@ -15892,7 +16262,7 @@ function staticShape(node) {
|
|
|
15892
16262
|
return "dynamic";
|
|
15893
16263
|
}
|
|
15894
16264
|
}
|
|
15895
|
-
function assertionShape(statement, context) {
|
|
16265
|
+
function assertionShape(statement, context, callback) {
|
|
15896
16266
|
if (statement.type !== AST_NODE_TYPES68.ExpressionStatement || statement.expression.type !== AST_NODE_TYPES68.CallExpression) return null;
|
|
15897
16267
|
const matcherCall = statement.expression;
|
|
15898
16268
|
if (matcherCall.callee.type !== AST_NODE_TYPES68.MemberExpression || matcherCall.callee.computed || matcherCall.callee.property.type !== AST_NODE_TYPES68.Identifier || matcherCall.arguments.length !== 1) return null;
|
|
@@ -15904,6 +16274,10 @@ function assertionShape(statement, context) {
|
|
|
15904
16274
|
const expected = matcherCall.arguments[0];
|
|
15905
16275
|
if (observed?.type !== AST_NODE_TYPES68.CallExpression || observed.callee.type !== AST_NODE_TYPES68.Identifier || observed.arguments.length === 0 || observed.arguments.some((arg) => arg.type === AST_NODE_TYPES68.SpreadElement || !isStatic(arg)) || expected?.type === AST_NODE_TYPES68.SpreadElement || expected === void 0 || !isStatic(expected)) return null;
|
|
15906
16276
|
const skeleton = `${observed.callee.name}/${observed.arguments.map((item) => staticShape(item)).join(",")}/${chain.modifiers.join(".")}/${matcher}/${staticShape(expected)}`;
|
|
16277
|
+
const binding = ASTUtils39.findVariable(context.sourceCode.getScope(observed.callee), observed.callee.name);
|
|
16278
|
+
if (binding?.defs.some(
|
|
16279
|
+
(definition) => definition.node.range[0] >= callback.range[0] && definition.node.range[1] <= callback.range[1]
|
|
16280
|
+
)) return null;
|
|
15907
16281
|
const values = [...observed.arguments, expected].map((item) => context.sourceCode.getText(item)).join("\0");
|
|
15908
16282
|
return { statement, skeleton, values };
|
|
15909
16283
|
}
|
|
@@ -15923,9 +16297,9 @@ var repeated_static_call_cases_default = createRule({
|
|
|
15923
16297
|
documentation: REPEATED_STATIC_CALL_CASES_DOCUMENTATION,
|
|
15924
16298
|
meta: {
|
|
15925
16299
|
type: "suggestion",
|
|
15926
|
-
docs: { description:
|
|
16300
|
+
docs: { description: REPEATED_STATIC_CALL_CASES_DOCUMENTATION.summary },
|
|
15927
16301
|
schema: [],
|
|
15928
|
-
messages: { repeatedStaticCallCases: "These {{count}} consecutive assertions repeat
|
|
16302
|
+
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." }
|
|
15929
16303
|
},
|
|
15930
16304
|
defaultOptions: [],
|
|
15931
16305
|
create(context) {
|
|
@@ -15960,7 +16334,7 @@ var repeated_static_call_cases_default = createRule({
|
|
|
15960
16334
|
run = [];
|
|
15961
16335
|
};
|
|
15962
16336
|
for (const statement of node.body.body) {
|
|
15963
|
-
const shape = assertionShape(statement, context);
|
|
16337
|
+
const shape = assertionShape(statement, context, node);
|
|
15964
16338
|
if (shape === null || run.length > 0 && run[0]?.skeleton !== shape.skeleton) flush();
|
|
15965
16339
|
if (shape !== null) run.push(shape);
|
|
15966
16340
|
}
|
|
@@ -15993,6 +16367,11 @@ var PREFER_ZOD_INFER_DOCUMENTATION = {
|
|
|
15993
16367
|
rationale: "A derived type stays synchronized when the runtime schema changes.",
|
|
15994
16368
|
remediation: "Replace the hand-written twin with `z.infer<typeof Schema>`.",
|
|
15995
16369
|
category: "correctness",
|
|
16370
|
+
limitations: [
|
|
16371
|
+
"Only module-level const schemas and module-level type declarations are paired; local declarations are excluded rather than matched by spelling across scopes.",
|
|
16372
|
+
"By default every field must positively agree; collection, nested-object and referenced-schema equivalence is not inferred from outer syntax alone.",
|
|
16373
|
+
"This is a bounded syntactic comparison, not general type equivalence. Review schema input versus output, interface augmentation, and separately evolving domain contracts before replacing a declaration."
|
|
16374
|
+
],
|
|
15996
16375
|
examples: [
|
|
15997
16376
|
{ id: "inferred-type", title: "Infer the schema type", outcome: "no-match", files: [{ path: "src/user.ts", source: 'import { z } from "zod"; const UserSchema = z.object({ id: z.string() }); type User = z.infer<typeof UserSchema>;' }], focusPath: "src/user.ts", expectedCount: 0, public: true },
|
|
15998
16377
|
{ id: "handwritten-twin", title: "Do not duplicate the schema shape", outcome: "match", files: [{ path: "src/user.ts", source: 'import { z } from "zod"; const UserSchema = z.object({ id: z.string() }); interface User { id: string }' }], focusPath: "src/user.ts", expectedCount: 1, public: true }
|
|
@@ -16172,6 +16551,10 @@ function sameDomain(left, right) {
|
|
|
16172
16551
|
function isExportedDeclaration(node) {
|
|
16173
16552
|
return node.parent?.type === AST_NODE_TYPES69.ExportNamedDeclaration;
|
|
16174
16553
|
}
|
|
16554
|
+
function isModuleLevelDeclaration(node) {
|
|
16555
|
+
const parent = node.parent;
|
|
16556
|
+
return parent?.type === AST_NODE_TYPES69.Program || parent?.type === AST_NODE_TYPES69.ExportNamedDeclaration && parent.parent.type === AST_NODE_TYPES69.Program;
|
|
16557
|
+
}
|
|
16175
16558
|
function isModuleLevelConst(node) {
|
|
16176
16559
|
const declaration = node.parent;
|
|
16177
16560
|
if (declaration.type !== AST_NODE_TYPES69.VariableDeclaration || declaration.kind !== "const") {
|
|
@@ -16222,6 +16605,9 @@ function leafAgrees(field, annotation) {
|
|
|
16222
16605
|
return annotationDomain !== null && sameDomain(field.domain, annotationDomain);
|
|
16223
16606
|
}
|
|
16224
16607
|
const { leaf } = field;
|
|
16608
|
+
if (leaf !== null && ["array", "tuple", "object", "strictObject", "looseObject", "record", "map", "set", "promise", "intersection"].includes(leaf)) {
|
|
16609
|
+
return false;
|
|
16610
|
+
}
|
|
16225
16611
|
if (leaf === null || annotation === null) {
|
|
16226
16612
|
return null;
|
|
16227
16613
|
}
|
|
@@ -16514,7 +16900,6 @@ var prefer_zod_infer_default = createRule({
|
|
|
16514
16900
|
if (fields.size !== members.size) {
|
|
16515
16901
|
return false;
|
|
16516
16902
|
}
|
|
16517
|
-
let agreements = 0;
|
|
16518
16903
|
for (const [name, field] of fields) {
|
|
16519
16904
|
const member = members.get(name);
|
|
16520
16905
|
if (member === void 0) {
|
|
@@ -16533,14 +16918,11 @@ var prefer_zod_infer_default = createRule({
|
|
|
16533
16918
|
return false;
|
|
16534
16919
|
}
|
|
16535
16920
|
const agrees = leafAgrees(field, member.annotation);
|
|
16536
|
-
if (agrees
|
|
16921
|
+
if (agrees !== true) {
|
|
16537
16922
|
return false;
|
|
16538
16923
|
}
|
|
16539
|
-
if (agrees === true) {
|
|
16540
|
-
agreements += 1;
|
|
16541
|
-
}
|
|
16542
16924
|
}
|
|
16543
|
-
return
|
|
16925
|
+
return true;
|
|
16544
16926
|
}
|
|
16545
16927
|
return {
|
|
16546
16928
|
Program(node) {
|
|
@@ -16554,7 +16936,7 @@ var prefer_zod_infer_default = createRule({
|
|
|
16554
16936
|
recordZodImport(node);
|
|
16555
16937
|
},
|
|
16556
16938
|
VariableDeclarator(node) {
|
|
16557
|
-
if (node.id.type !== AST_NODE_TYPES69.Identifier || node.init == null) {
|
|
16939
|
+
if (node.id.type !== AST_NODE_TYPES69.Identifier || node.init == null || !isModuleLevelConst(node)) {
|
|
16558
16940
|
return;
|
|
16559
16941
|
}
|
|
16560
16942
|
const fields = schemaFields(node.init);
|
|
@@ -16587,6 +16969,7 @@ var prefer_zod_infer_default = createRule({
|
|
|
16587
16969
|
}
|
|
16588
16970
|
},
|
|
16589
16971
|
TSInterfaceDeclaration(node) {
|
|
16972
|
+
if (!isModuleLevelDeclaration(node)) return;
|
|
16590
16973
|
if (node.typeParameters !== void 0 || (node.extends?.length ?? 0) > 0) {
|
|
16591
16974
|
return;
|
|
16592
16975
|
}
|
|
@@ -16602,6 +16985,7 @@ var prefer_zod_infer_default = createRule({
|
|
|
16602
16985
|
);
|
|
16603
16986
|
},
|
|
16604
16987
|
TSTypeAliasDeclaration(node) {
|
|
16988
|
+
if (!isModuleLevelDeclaration(node)) return;
|
|
16605
16989
|
const schemaName = inferredSchemaName(node.typeAnnotation);
|
|
16606
16990
|
if (schemaName !== null) {
|
|
16607
16991
|
inferredAliases.push({
|
|
@@ -16719,6 +17103,7 @@ var REQUIRE_ASSERT_NEVER_DOCUMENTATION = {
|
|
|
16719
17103
|
rationale: "An empty default silently accepts new union members instead of making the compiler identify the missing case.",
|
|
16720
17104
|
remediation: "Call `assertNever` with the discriminant in the exhaustive switch default.",
|
|
16721
17105
|
category: "correctness",
|
|
17106
|
+
limitations: ["Requires type information and singleton case values covering the current union. The helper must accept never to provide a compile-time check; its spelling alone is not a proof of that contract. Review the desired runtime behavior for unexpected external values."],
|
|
16722
17107
|
examples: [
|
|
16723
17108
|
{ id: "assert-never-default", title: "Make the default exhaustive", outcome: "no-match", files: [{ path: "src/render.ts", source: "declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: assertNever(kind); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
|
|
16724
17109
|
{ id: "empty-default", title: "Do not leave an exhaustive default empty", outcome: "match", files: [{ path: "src/render.ts", source: "declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: }" }], focusPath: "src/render.ts", expectedCount: 1, public: true }
|
|
@@ -16775,11 +17160,9 @@ function isExhaustiveFiniteSwitch(node, services) {
|
|
|
16775
17160
|
if (caseNode.test === null) continue;
|
|
16776
17161
|
const test = services.esTreeNodeToTSNodeMap.get(caseNode.test);
|
|
16777
17162
|
const testType = checker.getTypeAtLocation(test);
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
if (key !== null) handled.add(key);
|
|
16782
|
-
}
|
|
17163
|
+
if (testType.isUnion()) continue;
|
|
17164
|
+
const key = finiteTypeKey(testType, checker);
|
|
17165
|
+
if (key !== null) handled.add(key);
|
|
16783
17166
|
}
|
|
16784
17167
|
return [...expected].every((key) => handled.has(key));
|
|
16785
17168
|
}
|
|
@@ -16831,7 +17214,7 @@ var require_assert_never_default = createRule({
|
|
|
16831
17214
|
});
|
|
16832
17215
|
|
|
16833
17216
|
// src/rules/require-fetch-timeout.ts
|
|
16834
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES71, ASTUtils as
|
|
17217
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES71, ASTUtils as ASTUtils40 } from "@typescript-eslint/utils";
|
|
16835
17218
|
var REQUIRE_FETCH_TIMEOUT_DOCUMENTATION = {
|
|
16836
17219
|
summary: "Require an explicit abort signal on locally analyzable global fetch calls.",
|
|
16837
17220
|
rationale: "An unbounded request can occupy work indefinitely when an upstream stalls.",
|
|
@@ -16913,7 +17296,7 @@ var require_fetch_timeout_default = createRule({
|
|
|
16913
17296
|
}
|
|
16914
17297
|
function resolvesToGlobal(identifier) {
|
|
16915
17298
|
const scope = context.sourceCode.getScope(identifier);
|
|
16916
|
-
const variable =
|
|
17299
|
+
const variable = ASTUtils40.findVariable(scope, identifier.name);
|
|
16917
17300
|
return variable === null || variable.defs.length === 0;
|
|
16918
17301
|
}
|
|
16919
17302
|
function isGlobalFetchCall2(callee) {
|
|
@@ -16923,7 +17306,7 @@ var require_fetch_timeout_default = createRule({
|
|
|
16923
17306
|
return callee.type === AST_NODE_TYPES71.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES71.Identifier && callee.property.name === "fetch" && callee.object.type === AST_NODE_TYPES71.Identifier && GLOBAL_OBJECTS2.has(callee.object.name) && resolvesToGlobal(callee.object);
|
|
16924
17307
|
}
|
|
16925
17308
|
function localConstInitProvablyLacksSignal(identifier) {
|
|
16926
|
-
const variable =
|
|
17309
|
+
const variable = ASTUtils40.findVariable(
|
|
16927
17310
|
context.sourceCode.getScope(identifier),
|
|
16928
17311
|
identifier.name
|
|
16929
17312
|
);
|
|
@@ -16945,7 +17328,7 @@ var require_fetch_timeout_default = createRule({
|
|
|
16945
17328
|
function isForwardedRequest(argument) {
|
|
16946
17329
|
let value = argument;
|
|
16947
17330
|
if (value.type === AST_NODE_TYPES71.Identifier) {
|
|
16948
|
-
const binding =
|
|
17331
|
+
const binding = ASTUtils40.findVariable(context.sourceCode.getScope(value), value.name);
|
|
16949
17332
|
const definition = binding?.defs.length === 1 ? binding.defs[0] : void 0;
|
|
16950
17333
|
if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init === null || binding?.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
16951
17334
|
value = definition.node.init;
|
|
@@ -16973,10 +17356,12 @@ var require_fetch_timeout_default = createRule({
|
|
|
16973
17356
|
import { AST_NODE_TYPES as AST_NODE_TYPES72 } from "@typescript-eslint/utils";
|
|
16974
17357
|
var REQUIRE_INTERFACE_FOR_EXPORTED_CLASS_DOCUMENTATION = {
|
|
16975
17358
|
summary: "Require exported concrete classes with public behavior to declare a contract.",
|
|
16976
|
-
rationale: "
|
|
17359
|
+
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.",
|
|
16977
17360
|
remediation: "Declare a focused interface and add an implements clause, or inherit from an intentional base contract.",
|
|
16978
17361
|
category: "architecture",
|
|
16979
17362
|
limitations: [
|
|
17363
|
+
"JavaScript files are excluded because implements is TypeScript syntax; imported framework base contracts still require manual policy review.",
|
|
17364
|
+
"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.",
|
|
16980
17365
|
"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.",
|
|
16981
17366
|
"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.",
|
|
16982
17367
|
"Static factories and data-only classes without public instance methods are outside the contract requirement."
|
|
@@ -17048,7 +17433,7 @@ var require_interface_for_exported_class_default = createRule({
|
|
|
17048
17433
|
},
|
|
17049
17434
|
defaultOptions: [],
|
|
17050
17435
|
create(context) {
|
|
17051
|
-
if (isTestFile(context.filename) || isStoryFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
17436
|
+
if (/\.(?:js|jsx|mjs|cjs)$/iu.test(context.filename) || isTestFile(context.filename) || isStoryFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
17052
17437
|
return {
|
|
17053
17438
|
"Program:exit"(program) {
|
|
17054
17439
|
const classes = /* @__PURE__ */ new Map();
|
|
@@ -17434,15 +17819,14 @@ var publicMethodNames = (body2, functionAliases) => {
|
|
|
17434
17819
|
if (member.static || member.accessibility === "private" || member.accessibility === "protected") continue;
|
|
17435
17820
|
if (member.key.type === AST_NODE_TYPES73.PrivateIdentifier) continue;
|
|
17436
17821
|
if (member.value?.type !== AST_NODE_TYPES73.ArrowFunctionExpression && member.value?.type !== AST_NODE_TYPES73.FunctionExpression && member.typeAnnotation?.typeAnnotation.type !== AST_NODE_TYPES73.TSFunctionType && !(member.typeAnnotation?.typeAnnotation.type === AST_NODE_TYPES73.TSTypeReference && member.typeAnnotation.typeAnnotation.typeName.type === AST_NODE_TYPES73.Identifier && functionAliases.has(member.typeAnnotation.typeAnnotation.typeName.name))) continue;
|
|
17437
|
-
names.push(member
|
|
17822
|
+
names.push(declaredMemberName(member) ?? "\u2026");
|
|
17438
17823
|
continue;
|
|
17439
17824
|
}
|
|
17440
17825
|
if (member.type !== AST_NODE_TYPES73.MethodDefinition) continue;
|
|
17441
17826
|
if (member.kind !== "method" || member.static) continue;
|
|
17442
17827
|
if (member.accessibility === "private" || member.accessibility === "protected") continue;
|
|
17443
17828
|
if (member.key.type === AST_NODE_TYPES73.PrivateIdentifier) continue;
|
|
17444
|
-
|
|
17445
|
-
else names.push("\u2026");
|
|
17829
|
+
names.push(declaredMemberName(member) ?? "\u2026");
|
|
17446
17830
|
}
|
|
17447
17831
|
return names;
|
|
17448
17832
|
};
|
|
@@ -17483,6 +17867,11 @@ function localClassAbstractness(program) {
|
|
|
17483
17867
|
}
|
|
17484
17868
|
return classes;
|
|
17485
17869
|
}
|
|
17870
|
+
function declaredMemberName(member) {
|
|
17871
|
+
if (!member.computed && member.key.type === AST_NODE_TYPES73.Identifier) return member.key.name;
|
|
17872
|
+
if (member.key.type === AST_NODE_TYPES73.Literal && typeof member.key.value === "string") return member.key.value;
|
|
17873
|
+
return null;
|
|
17874
|
+
}
|
|
17486
17875
|
function localInterfaceSurfaces(program) {
|
|
17487
17876
|
const interfaces = /* @__PURE__ */ new Map();
|
|
17488
17877
|
const parents = /* @__PURE__ */ new Map();
|
|
@@ -17505,14 +17894,15 @@ function localInterfaceSurfaces(program) {
|
|
|
17505
17894
|
if (part.type !== AST_NODE_TYPES73.TSTypeLiteral) continue;
|
|
17506
17895
|
for (const member of part.members) {
|
|
17507
17896
|
if (member.type !== AST_NODE_TYPES73.TSMethodSignature && member.type !== AST_NODE_TYPES73.TSPropertySignature) continue;
|
|
17508
|
-
|
|
17897
|
+
const name = declaredMemberName(member);
|
|
17898
|
+
if (name === null) continue;
|
|
17509
17899
|
if (member.type === AST_NODE_TYPES73.TSMethodSignature) {
|
|
17510
|
-
callables2.add(
|
|
17900
|
+
callables2.add(name);
|
|
17511
17901
|
continue;
|
|
17512
17902
|
}
|
|
17513
17903
|
if (member.type !== AST_NODE_TYPES73.TSPropertySignature) continue;
|
|
17514
17904
|
const annotation = member.typeAnnotation?.typeAnnotation;
|
|
17515
|
-
if (annotation?.type === AST_NODE_TYPES73.TSFunctionType || annotation?.type === AST_NODE_TYPES73.TSTypeReference && annotation.typeName.type === AST_NODE_TYPES73.Identifier && functionAliases.has(annotation.typeName.name)) callables2.add(
|
|
17905
|
+
if (annotation?.type === AST_NODE_TYPES73.TSFunctionType || annotation?.type === AST_NODE_TYPES73.TSTypeReference && annotation.typeName.type === AST_NODE_TYPES73.Identifier && functionAliases.has(annotation.typeName.name)) callables2.add(name);
|
|
17516
17906
|
}
|
|
17517
17907
|
}
|
|
17518
17908
|
interfaces.set(declaration.id.name, callables2);
|
|
@@ -17523,8 +17913,9 @@ function localInterfaceSurfaces(program) {
|
|
|
17523
17913
|
const callables = interfaces.get(declaration.id.name) ?? /* @__PURE__ */ new Set();
|
|
17524
17914
|
for (const member of declaration.body.body) {
|
|
17525
17915
|
if (member.type !== AST_NODE_TYPES73.TSMethodSignature && member.type !== AST_NODE_TYPES73.TSPropertySignature) continue;
|
|
17526
|
-
|
|
17527
|
-
if (
|
|
17916
|
+
const name = declaredMemberName(member);
|
|
17917
|
+
if (name === null) continue;
|
|
17918
|
+
if (member.type === AST_NODE_TYPES73.TSMethodSignature || member.typeAnnotation?.typeAnnotation.type === AST_NODE_TYPES73.TSFunctionType || member.typeAnnotation?.typeAnnotation.type === AST_NODE_TYPES73.TSTypeReference && member.typeAnnotation.typeAnnotation.typeName.type === AST_NODE_TYPES73.Identifier && functionAliases.has(member.typeAnnotation.typeAnnotation.typeName.name)) callables.add(name);
|
|
17528
17919
|
}
|
|
17529
17920
|
interfaces.set(declaration.id.name, callables);
|
|
17530
17921
|
parents.set(
|
|
@@ -17649,7 +18040,7 @@ var require_port_for_service_default = createRule({
|
|
|
17649
18040
|
});
|
|
17650
18041
|
|
|
17651
18042
|
// src/rules/require-sql-access-class.ts
|
|
17652
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES74 } from "@typescript-eslint/utils";
|
|
18043
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES74, ASTUtils as ASTUtils41 } from "@typescript-eslint/utils";
|
|
17653
18044
|
var DIRECT_EXECUTION_METHODS = /* @__PURE__ */ new Set([
|
|
17654
18045
|
"all",
|
|
17655
18046
|
"batch",
|
|
@@ -17703,12 +18094,13 @@ var MODEL_EXECUTION_METHODS = /* @__PURE__ */ new Set([
|
|
|
17703
18094
|
var DATABASE_NAMES = /^(?:db|database|connection|pool|prisma|query|transaction|tx)$/iu;
|
|
17704
18095
|
var REQUIRE_SQL_ACCESS_CLASS_DOCUMENTATION = {
|
|
17705
18096
|
summary: "Keep SQL reads and writes inside a class that receives its database dependency.",
|
|
17706
|
-
rationale: "
|
|
18097
|
+
rationale: "An injected repository class is the preferred ownership boundary for database access under this architectural policy; free functions can also express explicit dependencies.",
|
|
17707
18098
|
remediation: "Move the query into a repository or store class and inject the pool, connection, transaction, or typed database binding through its constructor.",
|
|
17708
18099
|
category: "architecture",
|
|
17709
18100
|
limitations: [
|
|
17710
18101
|
"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.",
|
|
17711
18102
|
"Query construction without a recognized execution terminal is intentionally not reported.",
|
|
18103
|
+
"Stable local Map, WeakMap, and URLSearchParams instances are excluded. Other conventional receiver names are heuristics, not proof of a database API.",
|
|
17712
18104
|
"Constructor injection inherited from a base class or transformed through a wrapper is not inferred by this syntax-only rule."
|
|
17713
18105
|
],
|
|
17714
18106
|
examples: [
|
|
@@ -17914,11 +18306,23 @@ var require_sql_access_class_default = createRule({
|
|
|
17914
18306
|
create(context) {
|
|
17915
18307
|
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text))
|
|
17916
18308
|
return {};
|
|
18309
|
+
function knownNonDatabase(node, seen = /* @__PURE__ */ new Set()) {
|
|
18310
|
+
if (seen.has(node)) return false;
|
|
18311
|
+
seen.add(node);
|
|
18312
|
+
if (node.type === AST_NODE_TYPES74.Identifier) {
|
|
18313
|
+
const binding = ASTUtils41.findVariable(context.sourceCode.getScope(node), node.name);
|
|
18314
|
+
if (binding?.defs.length !== 1 || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
18315
|
+
const definition = binding.defs[0];
|
|
18316
|
+
return definition?.type === "Variable" && definition.node.init !== null && knownNonDatabase(definition.node.init, seen);
|
|
18317
|
+
}
|
|
18318
|
+
return node.type === AST_NODE_TYPES74.NewExpression && node.callee.type === AST_NODE_TYPES74.Identifier && ["Map", "WeakMap", "URLSearchParams"].includes(node.callee.name) && (ASTUtils41.findVariable(context.sourceCode.getScope(node.callee), node.callee.name)?.defs.length ?? 0) === 0;
|
|
18319
|
+
}
|
|
17917
18320
|
return {
|
|
17918
18321
|
CallExpression(node) {
|
|
17919
18322
|
if (node.callee.type !== AST_NODE_TYPES74.MemberExpression)
|
|
17920
18323
|
return;
|
|
17921
18324
|
const method = memberName6(node.callee);
|
|
18325
|
+
if (knownNonDatabase(node.callee.object)) return;
|
|
17922
18326
|
if (method === null || !isDatabaseOperation(method, node.callee.object))
|
|
17923
18327
|
return;
|
|
17924
18328
|
const owner = owningClass2(node);
|
|
@@ -17939,6 +18343,8 @@ var REQUIRE_STATIC_NEXT_MATCHER_DOCUMENTATION = {
|
|
|
17939
18343
|
rationale: "Next.js must statically analyze matcher values at build time; computed values are ignored.",
|
|
17940
18344
|
remediation: "Write matcher strings, arrays, and object fields as literals in the exported config.",
|
|
17941
18345
|
category: "correctness",
|
|
18346
|
+
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."],
|
|
18347
|
+
references: ["https://nextjs.org/docs/app/api-reference/file-conventions/proxy"],
|
|
17942
18348
|
examples: [
|
|
17943
18349
|
{ 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 },
|
|
17944
18350
|
{ 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 }
|
|
@@ -17986,7 +18392,7 @@ var require_static_next_matcher_default = createRule({
|
|
|
17986
18392
|
},
|
|
17987
18393
|
schema: [],
|
|
17988
18394
|
messages: {
|
|
17989
|
-
dynamicMatcher: "Next.js matcher
|
|
18395
|
+
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."
|
|
17990
18396
|
}
|
|
17991
18397
|
},
|
|
17992
18398
|
defaultOptions: [],
|
|
@@ -18022,7 +18428,7 @@ var require_static_next_matcher_default = createRule({
|
|
|
18022
18428
|
});
|
|
18023
18429
|
|
|
18024
18430
|
// src/rules/require-use-form-default-values.ts
|
|
18025
|
-
import { ASTUtils as
|
|
18431
|
+
import { ASTUtils as ASTUtils42 } from "@typescript-eslint/utils";
|
|
18026
18432
|
var REQUIRE_USE_FORM_DEFAULT_VALUES_DOCUMENTATION = {
|
|
18027
18433
|
summary: "react-hook-form useForm call without explicit initial or reactive values",
|
|
18028
18434
|
rationale: "Without an explicit initial value, fields can change from uncontrolled to controlled as data arrives, reset behavior becomes ambiguous, and the form's initial shape no longer documents the values users can edit.",
|
|
@@ -18076,13 +18482,13 @@ var require_use_form_default_values_default = createRule({
|
|
|
18076
18482
|
if (node.source.value !== "react-hook-form") return;
|
|
18077
18483
|
for (const specifier of node.specifiers) {
|
|
18078
18484
|
if (specifier.type !== "ImportSpecifier" || (specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value) !== "useForm") continue;
|
|
18079
|
-
const variable =
|
|
18485
|
+
const variable = ASTUtils42.findVariable(context.sourceCode.getScope(specifier.local), specifier.local.name);
|
|
18080
18486
|
if (variable) importedHooks.add(variable);
|
|
18081
18487
|
}
|
|
18082
18488
|
},
|
|
18083
18489
|
CallExpression(node) {
|
|
18084
18490
|
if (node.callee.type !== "Identifier") return;
|
|
18085
|
-
const variable =
|
|
18491
|
+
const variable = ASTUtils42.findVariable(context.sourceCode.getScope(node.callee), node.callee.name);
|
|
18086
18492
|
const options = node.arguments[0];
|
|
18087
18493
|
if (!variable || !importedHooks.has(variable) || options !== void 0 && options.type !== "ObjectExpression" || hasInitializationOrUnknownOptions(options)) return;
|
|
18088
18494
|
context.report({ node, messageId: "requireUseFormDefaultValues" });
|
|
@@ -18161,7 +18567,7 @@ var require_use_server_in_actions_file_default = createRule({
|
|
|
18161
18567
|
// src/rules/require-zod-form-validation.ts
|
|
18162
18568
|
import {
|
|
18163
18569
|
AST_NODE_TYPES as AST_NODE_TYPES76,
|
|
18164
|
-
ASTUtils as
|
|
18570
|
+
ASTUtils as ASTUtils43
|
|
18165
18571
|
} from "@typescript-eslint/utils";
|
|
18166
18572
|
var REQUIRE_ZOD_FORM_VALIDATION_DOCUMENTATION = {
|
|
18167
18573
|
summary: "Require Zod validation (`Schema.parse(...)` / `Schema.safeParse(...)`) when reading values out of a `FormData` object.",
|
|
@@ -18170,7 +18576,8 @@ var REQUIRE_ZOD_FORM_VALIDATION_DOCUMENTATION = {
|
|
|
18170
18576
|
category: "security",
|
|
18171
18577
|
limitations: [
|
|
18172
18578
|
"Tests are excluded; imported schema-shaped names are trusted when their implementation is outside the linted file.",
|
|
18173
|
-
"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."
|
|
18579
|
+
"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.",
|
|
18580
|
+
"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."
|
|
18174
18581
|
],
|
|
18175
18582
|
examples: [
|
|
18176
18583
|
{ 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 },
|
|
@@ -18229,7 +18636,7 @@ var require_zod_form_validation_default = createRule({
|
|
|
18229
18636
|
return {};
|
|
18230
18637
|
}
|
|
18231
18638
|
const zodBindings = /* @__PURE__ */ new Set();
|
|
18232
|
-
const resolvedBinding = (identifier) =>
|
|
18639
|
+
const resolvedBinding = (identifier) => ASTUtils43.findVariable(
|
|
18233
18640
|
context.sourceCode.getScope(identifier),
|
|
18234
18641
|
identifier.name
|
|
18235
18642
|
);
|
|
@@ -18285,6 +18692,15 @@ var require_zod_form_validation_default = createRule({
|
|
|
18285
18692
|
let parent = node.parent;
|
|
18286
18693
|
while (parent !== null && parent !== void 0) {
|
|
18287
18694
|
if (isZodParseCall(parent)) return parent;
|
|
18695
|
+
if (parent.type === AST_NODE_TYPES76.CallExpression && parent.callee.type === AST_NODE_TYPES76.Identifier && ["Number", "String", "Boolean"].includes(parent.callee.name) && parent.arguments.length === 1 && (resolvedBinding(parent.callee)?.defs.length ?? 0) === 0) {
|
|
18696
|
+
parent = parent.parent;
|
|
18697
|
+
continue;
|
|
18698
|
+
}
|
|
18699
|
+
if (parent.type === AST_NODE_TYPES76.CallExpression && parent.callee.type === AST_NODE_TYPES76.MemberExpression && !parent.callee.computed && parent.callee.object.type === AST_NODE_TYPES76.Identifier && parent.callee.object.name === "Object" && parent.callee.property.type === AST_NODE_TYPES76.Identifier && parent.callee.property.name === "fromEntries" && (resolvedBinding(parent.callee.object)?.defs.length ?? 0) === 0) {
|
|
18700
|
+
parent = parent.parent;
|
|
18701
|
+
continue;
|
|
18702
|
+
}
|
|
18703
|
+
if (parent.type === AST_NODE_TYPES76.CallExpression || parent.type === AST_NODE_TYPES76.NewExpression || parent.type === AST_NODE_TYPES76.TaggedTemplateExpression || parent.type === AST_NODE_TYPES76.ArrowFunctionExpression || parent.type === AST_NODE_TYPES76.FunctionExpression || parent.type === AST_NODE_TYPES76.FunctionDeclaration) return null;
|
|
18288
18704
|
parent = parent.parent;
|
|
18289
18705
|
}
|
|
18290
18706
|
return null;
|
|
@@ -18464,13 +18880,14 @@ var require_zod_form_validation_default = createRule({
|
|
|
18464
18880
|
// src/rules/store-insert-requires-on-conflict.ts
|
|
18465
18881
|
import "@typescript-eslint/utils";
|
|
18466
18882
|
var STORE_INSERT_REQUIRES_ON_CONFLICT_DOCUMENTATION = {
|
|
18467
|
-
summary: "
|
|
18468
|
-
rationale: "
|
|
18469
|
-
remediation: "
|
|
18883
|
+
summary: "Review conflict handling for embedded inserts in replay-named callables.",
|
|
18884
|
+
rationale: "Names such as seed, enqueue, or upsert suggest that repeated execution deserves a conflict-policy review, but do not prove a replay contract.",
|
|
18885
|
+
remediation: "Choose conflict handling appropriate to the schema and SQL dialect, or document why this insertion must fail on a duplicate.",
|
|
18470
18886
|
category: "correctness",
|
|
18887
|
+
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."],
|
|
18471
18888
|
examples: [
|
|
18472
|
-
{ id: "conflict-safe-insert", title: "
|
|
18473
|
-
{ id: "bare-insert", title: "
|
|
18889
|
+
{ 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 },
|
|
18890
|
+
{ 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 }
|
|
18474
18891
|
]
|
|
18475
18892
|
};
|
|
18476
18893
|
var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
|
|
@@ -18482,14 +18899,18 @@ function owningCallableName(node) {
|
|
|
18482
18899
|
return current.id?.name ?? null;
|
|
18483
18900
|
}
|
|
18484
18901
|
if (current.type === "MethodDefinition") {
|
|
18485
|
-
return current.key.type === "Identifier" ? current.key.name : null;
|
|
18902
|
+
return !current.computed && current.key.type === "Identifier" ? current.key.name : null;
|
|
18486
18903
|
}
|
|
18487
18904
|
if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "VariableDeclarator" && current.parent.id.type === "Identifier") {
|
|
18488
18905
|
return current.parent.id.name;
|
|
18489
18906
|
}
|
|
18490
|
-
if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "Property" && current.parent.key.type === "Identifier") {
|
|
18907
|
+
if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "Property" && !current.parent.computed && current.parent.key.type === "Identifier") {
|
|
18491
18908
|
return current.parent.key.name;
|
|
18492
18909
|
}
|
|
18910
|
+
if (current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") {
|
|
18911
|
+
const parent = current.parent;
|
|
18912
|
+
return parent.type === "MethodDefinition" && !parent.computed && parent.key.type === "Identifier" ? parent.key.name : null;
|
|
18913
|
+
}
|
|
18493
18914
|
}
|
|
18494
18915
|
return null;
|
|
18495
18916
|
}
|
|
@@ -18500,11 +18921,11 @@ var store_insert_requires_on_conflict_default = createRule({
|
|
|
18500
18921
|
meta: {
|
|
18501
18922
|
type: "problem",
|
|
18502
18923
|
docs: {
|
|
18503
|
-
description: "
|
|
18924
|
+
description: "Review conflict handling for embedded inserts in replay-named callables."
|
|
18504
18925
|
},
|
|
18505
18926
|
schema: [],
|
|
18506
18927
|
messages: {
|
|
18507
|
-
storeInsertRequiresOnConflict: "This INSERT is
|
|
18928
|
+
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."
|
|
18508
18929
|
}
|
|
18509
18930
|
},
|
|
18510
18931
|
defaultOptions: [],
|
|
@@ -18517,7 +18938,7 @@ var store_insert_requires_on_conflict_default = createRule({
|
|
|
18517
18938
|
return;
|
|
18518
18939
|
}
|
|
18519
18940
|
const owner = owningCallableName(node);
|
|
18520
|
-
if (owner
|
|
18941
|
+
if (owner === null || !REPLAY_CONTRACT_NAME.test(owner)) {
|
|
18521
18942
|
return;
|
|
18522
18943
|
}
|
|
18523
18944
|
context.report({ node, messageId: "storeInsertRequiresOnConflict" });
|
|
@@ -18526,19 +18947,17 @@ var store_insert_requires_on_conflict_default = createRule({
|
|
|
18526
18947
|
});
|
|
18527
18948
|
|
|
18528
18949
|
// src/rules/stepdown.ts
|
|
18529
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES77, ASTUtils as
|
|
18950
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES77, ASTUtils as ASTUtils44 } from "@typescript-eslint/utils";
|
|
18530
18951
|
var STEPDOWN_DOCUMENTATION = {
|
|
18531
18952
|
summary: "Place a private helper below its sole direct same-scope caller.",
|
|
18532
18953
|
rationale: "Caller-first ordering lets a reader follow the main flow before descending into implementation details.",
|
|
18533
|
-
remediation: "
|
|
18954
|
+
remediation: "Consider moving the private helper below its sole caller after reviewing initialization and reflection dependencies.",
|
|
18534
18955
|
category: "maintainability",
|
|
18535
|
-
autofix: "safe",
|
|
18536
18956
|
limitations: [
|
|
18537
18957
|
"Generated and test files, cycles, dynamic references, overload targets, and helpers with multiple callers are excluded.",
|
|
18538
|
-
"Class helpers must be private; their sole caller may be public, protected, or private
|
|
18958
|
+
"Class helpers must be private; their sole caller may be public, protected, or private.",
|
|
18539
18959
|
"Runtime class-field, static-block, computed-member, and decorator barriers are never crossed.",
|
|
18540
|
-
"
|
|
18541
|
-
"Overlapping helper chains remain report-only so ESLint never leaves a partially reordered class after exhausting its fix-pass limit."
|
|
18960
|
+
"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."
|
|
18542
18961
|
],
|
|
18543
18962
|
examples: [
|
|
18544
18963
|
{ 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 },
|
|
@@ -18548,7 +18967,7 @@ var STEPDOWN_DOCUMENTATION = {
|
|
|
18548
18967
|
function isFunction(node) {
|
|
18549
18968
|
return node.type === AST_NODE_TYPES77.ArrowFunctionExpression || node.type === AST_NODE_TYPES77.FunctionDeclaration || node.type === AST_NODE_TYPES77.FunctionExpression;
|
|
18550
18969
|
}
|
|
18551
|
-
function reportMisordered(context, candidates, scopeDefinitions, calls, pinned, canMove = () => true
|
|
18970
|
+
function reportMisordered(context, candidates, scopeDefinitions, calls, pinned, canMove = () => true) {
|
|
18552
18971
|
const byName = new Map(scopeDefinitions.map((definition) => [definition.name, definition]));
|
|
18553
18972
|
const cycles = cycleComponents(calls);
|
|
18554
18973
|
const callers = /* @__PURE__ */ new Map();
|
|
@@ -18567,12 +18986,10 @@ function reportMisordered(context, candidates, scopeDefinitions, calls, pinned,
|
|
|
18567
18986
|
if (callerName2 === void 0 || cycles.has(helper.name) && cycles.get(helper.name) === cycles.get(callerName2)) continue;
|
|
18568
18987
|
const caller = byName.get(callerName2);
|
|
18569
18988
|
if (caller === void 0 || helper.node.range[0] >= caller.node.range[0] || !canMove(helper, caller)) continue;
|
|
18570
|
-
const fix = makeFix?.(helper, caller);
|
|
18571
18989
|
context.report({
|
|
18572
18990
|
node: helper.node,
|
|
18573
18991
|
messageId: "helperAboveOnlyCaller",
|
|
18574
|
-
data: { helper: helper.name, caller: callerName2 }
|
|
18575
|
-
...fix === void 0 ? {} : { fix }
|
|
18992
|
+
data: { helper: helper.name, caller: callerName2 }
|
|
18576
18993
|
});
|
|
18577
18994
|
}
|
|
18578
18995
|
}
|
|
@@ -18731,7 +19148,7 @@ function methodName(node) {
|
|
|
18731
19148
|
return !node.computed && node.key.type === AST_NODE_TYPES77.Identifier ? node.key.name : null;
|
|
18732
19149
|
}
|
|
18733
19150
|
function referencedMethod(context, node, classVariables) {
|
|
18734
|
-
const objectVariable = node.object.type === AST_NODE_TYPES77.Identifier ?
|
|
19151
|
+
const objectVariable = node.object.type === AST_NODE_TYPES77.Identifier ? ASTUtils44.findVariable(context.sourceCode.getScope(node.object), node.object.name) : null;
|
|
18735
19152
|
const isClassReference = objectVariable !== null && classVariables.has(objectVariable);
|
|
18736
19153
|
if (node.object.type !== AST_NODE_TYPES77.ThisExpression && !isClassReference) return null;
|
|
18737
19154
|
if (node.property.type === AST_NODE_TYPES77.PrivateIdentifier) return `#${node.property.name}`;
|
|
@@ -18743,15 +19160,15 @@ function referencedPropertyName(node) {
|
|
|
18743
19160
|
if (!node.computed && node.property.type === AST_NODE_TYPES77.Identifier) return node.property.name;
|
|
18744
19161
|
return node.computed && node.property.type === AST_NODE_TYPES77.Literal && typeof node.property.value === "string" ? node.property.value : null;
|
|
18745
19162
|
}
|
|
18746
|
-
function
|
|
19163
|
+
function walk(node, visitorKeys, visit, nestedFunction = false) {
|
|
18747
19164
|
visit(node, nestedFunction);
|
|
18748
19165
|
const nested = nestedFunction || isFunction(node);
|
|
18749
19166
|
for (const key of visitorKeys[node.type] ?? []) {
|
|
18750
19167
|
const child = node[key];
|
|
18751
19168
|
if (Array.isArray(child)) {
|
|
18752
|
-
for (const item of child) if (typeof item === "object" && item !== null && "type" in item)
|
|
19169
|
+
for (const item of child) if (typeof item === "object" && item !== null && "type" in item) walk(item, visitorKeys, visit, nested);
|
|
18753
19170
|
} else if (typeof child === "object" && child !== null && "type" in child) {
|
|
18754
|
-
|
|
19171
|
+
walk(child, visitorKeys, visit, nested);
|
|
18755
19172
|
}
|
|
18756
19173
|
}
|
|
18757
19174
|
}
|
|
@@ -18784,11 +19201,11 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18784
19201
|
const pinned = /* @__PURE__ */ new Set();
|
|
18785
19202
|
const classVariables = /* @__PURE__ */ new Set();
|
|
18786
19203
|
if (node.id !== null) {
|
|
18787
|
-
const internal =
|
|
19204
|
+
const internal = ASTUtils44.findVariable(context.sourceCode.getScope(node), node.id.name);
|
|
18788
19205
|
if (internal !== null) classVariables.add(internal);
|
|
18789
19206
|
}
|
|
18790
19207
|
if (node.type === AST_NODE_TYPES77.ClassExpression && node.parent.type === AST_NODE_TYPES77.VariableDeclarator && node.parent.id.type === AST_NODE_TYPES77.Identifier) {
|
|
18791
|
-
const outer =
|
|
19208
|
+
const outer = ASTUtils44.findVariable(context.sourceCode.getScope(node.parent), node.parent.id.name);
|
|
18792
19209
|
if (outer !== null) classVariables.add(outer);
|
|
18793
19210
|
}
|
|
18794
19211
|
for (const method of methods) {
|
|
@@ -18799,7 +19216,7 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18799
19216
|
const parameterDecoratorNodes = /* @__PURE__ */ new Set();
|
|
18800
19217
|
for (const parameter of method.value.params) {
|
|
18801
19218
|
for (const decorator of parameter.decorators) {
|
|
18802
|
-
|
|
19219
|
+
walk(decorator, context.sourceCode.visitorKeys, (current) => parameterDecoratorNodes.add(current));
|
|
18803
19220
|
}
|
|
18804
19221
|
}
|
|
18805
19222
|
const thisValue = (value) => {
|
|
@@ -18824,17 +19241,17 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18824
19241
|
return;
|
|
18825
19242
|
}
|
|
18826
19243
|
if (binding.type !== AST_NODE_TYPES77.Identifier) return;
|
|
18827
|
-
const variable =
|
|
19244
|
+
const variable = ASTUtils44.findVariable(context.sourceCode.getScope(binding), binding.name);
|
|
18828
19245
|
if (variable !== null) {
|
|
18829
19246
|
methodClassVariables.add(variable);
|
|
18830
19247
|
methodAliases.add(variable);
|
|
18831
19248
|
}
|
|
18832
19249
|
};
|
|
18833
19250
|
for (const parameter of method.value.params) {
|
|
18834
|
-
|
|
19251
|
+
walk(parameter, context.sourceCode.visitorKeys, collectAlias);
|
|
18835
19252
|
}
|
|
18836
19253
|
for (const statement of method.value.body.body) {
|
|
18837
|
-
|
|
19254
|
+
walk(statement, context.sourceCode.visitorKeys, collectAlias);
|
|
18838
19255
|
}
|
|
18839
19256
|
const visitCall = (current, nestedFunction) => {
|
|
18840
19257
|
if (current.type === AST_NODE_TYPES77.VariableDeclarator && current.id.type === AST_NODE_TYPES77.ObjectPattern && thisValue(current.init)) {
|
|
@@ -18854,7 +19271,7 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18854
19271
|
return;
|
|
18855
19272
|
}
|
|
18856
19273
|
if (!privateNames.has(target)) return;
|
|
18857
|
-
const objectVariable = current.object.type === AST_NODE_TYPES77.Identifier ?
|
|
19274
|
+
const objectVariable = current.object.type === AST_NODE_TYPES77.Identifier ? ASTUtils44.findVariable(context.sourceCode.getScope(current.object), current.object.name) : null;
|
|
18858
19275
|
if (objectVariable !== null && methodAliases.has(objectVariable)) {
|
|
18859
19276
|
pinned.add(target);
|
|
18860
19277
|
return;
|
|
@@ -18868,19 +19285,19 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18868
19285
|
calls.set(caller, callees);
|
|
18869
19286
|
};
|
|
18870
19287
|
for (const decorator of method.decorators) {
|
|
18871
|
-
|
|
19288
|
+
walk(decorator, context.sourceCode.visitorKeys, visitCall, true);
|
|
18872
19289
|
}
|
|
18873
|
-
if (method.computed)
|
|
19290
|
+
if (method.computed) walk(method.key, context.sourceCode.visitorKeys, visitCall, true);
|
|
18874
19291
|
for (const parameter of method.value.params) {
|
|
18875
|
-
|
|
19292
|
+
walk(parameter, context.sourceCode.visitorKeys, visitCall);
|
|
18876
19293
|
}
|
|
18877
19294
|
for (const statement of method.value.body.body) {
|
|
18878
|
-
|
|
19295
|
+
walk(statement, context.sourceCode.visitorKeys, visitCall);
|
|
18879
19296
|
}
|
|
18880
19297
|
}
|
|
18881
19298
|
for (const member of node.body.body) {
|
|
18882
19299
|
if (member.type === AST_NODE_TYPES77.MethodDefinition || member.type === AST_NODE_TYPES77.TSAbstractMethodDefinition) continue;
|
|
18883
|
-
|
|
19300
|
+
walk(member, context.sourceCode.visitorKeys, (current) => {
|
|
18884
19301
|
if (current.type !== AST_NODE_TYPES77.MemberExpression) return;
|
|
18885
19302
|
const target = referencedMethod(context, current, classVariables);
|
|
18886
19303
|
const possibleTarget = target ?? referencedPropertyName(current);
|
|
@@ -18899,37 +19316,7 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18899
19316
|
if (helperIndex === void 0 || callerIndex === void 0) return false;
|
|
18900
19317
|
return runtimeBarrierPrefix[callerIndex + 1] === runtimeBarrierPrefix[helperIndex + 1];
|
|
18901
19318
|
};
|
|
18902
|
-
|
|
18903
|
-
for (const [caller, callees] of calls) {
|
|
18904
|
-
for (const callee of callees) {
|
|
18905
|
-
if (callee === caller) continue;
|
|
18906
|
-
const callers = incoming.get(callee) ?? /* @__PURE__ */ new Set();
|
|
18907
|
-
callers.add(caller);
|
|
18908
|
-
incoming.set(callee, callers);
|
|
18909
|
-
}
|
|
18910
|
-
}
|
|
18911
|
-
reportMisordered(context, definitions, scopeDefinitions, calls, pinned, canMove, (helper, caller) => {
|
|
18912
|
-
if (!canMove(helper, caller)) return void 0;
|
|
18913
|
-
const helperCallsAnother = [...calls.get(helper.name) ?? []].some((callee) => callee !== helper.name);
|
|
18914
|
-
const callerIsAnotherHelper = [...incoming.get(caller.name) ?? []].some((name) => name !== helper.name);
|
|
18915
|
-
if (helperCallsAnother || callerIsAnotherHelper) return void 0;
|
|
18916
|
-
const helperMember = helper.node;
|
|
18917
|
-
const callerMember = caller.node;
|
|
18918
|
-
const helperIndex = memberIndexes.get(helperMember);
|
|
18919
|
-
if (helperIndex === void 0) return void 0;
|
|
18920
|
-
const next = node.body.body[helperIndex + 1];
|
|
18921
|
-
const suffixEnd = next?.range[0] ?? node.body.range[1] - 1;
|
|
18922
|
-
const suffix = context.sourceCode.text.slice(helperMember.range[1], suffixEnd);
|
|
18923
|
-
if (!/^\s*$/u.test(suffix)) return void 0;
|
|
18924
|
-
const previous = node.body.body[helperIndex - 1];
|
|
18925
|
-
const prefixStart = previous?.range[1] ?? node.body.range[0] + 1;
|
|
18926
|
-
if (!/^\s*$/u.test(context.sourceCode.text.slice(prefixStart, helperMember.range[0]))) return void 0;
|
|
18927
|
-
const helperText = context.sourceCode.getText(helperMember);
|
|
18928
|
-
return (fixer) => [
|
|
18929
|
-
fixer.removeRange([helperMember.range[0], suffixEnd]),
|
|
18930
|
-
fixer.insertTextAfter(callerMember, `${suffix}${helperText}`)
|
|
18931
|
-
];
|
|
18932
|
-
});
|
|
19319
|
+
reportMisordered(context, definitions, scopeDefinitions, calls, pinned, canMove);
|
|
18933
19320
|
}
|
|
18934
19321
|
function isClassRuntimeBarrier(member) {
|
|
18935
19322
|
switch (member.type) {
|
|
@@ -18951,7 +19338,6 @@ var stepdown_default = createRule({
|
|
|
18951
19338
|
type: "suggestion",
|
|
18952
19339
|
docs: { description: "Place a private helper below its sole direct same-scope caller." },
|
|
18953
19340
|
schema: [],
|
|
18954
|
-
fixable: "code",
|
|
18955
19341
|
messages: {
|
|
18956
19342
|
helperAboveOnlyCaller: "Private helper `{{helper}}` is defined above its only caller `{{caller}}`; move it below the caller."
|
|
18957
19343
|
}
|
|
@@ -18970,7 +19356,7 @@ var stepdown_default = createRule({
|
|
|
18970
19356
|
"Program:exit": (program) => {
|
|
18971
19357
|
moduleScope(context, program);
|
|
18972
19358
|
const computedReferenceNames = /* @__PURE__ */ new Set();
|
|
18973
|
-
|
|
19359
|
+
walk(program, context.sourceCode.visitorKeys, (node) => {
|
|
18974
19360
|
if (node.type === AST_NODE_TYPES77.MemberExpression && node.computed && node.property.type === AST_NODE_TYPES77.Literal && typeof node.property.value === "string") computedReferenceNames.add(node.property.value);
|
|
18975
19361
|
});
|
|
18976
19362
|
for (const node of classes) classScope(context, node, computedReferenceNames);
|
|
@@ -18980,7 +19366,7 @@ var stepdown_default = createRule({
|
|
|
18980
19366
|
});
|
|
18981
19367
|
|
|
18982
19368
|
// src/rules/source-coupled-test.ts
|
|
18983
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES78 } from "@typescript-eslint/utils";
|
|
19369
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES78, ASTUtils as ASTUtils45 } from "@typescript-eslint/utils";
|
|
18984
19370
|
var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|jsonc|py|[cm]?[jt]s)$/iu;
|
|
18985
19371
|
var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
|
|
18986
19372
|
var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
|
|
@@ -19024,7 +19410,7 @@ var SOURCE_COUPLED_TEST_DOCUMENTATION = {
|
|
|
19024
19410
|
remediation: "Parse the artifact, execute its validator, or assert on another runtime contract.",
|
|
19025
19411
|
category: "testing",
|
|
19026
19412
|
limitations: [
|
|
19027
|
-
"The rule follows lexical
|
|
19413
|
+
"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.",
|
|
19028
19414
|
"When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."
|
|
19029
19415
|
],
|
|
19030
19416
|
examples: [
|
|
@@ -19063,6 +19449,11 @@ function stringValue(node) {
|
|
|
19063
19449
|
const current = unwrap7(node);
|
|
19064
19450
|
if (current.type === AST_NODE_TYPES78.Literal && typeof current.value === "string") return current.value;
|
|
19065
19451
|
if (current.type === AST_NODE_TYPES78.TemplateLiteral && current.expressions.length === 0) return current.quasis[0]?.value.cooked ?? null;
|
|
19452
|
+
if (current.type === AST_NODE_TYPES78.BinaryExpression && current.operator === "+") {
|
|
19453
|
+
const left = stringValue(current.left);
|
|
19454
|
+
const right = stringValue(current.right);
|
|
19455
|
+
return left === null || right === null ? null : left + right;
|
|
19456
|
+
}
|
|
19066
19457
|
return null;
|
|
19067
19458
|
}
|
|
19068
19459
|
function importSource(node) {
|
|
@@ -19092,14 +19483,19 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19092
19483
|
const scopes = [newScope()];
|
|
19093
19484
|
const reportedOrigins = /* @__PURE__ */ new Set();
|
|
19094
19485
|
const currentScope = () => scopes.at(-1) ?? scopes[0];
|
|
19095
|
-
const
|
|
19486
|
+
const bindingOf = (node) => ASTUtils45.findVariable(context.sourceCode.getScope(node), node.name);
|
|
19487
|
+
const visible = (kind, node) => {
|
|
19488
|
+
const name2 = bindingOf(node);
|
|
19489
|
+
if (name2 === null || name2.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
19096
19490
|
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
19097
19491
|
const scope = scopes[index];
|
|
19098
19492
|
if (scope.declared.has(name2)) return scope[kind].has(name2);
|
|
19099
19493
|
}
|
|
19100
19494
|
return false;
|
|
19101
19495
|
};
|
|
19102
|
-
const visibleRawOrigins = (
|
|
19496
|
+
const visibleRawOrigins = (node) => {
|
|
19497
|
+
const name2 = bindingOf(node);
|
|
19498
|
+
if (name2 === null || name2.references.some((reference) => reference.isWrite() && reference.init !== true)) return /* @__PURE__ */ new Set();
|
|
19103
19499
|
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
19104
19500
|
const scope = scopes[index];
|
|
19105
19501
|
if (scope.declared.has(name2)) return scope.rawOrigins.get(name2) ?? /* @__PURE__ */ new Set();
|
|
@@ -19110,15 +19506,14 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19110
19506
|
const current = unwrap7(node);
|
|
19111
19507
|
const value = stringValue(current);
|
|
19112
19508
|
if (value !== null) return sourceSuffixRe.test(value);
|
|
19113
|
-
if (current.type === AST_NODE_TYPES78.Identifier) return visible("paths", current
|
|
19114
|
-
if (current.type === AST_NODE_TYPES78.BinaryExpression && current.operator === "+") {
|
|
19115
|
-
return sourcePath(current.left) || sourcePath(current.right);
|
|
19116
|
-
}
|
|
19117
|
-
if (current.type === AST_NODE_TYPES78.TemplateLiteral) return current.expressions.some(sourcePath);
|
|
19509
|
+
if (current.type === AST_NODE_TYPES78.Identifier) return visible("paths", current);
|
|
19118
19510
|
if (current.type === AST_NODE_TYPES78.CallExpression || current.type === AST_NODE_TYPES78.NewExpression) {
|
|
19119
|
-
|
|
19511
|
+
const callee = current.callee;
|
|
19512
|
+
const first = current.arguments[0];
|
|
19513
|
+
if (first === void 0 || first.type === AST_NODE_TYPES78.SpreadElement) return false;
|
|
19514
|
+
if (current.type === AST_NODE_TYPES78.NewExpression && callee.type === AST_NODE_TYPES78.Identifier && callee.name === "URL" && (bindingOf(callee)?.defs.length ?? 0) === 0) return sourcePath(first);
|
|
19515
|
+
if (callee.type === AST_NODE_TYPES78.Identifier && bindingOf(callee)?.defs.some((definition) => definition.node.type === AST_NODE_TYPES78.ImportSpecifier && definition.node.imported.type === AST_NODE_TYPES78.Identifier && definition.node.imported.name === "fileURLToPath" && definition.node.parent.type === AST_NODE_TYPES78.ImportDeclaration && ["node:url", "url"].includes(String(definition.node.parent.source.value)))) return sourcePath(first);
|
|
19120
19516
|
}
|
|
19121
|
-
if (current.type === AST_NODE_TYPES78.MemberExpression) return sourcePath(current.object);
|
|
19122
19517
|
return false;
|
|
19123
19518
|
};
|
|
19124
19519
|
const rawRead = (node) => {
|
|
@@ -19126,16 +19521,16 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19126
19521
|
if (current.type !== AST_NODE_TYPES78.CallExpression || current.arguments.length === 0) return false;
|
|
19127
19522
|
const callee = unwrap7(current.callee);
|
|
19128
19523
|
if (callee.type === AST_NODE_TYPES78.Identifier) {
|
|
19129
|
-
return visible("fsReaders", callee
|
|
19524
|
+
return visible("fsReaders", callee) && sourcePath(current.arguments[0]);
|
|
19130
19525
|
}
|
|
19131
19526
|
if (callee.type !== AST_NODE_TYPES78.MemberExpression) return false;
|
|
19132
19527
|
const name2 = staticMemberName7(callee);
|
|
19133
19528
|
const object = unwrap7(callee.object);
|
|
19134
|
-
return name2 !== null && FS_READERS.has(name2) && object.type === AST_NODE_TYPES78.Identifier && visible("fsObjects", object
|
|
19529
|
+
return name2 !== null && FS_READERS.has(name2) && object.type === AST_NODE_TYPES78.Identifier && visible("fsObjects", object) && sourcePath(current.arguments[0]);
|
|
19135
19530
|
};
|
|
19136
19531
|
const rawOrigins = (node) => {
|
|
19137
19532
|
const current = unwrap7(node);
|
|
19138
|
-
if (current.type === AST_NODE_TYPES78.Identifier) return visibleRawOrigins(current
|
|
19533
|
+
if (current.type === AST_NODE_TYPES78.Identifier) return visibleRawOrigins(current);
|
|
19139
19534
|
if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
|
|
19140
19535
|
if (current.type === AST_NODE_TYPES78.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
|
|
19141
19536
|
if (current.type === AST_NODE_TYPES78.MemberExpression && staticMemberName7(current) === "length") return rawOrigins(current.object);
|
|
@@ -19176,14 +19571,9 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19176
19571
|
if (receiver.type !== AST_NODE_TYPES78.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
|
|
19177
19572
|
return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES78.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
19178
19573
|
};
|
|
19179
|
-
const
|
|
19180
|
-
const
|
|
19181
|
-
if (
|
|
19182
|
-
const argument = node.arguments[0];
|
|
19183
|
-
if (argument?.type !== AST_NODE_TYPES78.Literal || !(argument.value instanceof RegExp)) return /* @__PURE__ */ new Set();
|
|
19184
|
-
return rawOrigins(callee.object);
|
|
19185
|
-
};
|
|
19186
|
-
const declare = (name2, state) => {
|
|
19574
|
+
const declare = (node, state) => {
|
|
19575
|
+
const name2 = bindingOf(node);
|
|
19576
|
+
if (name2 === null) return;
|
|
19187
19577
|
const scope = currentScope();
|
|
19188
19578
|
scope.declared.add(name2);
|
|
19189
19579
|
scope.collections.delete(name2);
|
|
@@ -19203,18 +19593,8 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19203
19593
|
const current = unwrap7(node);
|
|
19204
19594
|
return current.type === AST_NODE_TYPES78.ArrayExpression && current.elements.length > 0 && current.elements.every((element) => element !== null && element.type !== AST_NODE_TYPES78.SpreadElement && sourcePath(element));
|
|
19205
19595
|
};
|
|
19206
|
-
const
|
|
19207
|
-
const current = unwrap7(node);
|
|
19208
|
-
if (current.type === AST_NODE_TYPES78.Identifier) return [current.name];
|
|
19209
|
-
if (current.type === AST_NODE_TYPES78.AssignmentPattern) return declaredNames2(current.left);
|
|
19210
|
-
if (current.type === AST_NODE_TYPES78.RestElement) return declaredNames2(current.argument);
|
|
19211
|
-
if (current.type === AST_NODE_TYPES78.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
|
|
19212
|
-
if (current.type === AST_NODE_TYPES78.ObjectPattern) return current.properties.flatMap((property) => property.type === AST_NODE_TYPES78.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
|
|
19213
|
-
return [];
|
|
19214
|
-
};
|
|
19215
|
-
const enterFunction = (node) => {
|
|
19596
|
+
const enterFunction = () => {
|
|
19216
19597
|
scopes.push(newScope());
|
|
19217
|
-
for (const parameter of node.params) for (const name2 of declaredNames2(parameter)) declare(name2, {});
|
|
19218
19598
|
};
|
|
19219
19599
|
const exitFunction = () => {
|
|
19220
19600
|
scopes.pop();
|
|
@@ -19226,9 +19606,9 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19226
19606
|
for (const specifier of node.specifiers) {
|
|
19227
19607
|
if (specifier.type === AST_NODE_TYPES78.ImportSpecifier) {
|
|
19228
19608
|
const imported = specifier.imported.type === AST_NODE_TYPES78.Identifier ? specifier.imported.name : String(specifier.imported.value);
|
|
19229
|
-
if (FS_READERS.has(imported)) declare(specifier.local
|
|
19609
|
+
if (FS_READERS.has(imported)) declare(specifier.local, { fsReader: true });
|
|
19230
19610
|
} else {
|
|
19231
|
-
declare(specifier.local
|
|
19611
|
+
declare(specifier.local, { fsObject: true });
|
|
19232
19612
|
}
|
|
19233
19613
|
}
|
|
19234
19614
|
},
|
|
@@ -19237,35 +19617,34 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19237
19617
|
VariableDeclarator(node) {
|
|
19238
19618
|
if (node.init === null) return;
|
|
19239
19619
|
const required = requireSource(node.init);
|
|
19620
|
+
const initializer = unwrap7(node.init);
|
|
19621
|
+
if (required !== null && initializer.type === AST_NODE_TYPES78.CallExpression && initializer.callee.type === AST_NODE_TYPES78.Identifier && (bindingOf(initializer.callee)?.defs.length ?? 0) > 0) return;
|
|
19240
19622
|
if (required !== null && FS_MODULES.has(required) && node.id.type === AST_NODE_TYPES78.Identifier) {
|
|
19241
|
-
declare(node.id
|
|
19623
|
+
declare(node.id, { fsObject: true });
|
|
19242
19624
|
return;
|
|
19243
19625
|
}
|
|
19244
19626
|
if (node.id.type === AST_NODE_TYPES78.ObjectPattern && required !== null && FS_MODULES.has(required)) {
|
|
19245
19627
|
for (const property of node.id.properties) {
|
|
19246
19628
|
if (property.type !== AST_NODE_TYPES78.Property || property.value.type !== AST_NODE_TYPES78.Identifier) continue;
|
|
19247
19629
|
const key = property.key.type === AST_NODE_TYPES78.Identifier ? property.key.name : property.key.type === AST_NODE_TYPES78.Literal ? String(property.key.value) : "";
|
|
19248
|
-
if (FS_READERS.has(key)) declare(property.value
|
|
19630
|
+
if (FS_READERS.has(key)) declare(property.value, { fsReader: true });
|
|
19249
19631
|
}
|
|
19250
19632
|
return;
|
|
19251
19633
|
}
|
|
19252
19634
|
if (node.id.type !== AST_NODE_TYPES78.Identifier) return;
|
|
19253
|
-
declare(node.id
|
|
19635
|
+
declare(node.id, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
|
|
19254
19636
|
},
|
|
19255
19637
|
AssignmentExpression(node) {
|
|
19256
|
-
if (node.left.type === AST_NODE_TYPES78.Identifier) declare(node.left
|
|
19638
|
+
if (node.left.type === AST_NODE_TYPES78.Identifier) declare(node.left, {});
|
|
19257
19639
|
},
|
|
19258
19640
|
ForOfStatement(node) {
|
|
19259
19641
|
const right = unwrap7(node.right);
|
|
19260
|
-
const collection = right.type === AST_NODE_TYPES78.Identifier && visible("collections", right
|
|
19642
|
+
const collection = right.type === AST_NODE_TYPES78.Identifier && visible("collections", right);
|
|
19261
19643
|
const left = node.left.type === AST_NODE_TYPES78.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
|
|
19262
|
-
if (collection && left?.type === AST_NODE_TYPES78.Identifier) declare(left
|
|
19644
|
+
if (collection && left?.type === AST_NODE_TYPES78.Identifier) declare(left, { path: true });
|
|
19263
19645
|
},
|
|
19264
19646
|
CallExpression(node) {
|
|
19265
|
-
const origins =
|
|
19266
|
-
...rawAssertionOrigins(node),
|
|
19267
|
-
...rawRegexExtractionOrigins(node)
|
|
19268
|
-
]);
|
|
19647
|
+
const origins = rawAssertionOrigins(node);
|
|
19269
19648
|
if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
|
|
19270
19649
|
for (const origin of origins) reportedOrigins.add(origin);
|
|
19271
19650
|
context.report({ node, messageId: "rawSourceOracle" });
|
|
@@ -19408,8 +19787,8 @@ var IAC_SOURCE_COUPLED_TEST_DOCUMENTATION = {
|
|
|
19408
19787
|
remediation: "Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
|
|
19409
19788
|
category: "testing",
|
|
19410
19789
|
limitations: [
|
|
19411
|
-
"The rule follows lexical
|
|
19412
|
-
"
|
|
19790
|
+
"The rule follows stable lexical bindings and static source paths. Reassigned bindings, dynamic paths, unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.",
|
|
19791
|
+
"When raw representation is genuinely the contract, use an exact line suppression explaining that contract."
|
|
19413
19792
|
],
|
|
19414
19793
|
examples: [
|
|
19415
19794
|
{
|
|
@@ -19441,7 +19820,7 @@ var iac_source_coupled_test_default = createSourceCoupledRule(
|
|
|
19441
19820
|
// src/rules/require-pascal-case-zod-schema-name.ts
|
|
19442
19821
|
import {
|
|
19443
19822
|
AST_NODE_TYPES as AST_NODE_TYPES80,
|
|
19444
|
-
ASTUtils as
|
|
19823
|
+
ASTUtils as ASTUtils46
|
|
19445
19824
|
} from "@typescript-eslint/utils";
|
|
19446
19825
|
var REQUIRE_PASCAL_CASE_ZOD_SCHEMA_NAME_DOCUMENTATION = {
|
|
19447
19826
|
summary: "Require confirmed module-level Zod schema contracts to use PascalCase with a `Schema` suffix.",
|
|
@@ -19642,7 +20021,7 @@ var require_pascal_case_zod_schema_name_default = createRule({
|
|
|
19642
20021
|
const zodBindings = /* @__PURE__ */ new Set();
|
|
19643
20022
|
const schemaBindings = /* @__PURE__ */ new Set();
|
|
19644
20023
|
function resolvedBinding(identifier) {
|
|
19645
|
-
return
|
|
20024
|
+
return ASTUtils46.findVariable(
|
|
19646
20025
|
context.sourceCode.getScope(identifier),
|
|
19647
20026
|
identifier.name
|
|
19648
20027
|
);
|
|
@@ -19887,7 +20266,7 @@ var RULES = {
|
|
|
19887
20266
|
};
|
|
19888
20267
|
var meta = {
|
|
19889
20268
|
name: "@sarj/eslint-plugin",
|
|
19890
|
-
version: "15.17.
|
|
20269
|
+
version: "15.17.11"
|
|
19891
20270
|
};
|
|
19892
20271
|
var APPLICATION_ONLY_RULES = [];
|
|
19893
20272
|
var LIBRARY_IMPORT_POLICY = ["error", {
|