@sarj/eslint-plugin 15.17.10 → 15.17.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +725 -278
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +816 -369
- package/package.json +1 -1
package/dist/index.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);
|
|
@@ -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) {
|
|
@@ -4549,7 +4614,7 @@ var interface_contract_members_private_default = createRule({
|
|
|
4549
4614
|
});
|
|
4550
4615
|
|
|
4551
4616
|
// src/rules/no-log-only-catch.ts
|
|
4552
|
-
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";
|
|
4553
4618
|
|
|
4554
4619
|
// src/rules/_logging.ts
|
|
4555
4620
|
import "@typescript-eslint/utils";
|
|
@@ -4721,7 +4786,7 @@ function seededFallbackHandled(tryStatement, scope) {
|
|
|
4721
4786
|
if (previous.declarations.length !== 1 || declarator === void 0) return false;
|
|
4722
4787
|
if (declarator.id.type !== AST_NODE_TYPES18.Identifier) return false;
|
|
4723
4788
|
if (declarator.init == null || !isSeedValue(declarator.init)) return false;
|
|
4724
|
-
const variable =
|
|
4789
|
+
const variable = ASTUtils8.findVariable(scope, declarator.id.name);
|
|
4725
4790
|
if (variable === null) return false;
|
|
4726
4791
|
const [tryStart, tryEnd] = tryStatement.block.range;
|
|
4727
4792
|
let writtenInTry = false;
|
|
@@ -4775,6 +4840,39 @@ var no_log_only_catch_default = createRule({
|
|
|
4775
4840
|
const matcher = createLogMatcher(loggingOptions);
|
|
4776
4841
|
const filename = context.filename;
|
|
4777
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
|
+
}
|
|
4778
4876
|
function isLoggingCallStatement(statement) {
|
|
4779
4877
|
if (statement.type !== "ExpressionStatement") {
|
|
4780
4878
|
return false;
|
|
@@ -4801,17 +4899,11 @@ var no_log_only_catch_default = createRule({
|
|
|
4801
4899
|
CatchClause(node) {
|
|
4802
4900
|
const statements = node.body.body;
|
|
4803
4901
|
const isDocumented = sourceCode.getCommentsInside(node.body).length > 0 || hasAdjacentRationale(node);
|
|
4804
|
-
if (
|
|
4805
|
-
if (isDocumented) {
|
|
4806
|
-
return;
|
|
4807
|
-
}
|
|
4808
|
-
if (fallbackFollowsTry(node.parent) || seededFallbackHandled(node.parent, sourceCode.getScope(node))) {
|
|
4809
|
-
return;
|
|
4810
|
-
}
|
|
4811
|
-
context.report({ node, messageId: "emptyCatch" });
|
|
4902
|
+
if (isDocumented || hasCoercionValidation(node) || fallbackFollowsTry(node.parent) || seededFallbackHandled(node.parent, sourceCode.getScope(node))) {
|
|
4812
4903
|
return;
|
|
4813
4904
|
}
|
|
4814
|
-
if (
|
|
4905
|
+
if (statements.length === 0) {
|
|
4906
|
+
context.report({ node, messageId: "emptyCatch" });
|
|
4815
4907
|
return;
|
|
4816
4908
|
}
|
|
4817
4909
|
const everyStatementIsLogging = statements.every(
|
|
@@ -4826,10 +4918,10 @@ var no_log_only_catch_default = createRule({
|
|
|
4826
4918
|
});
|
|
4827
4919
|
|
|
4828
4920
|
// src/rules/no-bare-return-from-test-catch.ts
|
|
4829
|
-
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";
|
|
4830
4922
|
var NO_BARE_RETURN_FROM_TEST_CATCH_DOCUMENTATION = {
|
|
4831
4923
|
summary: "Disallow a bare return from a test catch block when it skips a later assertion.",
|
|
4832
|
-
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.",
|
|
4833
4925
|
remediation: "Rethrow the error, assert on it, or use the runner's explicit skip mechanism when the capability is optional.",
|
|
4834
4926
|
category: "testing",
|
|
4835
4927
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
@@ -4851,7 +4943,7 @@ function staticMemberName2(node) {
|
|
|
4851
4943
|
return null;
|
|
4852
4944
|
}
|
|
4853
4945
|
function importedName3(identifier, context, modules) {
|
|
4854
|
-
const variable =
|
|
4946
|
+
const variable = ASTUtils9.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
4855
4947
|
if (variable === null || variable.defs.length === 0) return identifier.name;
|
|
4856
4948
|
for (const definition of variable.defs) {
|
|
4857
4949
|
if (definition.node.type !== AST_NODE_TYPES19.ImportSpecifier) continue;
|
|
@@ -4916,7 +5008,7 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
4916
5008
|
type: "problem",
|
|
4917
5009
|
docs: { description: "Disallow a bare return from a test catch block when it skips a later assertion." },
|
|
4918
5010
|
schema: [],
|
|
4919
|
-
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." }
|
|
4920
5012
|
},
|
|
4921
5013
|
defaultOptions: [],
|
|
4922
5014
|
create(context) {
|
|
@@ -4935,6 +5027,23 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
4935
5027
|
if (current === null || current === void 0) break;
|
|
4936
5028
|
}
|
|
4937
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
|
+
}
|
|
4938
5047
|
if (walkOwnScope(catchClause.body, (current) => current.type === AST_NODE_TYPES19.ThrowStatement || isExplicitSkip(current, context))) return;
|
|
4939
5048
|
if (!walkOwnScope(owner.body, (current) => current.range[0] > node.range[1] && isAssertion(current, context))) return;
|
|
4940
5049
|
context.report({ node, messageId: "bareReturnFromTestCatch" });
|
|
@@ -4944,15 +5053,15 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
4944
5053
|
});
|
|
4945
5054
|
|
|
4946
5055
|
// src/rules/no-bespoke-api-case-conversion.ts
|
|
4947
|
-
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";
|
|
4948
5057
|
var NO_BESPOKE_API_CASE_CONVERSION_DOCUMENTATION = {
|
|
4949
|
-
summary: "
|
|
4950
|
-
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.",
|
|
4951
5060
|
remediation: "Move wire-name ownership and case conversion into the generated SDK/model layer; keep application adapters on the generated typed surface.",
|
|
4952
5061
|
category: "architecture",
|
|
4953
5062
|
autofix: "none",
|
|
4954
5063
|
limitations: [
|
|
4955
|
-
"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.",
|
|
4956
5065
|
"Only object properties that directly translate the same identifier between snake_case and lowerCamelCase are reported.",
|
|
4957
5066
|
"Quoted/computed protocol keys, generated/vendor code, tests, fixtures, and indirect conversions are intentionally excluded."
|
|
4958
5067
|
],
|
|
@@ -5024,7 +5133,7 @@ var no_bespoke_api_case_conversion_default = createRule({
|
|
|
5024
5133
|
docs: { description: NO_BESPOKE_API_CASE_CONVERSION_DOCUMENTATION.summary },
|
|
5025
5134
|
schema: [],
|
|
5026
5135
|
messages: {
|
|
5027
|
-
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."
|
|
5028
5137
|
}
|
|
5029
5138
|
},
|
|
5030
5139
|
defaultOptions: [],
|
|
@@ -5034,16 +5143,33 @@ var no_bespoke_api_case_conversion_default = createRule({
|
|
|
5034
5143
|
if (!ADAPTER_BASENAME_RE.test(basename) || isGeneratedFile(filename, context.sourceCode.text) || isTestFile(filename, ["fixtureTree"])) {
|
|
5035
5144
|
return {};
|
|
5036
5145
|
}
|
|
5037
|
-
const
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
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
|
+
};
|
|
5041
5166
|
return {
|
|
5042
5167
|
Property(node) {
|
|
5043
5168
|
if (node.computed || node.method || node.shorthand) return;
|
|
5044
5169
|
const key = propertyName(node.key);
|
|
5045
5170
|
const value = memberName3(node.value);
|
|
5046
5171
|
if (key === null || value === null || !isDirectCaseTranslation(key, value)) return;
|
|
5172
|
+
if (!hasApiReceiver(node.value)) return;
|
|
5047
5173
|
const wireName = SNAKE_CASE_RE.test(key) ? key : value;
|
|
5048
5174
|
const applicationName = wireName === key ? value : key;
|
|
5049
5175
|
context.report({
|
|
@@ -5166,7 +5292,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
|
|
|
5166
5292
|
limitations: [
|
|
5167
5293
|
"Only ESLint disable comments and TypeScript expect-error directives are checked.",
|
|
5168
5294
|
"The rule uses a small anchored vocabulary and does not score prose quality generally.",
|
|
5169
|
-
"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."
|
|
5170
5296
|
],
|
|
5171
5297
|
examples: [
|
|
5172
5298
|
{
|
|
@@ -5176,7 +5302,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
|
|
|
5176
5302
|
files: [
|
|
5177
5303
|
{
|
|
5178
5304
|
path: "src/adapter.ts",
|
|
5179
|
-
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}"
|
|
5180
5306
|
}
|
|
5181
5307
|
],
|
|
5182
5308
|
focusPath: "src/adapter.ts",
|
|
@@ -5190,7 +5316,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
|
|
|
5190
5316
|
files: [
|
|
5191
5317
|
{
|
|
5192
5318
|
path: "src/adapter.ts",
|
|
5193
|
-
source: "// @ts-expect-error -- false positive\
|
|
5319
|
+
source: "function requestId(response: object) {\n // @ts-expect-error -- false positive\n return response.requestId;\n}"
|
|
5194
5320
|
}
|
|
5195
5321
|
],
|
|
5196
5322
|
focusPath: "src/adapter.ts",
|
|
@@ -5235,7 +5361,7 @@ var no_vague_suppression_description_default = createRule({
|
|
|
5235
5361
|
});
|
|
5236
5362
|
|
|
5237
5363
|
// src/rules/no-generic-single-export-module.ts
|
|
5238
|
-
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";
|
|
5239
5365
|
var NO_GENERIC_SINGLE_EXPORT_MODULE_DOCUMENTATION = {
|
|
5240
5366
|
summary: "Disallow generic module stems when one runtime export already names the responsibility.",
|
|
5241
5367
|
rationale: "A generic filename hides the sole exported responsibility and makes navigation less descriptive.",
|
|
@@ -5362,7 +5488,7 @@ function typeOnlyBindings(program) {
|
|
|
5362
5488
|
return new Set([...names].filter((name) => !runtimeNames.has(name)));
|
|
5363
5489
|
}
|
|
5364
5490
|
function isGlobalIdentifier2(context, node) {
|
|
5365
|
-
const variable =
|
|
5491
|
+
const variable = ASTUtils11.findVariable(context.sourceCode.getScope(node), node.name);
|
|
5366
5492
|
return variable === null || variable.defs.length === 0;
|
|
5367
5493
|
}
|
|
5368
5494
|
function isConventionalFrameworkUtility(filename, exported) {
|
|
@@ -5423,11 +5549,11 @@ var no_generic_single_export_module_default = createRule({
|
|
|
5423
5549
|
// src/rules/no-offset-pagination.ts
|
|
5424
5550
|
import "@typescript-eslint/utils";
|
|
5425
5551
|
var NO_OFFSET_PAGINATION_DOCUMENTATION = {
|
|
5426
|
-
summary: "
|
|
5552
|
+
summary: "Prefer keyset pagination for embedded SQL queries using OFFSET.",
|
|
5427
5553
|
rationale: "Offset pagination scans skipped rows and shifts page boundaries under concurrent writes.",
|
|
5428
|
-
remediation: "
|
|
5554
|
+
remediation: "Consider a keyset cursor that preserves the query's complete ordering, tie-breakers, and filters.",
|
|
5429
5555
|
category: "performance",
|
|
5430
|
-
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."],
|
|
5431
5557
|
examples: [
|
|
5432
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 },
|
|
5433
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 }
|
|
@@ -5435,17 +5561,18 @@ var NO_OFFSET_PAGINATION_DOCUMENTATION = {
|
|
|
5435
5561
|
};
|
|
5436
5562
|
var OFFSET_PAGINATION = /\bOFFSET\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
|
|
5437
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;
|
|
5438
5565
|
var no_offset_pagination_default = createRule({
|
|
5439
5566
|
name: "no-offset-pagination",
|
|
5440
5567
|
documentation: NO_OFFSET_PAGINATION_DOCUMENTATION,
|
|
5441
5568
|
meta: {
|
|
5442
5569
|
type: "problem",
|
|
5443
5570
|
docs: {
|
|
5444
|
-
description: "
|
|
5571
|
+
description: "Prefer keyset pagination for embedded SQL queries using OFFSET."
|
|
5445
5572
|
},
|
|
5446
5573
|
schema: [],
|
|
5447
5574
|
messages: {
|
|
5448
|
-
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."
|
|
5449
5576
|
}
|
|
5450
5577
|
},
|
|
5451
5578
|
defaultOptions: [],
|
|
@@ -5454,7 +5581,7 @@ var no_offset_pagination_default = createRule({
|
|
|
5454
5581
|
return {};
|
|
5455
5582
|
}
|
|
5456
5583
|
return createSqlListener((sql, node) => {
|
|
5457
|
-
if (!OFFSET_PAGINATION.test(sql)) {
|
|
5584
|
+
if (!PAGINATION_CONTEXT.test(sql) || !OFFSET_PAGINATION.test(sql)) {
|
|
5458
5585
|
return;
|
|
5459
5586
|
}
|
|
5460
5587
|
context.report({ node, messageId: "noOffsetPagination" });
|
|
@@ -5463,7 +5590,7 @@ var no_offset_pagination_default = createRule({
|
|
|
5463
5590
|
});
|
|
5464
5591
|
|
|
5465
5592
|
// src/rules/no-positional-tuple-return.ts
|
|
5466
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES23, ASTUtils as
|
|
5593
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES23, ASTUtils as ASTUtils12 } from "@typescript-eslint/utils";
|
|
5467
5594
|
var NO_POSITIONAL_TUPLE_RETURN_DOCUMENTATION = {
|
|
5468
5595
|
summary: "Disallow returning a multi-field tuple from a named function; return a named object so call sites cannot mismatch slots.",
|
|
5469
5596
|
rationale: "Tuple fields are identified only by position, so reordering can preserve types while changing meaning.",
|
|
@@ -5597,7 +5724,7 @@ function typeAliases(sourceCode) {
|
|
|
5597
5724
|
get(identifier) {
|
|
5598
5725
|
const declaration = aliases.get(identifier.name);
|
|
5599
5726
|
if (declaration === void 0) return void 0;
|
|
5600
|
-
const binding =
|
|
5727
|
+
const binding = ASTUtils12.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
5601
5728
|
return binding?.defs.length === 1 && binding.defs[0]?.node === declaration ? declaration.typeAnnotation : void 0;
|
|
5602
5729
|
}
|
|
5603
5730
|
};
|
|
@@ -5898,7 +6025,7 @@ var no_production_browser_source_maps_default = createRule({
|
|
|
5898
6025
|
});
|
|
5899
6026
|
|
|
5900
6027
|
// src/rules/no-raw-env.ts
|
|
5901
|
-
import { ASTUtils as
|
|
6028
|
+
import { ASTUtils as ASTUtils13 } from "@typescript-eslint/utils";
|
|
5902
6029
|
var NO_RAW_ENV_DOCUMENTATION = {
|
|
5903
6030
|
summary: "Disallow direct `process.env` and `import.meta.env` reads outside validated boundaries.",
|
|
5904
6031
|
rationale: "Raw environment reads are untyped and defer invalid configuration failures until use.",
|
|
@@ -5980,7 +6107,7 @@ var no_raw_env_default = createRule({
|
|
|
5980
6107
|
},
|
|
5981
6108
|
MemberExpression(node) {
|
|
5982
6109
|
if (isProcessEnv(node) && node.object.type === "Identifier") {
|
|
5983
|
-
const binding =
|
|
6110
|
+
const binding = ASTUtils13.findVariable(context.sourceCode.getScope(node), node.object.name);
|
|
5984
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;
|
|
5985
6112
|
}
|
|
5986
6113
|
if ((isProcessEnv(node) || isImportMetaEnv(node)) && !isExemptVariableAccess(node) && !isWriteTarget(node) && !isWholeEnvSpread(node)) {
|
|
@@ -5996,7 +6123,7 @@ var no_raw_env_default = createRule({
|
|
|
5996
6123
|
});
|
|
5997
6124
|
|
|
5998
6125
|
// src/rules/no-raw-fetch-outside-clients.ts
|
|
5999
|
-
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";
|
|
6000
6127
|
var NO_RAW_FETCH_OUTSIDE_CLIENTS_DOCUMENTATION = {
|
|
6001
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.",
|
|
6002
6129
|
rationale: "Scattered fetch calls bypass shared transport policy and are harder to stub and observe consistently.",
|
|
@@ -6184,7 +6311,7 @@ var no_raw_fetch_outside_clients_default = createRule({
|
|
|
6184
6311
|
internalApiPrefixes.push(`${options.basePath}/api`);
|
|
6185
6312
|
}
|
|
6186
6313
|
function resolvesToGlobal(identifier) {
|
|
6187
|
-
const variable =
|
|
6314
|
+
const variable = ASTUtils14.findVariable(
|
|
6188
6315
|
context.sourceCode.getScope(identifier),
|
|
6189
6316
|
identifier.name
|
|
6190
6317
|
);
|
|
@@ -6193,7 +6320,7 @@ var no_raw_fetch_outside_clients_default = createRule({
|
|
|
6193
6320
|
function resolveNode2(node) {
|
|
6194
6321
|
if (node === void 0) return null;
|
|
6195
6322
|
if (node.type !== AST_NODE_TYPES24.Identifier) return node;
|
|
6196
|
-
const variable =
|
|
6323
|
+
const variable = ASTUtils14.findVariable(
|
|
6197
6324
|
context.sourceCode.getScope(node),
|
|
6198
6325
|
node.name
|
|
6199
6326
|
);
|
|
@@ -6272,7 +6399,7 @@ var no_raw_fetch_outside_clients_default = createRule({
|
|
|
6272
6399
|
});
|
|
6273
6400
|
|
|
6274
6401
|
// src/rules/no-restricted-library-load.ts
|
|
6275
|
-
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";
|
|
6276
6403
|
var NO_RESTRICTED_LIBRARY_LOAD_DOCUMENTATION = {
|
|
6277
6404
|
summary: "Apply configured library restrictions to literal runtime loads and CommonJS resolution references.",
|
|
6278
6405
|
rationale: "Dynamic imports, CommonJS loads, and package resolution checks can bypass library restrictions enforced for static imports.",
|
|
@@ -6345,7 +6472,7 @@ var no_restricted_library_load_default = createRule({
|
|
|
6345
6472
|
});
|
|
6346
6473
|
}
|
|
6347
6474
|
function isUnshadowedRequire(node) {
|
|
6348
|
-
const variable =
|
|
6475
|
+
const variable = ASTUtils15.findVariable(
|
|
6349
6476
|
context.sourceCode.getScope(node),
|
|
6350
6477
|
node.name
|
|
6351
6478
|
);
|
|
@@ -6378,7 +6505,7 @@ var no_restricted_library_load_default = createRule({
|
|
|
6378
6505
|
});
|
|
6379
6506
|
|
|
6380
6507
|
// src/rules/no-router-refresh-polling.ts
|
|
6381
|
-
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";
|
|
6382
6509
|
var NO_ROUTER_REFRESH_POLLING_DOCUMENTATION = {
|
|
6383
6510
|
summary: "Do not poll by calling a Next.js router's refresh method from a timer.",
|
|
6384
6511
|
rationale: "A route refresh refetches and rerenders the whole route on every tick instead of loading the named resource that changed.",
|
|
@@ -6408,7 +6535,7 @@ function isIntervalCallee(sourceCode, node) {
|
|
|
6408
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";
|
|
6409
6536
|
}
|
|
6410
6537
|
function isUnshadowedGlobal2(sourceCode, node) {
|
|
6411
|
-
const variable =
|
|
6538
|
+
const variable = ASTUtils16.findVariable(sourceCode.getScope(node), node.name);
|
|
6412
6539
|
return variable === null || variable.defs.length === 0;
|
|
6413
6540
|
}
|
|
6414
6541
|
var no_router_refresh_polling_default = createRule({
|
|
@@ -6431,21 +6558,21 @@ var no_router_refresh_polling_default = createRule({
|
|
|
6431
6558
|
if (node.source.value !== "next/navigation") return;
|
|
6432
6559
|
for (const specifier of node.specifiers) {
|
|
6433
6560
|
if (specifier.type === AST_NODE_TYPES26.ImportSpecifier && importedName4(specifier) === "useRouter") {
|
|
6434
|
-
const variable =
|
|
6561
|
+
const variable = ASTUtils16.findVariable(context.sourceCode.getScope(specifier.local), specifier.local.name);
|
|
6435
6562
|
if (variable !== null) routerHooks.add(variable);
|
|
6436
6563
|
}
|
|
6437
6564
|
}
|
|
6438
6565
|
},
|
|
6439
6566
|
VariableDeclarator(node) {
|
|
6440
6567
|
if (node.id.type === AST_NODE_TYPES26.Identifier && node.init?.type === AST_NODE_TYPES26.CallExpression && node.init.callee.type === AST_NODE_TYPES26.Identifier) {
|
|
6441
|
-
const hook =
|
|
6442
|
-
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);
|
|
6443
6570
|
if (hook !== null && router !== null && routerHooks.has(hook)) routers.add(router);
|
|
6444
6571
|
}
|
|
6445
6572
|
},
|
|
6446
6573
|
CallExpression(node) {
|
|
6447
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;
|
|
6448
|
-
const router =
|
|
6575
|
+
const router = ASTUtils16.findVariable(
|
|
6449
6576
|
context.sourceCode.getScope(node.callee.object),
|
|
6450
6577
|
node.callee.object.name
|
|
6451
6578
|
);
|
|
@@ -6485,7 +6612,7 @@ var NO_REPEATED_STRING_LITERAL_DOCUMENTATION = {
|
|
|
6485
6612
|
]
|
|
6486
6613
|
};
|
|
6487
6614
|
function isStructured(value) {
|
|
6488
|
-
return
|
|
6615
|
+
return SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value) || URL_PATH_RE.test(value);
|
|
6489
6616
|
}
|
|
6490
6617
|
function preview(value) {
|
|
6491
6618
|
const oneLine = value.replaceAll("\n", " ").trim();
|
|
@@ -6506,7 +6633,7 @@ function isScaffolding(node) {
|
|
|
6506
6633
|
}
|
|
6507
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;
|
|
6508
6635
|
const isRequireSource = parent.type === AST_NODE_TYPES27.CallExpression && parent.callee.type === AST_NODE_TYPES27.Identifier && parent.callee.name === "require";
|
|
6509
|
-
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;
|
|
6510
6637
|
}
|
|
6511
6638
|
var no_repeated_string_literal_default = createRule({
|
|
6512
6639
|
name: "no-repeated-string-literal",
|
|
@@ -7426,11 +7553,11 @@ var no_server_env_in_client_component_default = createRule({
|
|
|
7426
7553
|
// src/rules/no-select-star.ts
|
|
7427
7554
|
import "@typescript-eslint/utils";
|
|
7428
7555
|
var NO_SELECT_STAR_DOCUMENTATION = {
|
|
7429
|
-
summary: "
|
|
7556
|
+
summary: "Prefer explicit column projections over SELECT * in embedded SQL.",
|
|
7430
7557
|
rationale: "Wildcard projections couple row shape and query cost to unrelated schema changes.",
|
|
7431
7558
|
remediation: "List every required column explicitly in the projection.",
|
|
7432
7559
|
category: "correctness",
|
|
7433
|
-
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."],
|
|
7434
7561
|
examples: [
|
|
7435
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 },
|
|
7436
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 }
|
|
@@ -7481,7 +7608,7 @@ var no_select_star_default = createRule({
|
|
|
7481
7608
|
meta: {
|
|
7482
7609
|
type: "problem",
|
|
7483
7610
|
docs: {
|
|
7484
|
-
description: "
|
|
7611
|
+
description: "Prefer explicit column projections over SELECT * in embedded SQL."
|
|
7485
7612
|
},
|
|
7486
7613
|
schema: [],
|
|
7487
7614
|
messages: {
|
|
@@ -7503,7 +7630,7 @@ var no_select_star_default = createRule({
|
|
|
7503
7630
|
});
|
|
7504
7631
|
|
|
7505
7632
|
// src/rules/no-sentinel-return-on-catch.ts
|
|
7506
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES30, ASTUtils as
|
|
7633
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES30, ASTUtils as ASTUtils17 } from "@typescript-eslint/utils";
|
|
7507
7634
|
var NO_SENTINEL_RETURN_ON_CATCH_DOCUMENTATION = {
|
|
7508
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.",
|
|
7509
7636
|
rationale: "An unreported fallback makes operational failure indistinguishable from a legitimate empty result.",
|
|
@@ -7925,7 +8052,7 @@ var no_sentinel_return_on_catch_default = createRule({
|
|
|
7925
8052
|
return;
|
|
7926
8053
|
}
|
|
7927
8054
|
const returned = unwrapSentinelExpression(last.argument);
|
|
7928
|
-
if (returned?.type === AST_NODE_TYPES30.Identifier && returned.name === "undefined" && (
|
|
8055
|
+
if (returned?.type === AST_NODE_TYPES30.Identifier && returned.name === "undefined" && (ASTUtils17.findVariable(context.sourceCode.getScope(returned), returned.name)?.defs.length ?? 0) > 0) return;
|
|
7929
8056
|
if (containsThrow(node.body)) {
|
|
7930
8057
|
return;
|
|
7931
8058
|
}
|
|
@@ -7959,13 +8086,13 @@ var no_sentinel_return_on_catch_default = createRule({
|
|
|
7959
8086
|
});
|
|
7960
8087
|
|
|
7961
8088
|
// src/rules/no-silent-promise-catch.ts
|
|
7962
|
-
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";
|
|
7963
8090
|
var NO_SILENT_PROMISE_CATCH_DOCUMENTATION = {
|
|
7964
8091
|
summary: "Disallow `.catch()` and second-argument `.then()` handlers that silently swallow a rejection; log, rethrow, or handle the error.",
|
|
7965
8092
|
rationale: "A swallowed rejection hides failures and gives callers an indistinguishable fallback value.",
|
|
7966
8093
|
remediation: "Log, rethrow, or explicitly recover from the rejection; explain intentional teardown suppression.",
|
|
7967
8094
|
category: "correctness",
|
|
7968
|
-
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."],
|
|
7969
8096
|
examples: [
|
|
7970
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 },
|
|
7971
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 }
|
|
@@ -7979,6 +8106,43 @@ var BODY_PARSE_METHODS = /* @__PURE__ */ new Set([
|
|
|
7979
8106
|
"json",
|
|
7980
8107
|
"text"
|
|
7981
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
|
+
]);
|
|
7982
8146
|
function isBodyParseCall(node) {
|
|
7983
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);
|
|
7984
8148
|
}
|
|
@@ -8052,6 +8216,26 @@ var no_silent_promise_catch_default = createRule({
|
|
|
8052
8216
|
if (isTestFile(context.filename) || isScriptFile(context.filename)) {
|
|
8053
8217
|
return {};
|
|
8054
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
|
+
}
|
|
8055
8239
|
const hasExplanatoryComment = (call, handler) => {
|
|
8056
8240
|
const sourceCode = context.sourceCode;
|
|
8057
8241
|
if (sourceCode.getCommentsInside(handler).some(isExplanatory)) {
|
|
@@ -8076,6 +8260,7 @@ var no_silent_promise_catch_default = createRule({
|
|
|
8076
8260
|
const method = node.callee.property.name;
|
|
8077
8261
|
const handlerIndex = method === "catch" ? 0 : method === "then" ? 1 : null;
|
|
8078
8262
|
if (handlerIndex === null) return;
|
|
8263
|
+
if (method === "catch" && isZodSchema(node.callee.object)) return;
|
|
8079
8264
|
if (isBodyParseCall(node.callee.object)) {
|
|
8080
8265
|
return;
|
|
8081
8266
|
}
|
|
@@ -8108,16 +8293,16 @@ var no_silent_promise_catch_default = createRule({
|
|
|
8108
8293
|
});
|
|
8109
8294
|
|
|
8110
8295
|
// src/rules/no-sleep-in-test-body.ts
|
|
8111
|
-
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";
|
|
8112
8297
|
var NO_SLEEP_IN_TEST_BODY_DOCUMENTATION = {
|
|
8113
|
-
summary: "
|
|
8298
|
+
summary: "Avoid fixed timed sleeps directly in test bodies; synchronize on observable behavior or use controlled timers.",
|
|
8114
8299
|
rationale: "Wall-clock delays make test correctness depend on scheduler and machine speed.",
|
|
8115
|
-
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.",
|
|
8116
8301
|
category: "testing",
|
|
8117
8302
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
8118
|
-
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."],
|
|
8119
8304
|
examples: [
|
|
8120
|
-
{ 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 },
|
|
8121
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 }
|
|
8122
8307
|
]
|
|
8123
8308
|
};
|
|
@@ -8136,9 +8321,6 @@ var FUNCTION_TYPES5 = /* @__PURE__ */ new Set([
|
|
|
8136
8321
|
function isNonzeroNumericLiteral(node) {
|
|
8137
8322
|
return node?.type === AST_NODE_TYPES32.Literal && typeof node.value === "number" && node.value !== 0;
|
|
8138
8323
|
}
|
|
8139
|
-
function isTimedSetTimeout(node) {
|
|
8140
|
-
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]);
|
|
8141
|
-
}
|
|
8142
8324
|
function isPromiseSleep(node) {
|
|
8143
8325
|
if (node.callee.type !== AST_NODE_TYPES32.Identifier || node.callee.name !== "Promise") {
|
|
8144
8326
|
return false;
|
|
@@ -8148,12 +8330,14 @@ function isPromiseSleep(node) {
|
|
|
8148
8330
|
return false;
|
|
8149
8331
|
}
|
|
8150
8332
|
const body2 = executor.body;
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
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]);
|
|
8157
8341
|
}
|
|
8158
8342
|
function isHelperSleep(node) {
|
|
8159
8343
|
return node.callee.type === AST_NODE_TYPES32.Identifier && SLEEP_HELPERS.has(node.callee.name) && node.arguments.length >= 1 && isNonzeroNumericLiteral(node.arguments[0]);
|
|
@@ -8207,11 +8391,11 @@ var no_sleep_in_test_body_default = createRule({
|
|
|
8207
8391
|
meta: {
|
|
8208
8392
|
type: "problem",
|
|
8209
8393
|
docs: {
|
|
8210
|
-
description: "
|
|
8394
|
+
description: "Avoid fixed timed sleeps directly in test bodies; synchronize on observable behavior or use controlled timers."
|
|
8211
8395
|
},
|
|
8212
8396
|
schema: [],
|
|
8213
8397
|
messages: {
|
|
8214
|
-
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."
|
|
8215
8399
|
}
|
|
8216
8400
|
},
|
|
8217
8401
|
defaultOptions: [],
|
|
@@ -8232,11 +8416,17 @@ var no_sleep_in_test_body_default = createRule({
|
|
|
8232
8416
|
return {
|
|
8233
8417
|
NewExpression(node) {
|
|
8234
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;
|
|
8235
8422
|
report2(node);
|
|
8236
8423
|
}
|
|
8237
8424
|
},
|
|
8238
8425
|
CallExpression(node) {
|
|
8239
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;
|
|
8240
8430
|
report2(node);
|
|
8241
8431
|
}
|
|
8242
8432
|
}
|
|
@@ -8257,7 +8447,7 @@ var NO_STORAGE_IN_STATELESS_MODULES_DOCUMENTATION = {
|
|
|
8257
8447
|
rationale: "Private storage in a stateless workflow creates another source of truth that can silently diverge.",
|
|
8258
8448
|
remediation: "Read from the system of record or derive state from an artifact the workflow already produces.",
|
|
8259
8449
|
category: "architecture",
|
|
8260
|
-
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."],
|
|
8261
8451
|
examples: [
|
|
8262
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 },
|
|
8263
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 }
|
|
@@ -8291,6 +8481,17 @@ function storageMethodName(node, methods) {
|
|
|
8291
8481
|
if (name === "put" && !isStorageLikeReceiver(callee.object)) {
|
|
8292
8482
|
return null;
|
|
8293
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
|
+
}
|
|
8294
8495
|
return name;
|
|
8295
8496
|
}
|
|
8296
8497
|
function isStorageLikeReceiver(node) {
|
|
@@ -8628,14 +8829,14 @@ var no_string_concat_in_loop_default = createRule({
|
|
|
8628
8829
|
});
|
|
8629
8830
|
|
|
8630
8831
|
// src/rules/no-tautological-expect.ts
|
|
8631
|
-
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";
|
|
8632
8833
|
var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
8633
|
-
summary: "Disallow
|
|
8834
|
+
summary: "Disallow supported literal-only assertions that are statically known to pass.",
|
|
8634
8835
|
rationale: "An assertion determined entirely by literals does not observe the code under test and can keep passing after that code is removed.",
|
|
8635
8836
|
remediation: "Assert on a value produced by the behavior under test, or remove the assertion.",
|
|
8636
8837
|
category: "testing",
|
|
8637
8838
|
limitations: [
|
|
8638
|
-
"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."
|
|
8639
8840
|
],
|
|
8640
8841
|
examples: [
|
|
8641
8842
|
{
|
|
@@ -8672,11 +8873,11 @@ var NUMERIC_SIGNS = /* @__PURE__ */ new Set(["-", "+"]);
|
|
|
8672
8873
|
function isLiteral(node) {
|
|
8673
8874
|
switch (node.type) {
|
|
8674
8875
|
case AST_NODE_TYPES34.Literal:
|
|
8675
|
-
return
|
|
8876
|
+
return !("regex" in node);
|
|
8676
8877
|
case AST_NODE_TYPES34.TemplateLiteral:
|
|
8677
8878
|
return node.expressions.length === 0;
|
|
8678
8879
|
case AST_NODE_TYPES34.UnaryExpression:
|
|
8679
|
-
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";
|
|
8680
8881
|
case AST_NODE_TYPES34.ArrayExpression:
|
|
8681
8882
|
return node.elements.every((element) => element !== null && isLiteral(element));
|
|
8682
8883
|
case AST_NODE_TYPES34.ObjectExpression:
|
|
@@ -8690,6 +8891,43 @@ function isLiteral(node) {
|
|
|
8690
8891
|
function isStructuralLiteral(node) {
|
|
8691
8892
|
return node.type === AST_NODE_TYPES34.ArrayExpression || node.type === AST_NODE_TYPES34.ObjectExpression;
|
|
8692
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
|
+
}
|
|
8693
8931
|
function expectOperand(callee) {
|
|
8694
8932
|
const receiver = callee.object;
|
|
8695
8933
|
if (receiver.type !== AST_NODE_TYPES34.CallExpression || receiver.callee.type !== AST_NODE_TYPES34.Identifier || receiver.callee.name !== "expect" || receiver.arguments.length !== 1) {
|
|
@@ -8703,12 +8941,12 @@ var no_tautological_expect_default = createRule({
|
|
|
8703
8941
|
meta: {
|
|
8704
8942
|
type: "problem",
|
|
8705
8943
|
docs: {
|
|
8706
|
-
description: "Disallow
|
|
8944
|
+
description: "Disallow supported literal-only assertions that are statically known to pass."
|
|
8707
8945
|
},
|
|
8708
8946
|
schema: [],
|
|
8709
8947
|
messages: {
|
|
8710
|
-
tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` compares
|
|
8711
|
-
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."
|
|
8712
8950
|
}
|
|
8713
8951
|
},
|
|
8714
8952
|
defaultOptions: [],
|
|
@@ -8730,11 +8968,20 @@ var no_tautological_expect_default = createRule({
|
|
|
8730
8968
|
return;
|
|
8731
8969
|
}
|
|
8732
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;
|
|
8733
8980
|
const operand = expectOperand(callee);
|
|
8734
8981
|
if (operand === null || !isLiteral(operand)) {
|
|
8735
8982
|
return;
|
|
8736
8983
|
}
|
|
8737
|
-
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0) {
|
|
8984
|
+
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0 && passesZeroArgumentMatcher(operand, matcher)) {
|
|
8738
8985
|
context.report({
|
|
8739
8986
|
node,
|
|
8740
8987
|
messageId: "tautologicalMatcher",
|
|
@@ -9968,7 +10215,7 @@ var no_unnecessary_use_client_default = createRule({
|
|
|
9968
10215
|
// src/rules/no-unsafe-mock-casting.ts
|
|
9969
10216
|
import {
|
|
9970
10217
|
AST_NODE_TYPES as AST_NODE_TYPES40,
|
|
9971
|
-
ASTUtils as
|
|
10218
|
+
ASTUtils as ASTUtils21
|
|
9972
10219
|
} from "@typescript-eslint/utils";
|
|
9973
10220
|
var MOCK_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
9974
10221
|
"Mock",
|
|
@@ -9989,15 +10236,16 @@ var MOCK_MODULES = /* @__PURE__ */ new Set([
|
|
|
9989
10236
|
var NO_UNSAFE_MOCK_CASTING_DOCUMENTATION = {
|
|
9990
10237
|
summary: "Disallow casting to mock types like `jest.Mock` or `vi.Mock`. Use `vi.mocked()` or `jest.mocked()` instead.",
|
|
9991
10238
|
rationale: "A type assertion can claim an unmocked value is a mock and bypass checking between the original callable and the mock API.",
|
|
9992
|
-
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.",
|
|
9993
10240
|
category: "testing",
|
|
9994
|
-
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"],
|
|
9995
10243
|
examples: [
|
|
9996
10244
|
{
|
|
9997
10245
|
id: "typed-mock-helper",
|
|
9998
10246
|
title: "Use the framework helper",
|
|
9999
10247
|
outcome: "no-match",
|
|
10000
|
-
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);" }],
|
|
10001
10249
|
focusPath: "src/client.test.ts",
|
|
10002
10250
|
expectedCount: 0,
|
|
10003
10251
|
public: true
|
|
@@ -10023,7 +10271,7 @@ var no_unsafe_mock_casting_default = createRule({
|
|
|
10023
10271
|
},
|
|
10024
10272
|
schema: [],
|
|
10025
10273
|
messages: {
|
|
10026
|
-
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."
|
|
10027
10275
|
}
|
|
10028
10276
|
},
|
|
10029
10277
|
defaultOptions: [],
|
|
@@ -10034,7 +10282,7 @@ var no_unsafe_mock_casting_default = createRule({
|
|
|
10034
10282
|
const directBindings = /* @__PURE__ */ new Set();
|
|
10035
10283
|
const namespaceBindings = /* @__PURE__ */ new Set();
|
|
10036
10284
|
function resolve2(identifier) {
|
|
10037
|
-
return
|
|
10285
|
+
return ASTUtils21.findVariable(
|
|
10038
10286
|
context.sourceCode.getScope(identifier),
|
|
10039
10287
|
identifier.name
|
|
10040
10288
|
);
|
|
@@ -10084,12 +10332,12 @@ var no_unsafe_mock_casting_default = createRule({
|
|
|
10084
10332
|
import {
|
|
10085
10333
|
ESLintUtils as ESLintUtils3,
|
|
10086
10334
|
AST_NODE_TYPES as AST_NODE_TYPES41,
|
|
10087
|
-
ASTUtils as
|
|
10335
|
+
ASTUtils as ASTUtils22
|
|
10088
10336
|
} from "@typescript-eslint/utils";
|
|
10089
10337
|
import * as ts2 from "typescript";
|
|
10090
10338
|
var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
|
|
10091
10339
|
summary: 'Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum(["a", "b"])` with a string-literal union instead.',
|
|
10092
|
-
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.",
|
|
10093
10341
|
remediation: "Pass string literals directly to `z.enum` and derive the TypeScript type with `z.infer`.",
|
|
10094
10342
|
category: "maintainability",
|
|
10095
10343
|
autofix: "none",
|
|
@@ -10194,7 +10442,7 @@ var no_zod_native_enum_default = createRule({
|
|
|
10194
10442
|
const zodImportedBindings = /* @__PURE__ */ new Map();
|
|
10195
10443
|
const zodNamespaceBindings = /* @__PURE__ */ new Set();
|
|
10196
10444
|
function resolvedBinding(identifier) {
|
|
10197
|
-
return
|
|
10445
|
+
return ASTUtils22.findVariable(
|
|
10198
10446
|
sourceCode.getScope(identifier),
|
|
10199
10447
|
identifier.name
|
|
10200
10448
|
);
|
|
@@ -10260,14 +10508,14 @@ var no_zod_native_enum_default = createRule({
|
|
|
10260
10508
|
});
|
|
10261
10509
|
|
|
10262
10510
|
// src/rules/test-loops-over-literal-cases.ts
|
|
10263
|
-
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";
|
|
10264
10512
|
var TEST_LOOPS_OVER_LITERAL_CASES_DOCUMENTATION = {
|
|
10265
10513
|
summary: "Disallow assertions over an inline literal case loop in a test; parameterization reports and names every case independently.",
|
|
10266
10514
|
rationale: "A loop is reported as one test, so failures hide the individual case name and may stop later cases from running.",
|
|
10267
10515
|
remediation: "Create one named parameterized test or runner-aware subtest for each literal case.",
|
|
10268
10516
|
category: "testing",
|
|
10269
10517
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**"],
|
|
10270
|
-
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."],
|
|
10271
10519
|
examples: [
|
|
10272
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 },
|
|
10273
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 }
|
|
@@ -10414,7 +10662,7 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10414
10662
|
},
|
|
10415
10663
|
schema: [],
|
|
10416
10664
|
messages: {
|
|
10417
|
-
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."
|
|
10418
10666
|
}
|
|
10419
10667
|
},
|
|
10420
10668
|
defaultOptions: [],
|
|
@@ -10423,7 +10671,7 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10423
10671
|
return {};
|
|
10424
10672
|
}
|
|
10425
10673
|
const isFrameworkIdentifier = (identifier, modules) => {
|
|
10426
|
-
const variable =
|
|
10674
|
+
const variable = ASTUtils23.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
10427
10675
|
if (variable === null || variable.defs.length === 0) return true;
|
|
10428
10676
|
return variable.defs.some((definition) => {
|
|
10429
10677
|
let current = definition.node;
|
|
@@ -10439,6 +10687,9 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10439
10687
|
if (enclosing === null || !isTestBody2(enclosing, isFrameworkTest)) {
|
|
10440
10688
|
return;
|
|
10441
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
|
+
}
|
|
10442
10693
|
const cases = unwrapExpression(node.right);
|
|
10443
10694
|
const callbackParameters = new Set(
|
|
10444
10695
|
enclosing.params.flatMap((parameter) => parameter.type === AST_NODE_TYPES42.Identifier ? [parameter.name] : [])
|
|
@@ -10448,6 +10699,16 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10448
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))) {
|
|
10449
10700
|
return;
|
|
10450
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;
|
|
10451
10712
|
context.report({
|
|
10452
10713
|
node,
|
|
10453
10714
|
messageId: "literalCaseLoop",
|
|
@@ -10900,7 +11161,7 @@ var prefer_discriminated_union_default = createRule({
|
|
|
10900
11161
|
});
|
|
10901
11162
|
|
|
10902
11163
|
// src/rules/prefer-input-group-search.ts
|
|
10903
|
-
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";
|
|
10904
11165
|
var PREFER_INPUT_GROUP_SEARCH_DOCUMENTATION = {
|
|
10905
11166
|
summary: "Require search icons and shared Input controls in the same visual wrapper to use InputGroup.",
|
|
10906
11167
|
rationale: "The shared compound control provides consistent spacing, focus behavior, and accessible composition.",
|
|
@@ -11024,7 +11285,7 @@ var prefer_input_group_search_default = createRule({
|
|
|
11024
11285
|
JSXOpeningElement(node) {
|
|
11025
11286
|
const name = elementName(node);
|
|
11026
11287
|
if (name === null) return;
|
|
11027
|
-
const binding =
|
|
11288
|
+
const binding = ASTUtils24.findVariable(context.sourceCode.getScope(node), name);
|
|
11028
11289
|
if (binding?.defs.length !== 1 || binding.defs[0]?.node.type !== AST_NODE_TYPES47.ImportSpecifier || binding.defs[0].node.importKind === "type") return;
|
|
11029
11290
|
const occurrence = {
|
|
11030
11291
|
ancestors: context.sourceCode.getAncestors(node),
|
|
@@ -11064,7 +11325,7 @@ var prefer_input_group_search_default = createRule({
|
|
|
11064
11325
|
|
|
11065
11326
|
// src/rules/prefer-millisecond-control-duration-schema.ts
|
|
11066
11327
|
import {
|
|
11067
|
-
ASTUtils as
|
|
11328
|
+
ASTUtils as ASTUtils25,
|
|
11068
11329
|
AST_NODE_TYPES as AST_NODE_TYPES48
|
|
11069
11330
|
} from "@typescript-eslint/utils";
|
|
11070
11331
|
var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
@@ -11074,7 +11335,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
|
11074
11335
|
category: "correctness",
|
|
11075
11336
|
autofix: "none",
|
|
11076
11337
|
limitations: [
|
|
11077
|
-
"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.",
|
|
11078
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.",
|
|
11079
11340
|
"Quoted/computed protocol keys, generated/vendor code, tests, fixtures, and non-Zod schemas are excluded."
|
|
11080
11341
|
],
|
|
@@ -11086,7 +11347,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
|
11086
11347
|
files: [
|
|
11087
11348
|
{
|
|
11088
11349
|
path: "src/request.ts",
|
|
11089
|
-
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) });"
|
|
11090
11351
|
}
|
|
11091
11352
|
],
|
|
11092
11353
|
focusPath: "src/request.ts",
|
|
@@ -11100,7 +11361,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
|
11100
11361
|
files: [
|
|
11101
11362
|
{
|
|
11102
11363
|
path: "src/request.ts",
|
|
11103
|
-
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) });"
|
|
11104
11365
|
}
|
|
11105
11366
|
],
|
|
11106
11367
|
focusPath: "src/request.ts",
|
|
@@ -11132,8 +11393,9 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11132
11393
|
}
|
|
11133
11394
|
const zodNamespaces = /* @__PURE__ */ new Set();
|
|
11134
11395
|
const objectFactories = /* @__PURE__ */ new Set();
|
|
11396
|
+
const numberFactories = /* @__PURE__ */ new Set();
|
|
11135
11397
|
function binding(identifier) {
|
|
11136
|
-
return
|
|
11398
|
+
return ASTUtils25.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
11137
11399
|
}
|
|
11138
11400
|
function record(target, identifier) {
|
|
11139
11401
|
const variable = binding(identifier);
|
|
@@ -11151,10 +11413,27 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11151
11413
|
const variable = binding(callee.object);
|
|
11152
11414
|
return variable !== null && zodNamespaces.has(variable);
|
|
11153
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
|
+
}
|
|
11154
11430
|
return {
|
|
11155
11431
|
ImportDeclaration(node) {
|
|
11156
11432
|
if (!isZodModule(node.source.value)) return;
|
|
11157
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
|
+
}
|
|
11158
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") {
|
|
11159
11438
|
record(zodNamespaces, specifier.local);
|
|
11160
11439
|
} else if (specifier.type === AST_NODE_TYPES48.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES48.Identifier && (specifier.imported.name === "object" || specifier.imported.name === "strictObject")) {
|
|
@@ -11167,7 +11446,7 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11167
11446
|
const shape = node.arguments[0];
|
|
11168
11447
|
if (shape?.type !== AST_NODE_TYPES48.ObjectExpression) return;
|
|
11169
11448
|
for (const member of shape.properties) {
|
|
11170
|
-
if (member.type !== AST_NODE_TYPES48.Property) continue;
|
|
11449
|
+
if (member.type !== AST_NODE_TYPES48.Property || !isNumericSchema(member.value)) continue;
|
|
11171
11450
|
const key = directIdentifierKey(member);
|
|
11172
11451
|
if (key === null || !CONTROL_SECONDS_RE.test(key.name) && !CONTROL_SECONDS_CAMEL_RE.test(key.name)) {
|
|
11173
11452
|
continue;
|
|
@@ -11180,14 +11459,14 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11180
11459
|
});
|
|
11181
11460
|
|
|
11182
11461
|
// src/rules/prefer-immutable-module-constant.ts
|
|
11183
|
-
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";
|
|
11184
11463
|
var PREFER_IMMUTABLE_MODULE_CONSTANT_DOCUMENTATION = {
|
|
11185
11464
|
summary: "Require module-level constant collections to expose readonly state.",
|
|
11186
11465
|
rationale: "A const binding prevents reassignment but does not stop callers from mutating its array, object, Set, or Map contents.",
|
|
11187
11466
|
remediation: "Expose literals with `as const` or a readonly type, and expose Set or Map values through ReadonlySet or ReadonlyMap.",
|
|
11188
11467
|
category: "correctness",
|
|
11189
11468
|
limitations: [
|
|
11190
|
-
"
|
|
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."
|
|
11191
11470
|
],
|
|
11192
11471
|
examples: [
|
|
11193
11472
|
{
|
|
@@ -11333,7 +11612,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11333
11612
|
create(context) {
|
|
11334
11613
|
const sourceCode = context.sourceCode;
|
|
11335
11614
|
const isUnshadowedGlobal3 = (identifier) => {
|
|
11336
|
-
const variable =
|
|
11615
|
+
const variable = ASTUtils26.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
11337
11616
|
return variable === null || variable.defs.length === 0;
|
|
11338
11617
|
};
|
|
11339
11618
|
if (JAVASCRIPT_FILE_RE.test(context.filename) || isTestFile(context.filename) || isGeneratedFile(context.filename, sourceCode.getText())) {
|
|
@@ -11341,7 +11620,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11341
11620
|
}
|
|
11342
11621
|
const exportedNames2 = /* @__PURE__ */ new Set();
|
|
11343
11622
|
const typeAliases2 = /* @__PURE__ */ new Map();
|
|
11344
|
-
const
|
|
11623
|
+
const mutatesThroughAlias = (root) => {
|
|
11345
11624
|
const pending = [root];
|
|
11346
11625
|
const seen = /* @__PURE__ */ new Set();
|
|
11347
11626
|
while (pending.length > 0) {
|
|
@@ -11353,7 +11632,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11353
11632
|
if (identifier.type !== AST_NODE_TYPES49.Identifier) continue;
|
|
11354
11633
|
if (referenceMutates(identifier, isUnshadowedGlobal3)) return true;
|
|
11355
11634
|
const declarator = identifier.parent;
|
|
11356
|
-
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) {
|
|
11357
11636
|
continue;
|
|
11358
11637
|
}
|
|
11359
11638
|
const alias = sourceCode.getDeclaredVariables(declarator)[0];
|
|
@@ -11402,7 +11681,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11402
11681
|
return;
|
|
11403
11682
|
}
|
|
11404
11683
|
const variable = sourceCode.getDeclaredVariables(node)[0];
|
|
11405
|
-
if (!directlyExported && !exportedNames2.has(node.id.name) && variable !== void 0 &&
|
|
11684
|
+
if (!directlyExported && !exportedNames2.has(node.id.name) && variable !== void 0 && mutatesThroughAlias(variable)) {
|
|
11406
11685
|
return;
|
|
11407
11686
|
}
|
|
11408
11687
|
context.report({
|
|
@@ -11829,7 +12108,7 @@ var PREFER_MODULE_LEVEL_CONSTANT_DOCUMENTATION = {
|
|
|
11829
12108
|
rationale: "Recreating immutable lookup data on every call wastes allocations and obscures its constant nature.",
|
|
11830
12109
|
remediation: "Declare immutable literal collections and non-stateful regular expressions once at module scope.",
|
|
11831
12110
|
category: "performance",
|
|
11832
|
-
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."],
|
|
11833
12112
|
examples: [
|
|
11834
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 },
|
|
11835
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 }
|
|
@@ -11984,12 +12263,16 @@ var NON_RETAINING_BUILTINS = /* @__PURE__ */ new Map(
|
|
|
11984
12263
|
]
|
|
11985
12264
|
);
|
|
11986
12265
|
function isSafeRead(identifier) {
|
|
11987
|
-
|
|
12266
|
+
let parent = identifier.parent;
|
|
11988
12267
|
if (parent.type === AST_NODE_TYPES51.MemberExpression) {
|
|
11989
12268
|
if (parent.object !== identifier) {
|
|
11990
12269
|
return true;
|
|
11991
12270
|
}
|
|
12271
|
+
while (parent.parent.type === AST_NODE_TYPES51.MemberExpression && parent.parent.object === parent) {
|
|
12272
|
+
parent = parent.parent;
|
|
12273
|
+
}
|
|
11992
12274
|
const grandparent = parent.parent;
|
|
12275
|
+
if (grandparent.type === AST_NODE_TYPES51.VariableDeclarator || grandparent.type === AST_NODE_TYPES51.SpreadElement) return false;
|
|
11993
12276
|
if (grandparent.type === AST_NODE_TYPES51.AssignmentExpression && grandparent.left === parent) {
|
|
11994
12277
|
return false;
|
|
11995
12278
|
}
|
|
@@ -11999,7 +12282,7 @@ function isSafeRead(identifier) {
|
|
|
11999
12282
|
if (grandparent.type === AST_NODE_TYPES51.UnaryExpression && grandparent.operator === "delete") {
|
|
12000
12283
|
return false;
|
|
12001
12284
|
}
|
|
12002
|
-
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))) {
|
|
12003
12286
|
return false;
|
|
12004
12287
|
}
|
|
12005
12288
|
return true;
|
|
@@ -12107,9 +12390,15 @@ var prefer_module_level_constant_default = createRule({
|
|
|
12107
12390
|
if (node.id.type !== AST_NODE_TYPES51.Identifier || node.init === null) {
|
|
12108
12391
|
return;
|
|
12109
12392
|
}
|
|
12110
|
-
|
|
12393
|
+
const owner = enclosingFunction3(node);
|
|
12394
|
+
if (owner === null) {
|
|
12111
12395
|
return;
|
|
12112
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;
|
|
12113
12402
|
const candidate2 = classify(node.init, checkRegex);
|
|
12114
12403
|
if (candidate2 === null) {
|
|
12115
12404
|
return;
|
|
@@ -12134,10 +12423,10 @@ var prefer_module_level_constant_default = createRule({
|
|
|
12134
12423
|
import { AST_NODE_TYPES as AST_NODE_TYPES52 } from "@typescript-eslint/utils";
|
|
12135
12424
|
var PREFER_MODULE_LEVEL_SCHEMA_DOCUMENTATION = {
|
|
12136
12425
|
summary: "Declare a Zod schema at module scope when it closes over nothing in the enclosing function",
|
|
12137
|
-
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.",
|
|
12138
12427
|
remediation: "Move the closed schema declaration to module scope and reference it from the function.",
|
|
12139
12428
|
category: "performance",
|
|
12140
|
-
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."],
|
|
12141
12430
|
examples: [
|
|
12142
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 },
|
|
12143
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 }
|
|
@@ -12154,6 +12443,36 @@ var DEFAULT_FACTORIES = [
|
|
|
12154
12443
|
"union"
|
|
12155
12444
|
];
|
|
12156
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
|
+
]);
|
|
12157
12476
|
var MEMO_CALLEES = /* @__PURE__ */ new Set([
|
|
12158
12477
|
"lazy",
|
|
12159
12478
|
"memo",
|
|
@@ -12230,7 +12549,7 @@ function outermostEnclosingFunction(node) {
|
|
|
12230
12549
|
}
|
|
12231
12550
|
return outermost;
|
|
12232
12551
|
}
|
|
12233
|
-
function subtreeSome(root, predicate) {
|
|
12552
|
+
function subtreeSome(root, predicate, skipDeferredFunctions = false) {
|
|
12234
12553
|
let found = false;
|
|
12235
12554
|
const visit = (value) => {
|
|
12236
12555
|
if (found || value === null || typeof value !== "object") {
|
|
@@ -12246,6 +12565,7 @@ function subtreeSome(root, predicate) {
|
|
|
12246
12565
|
if (typeof candidate2.type !== "string") {
|
|
12247
12566
|
return;
|
|
12248
12567
|
}
|
|
12568
|
+
if (skipDeferredFunctions && FUNCTION_TYPES8.has(candidate2.type)) return;
|
|
12249
12569
|
if (predicate(candidate2)) {
|
|
12250
12570
|
found = true;
|
|
12251
12571
|
return;
|
|
@@ -12344,6 +12664,15 @@ var prefer_module_level_schema_default = createRule({
|
|
|
12344
12664
|
function isZodCall(node) {
|
|
12345
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);
|
|
12346
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
|
+
}
|
|
12347
12676
|
function isCovered(node) {
|
|
12348
12677
|
let current = node.parent ?? void 0;
|
|
12349
12678
|
while (current !== void 0) {
|
|
@@ -12473,6 +12802,7 @@ var prefer_module_level_schema_default = createRule({
|
|
|
12473
12802
|
return;
|
|
12474
12803
|
}
|
|
12475
12804
|
const outermost = outermostSchemaExpression(expression);
|
|
12805
|
+
if (hasEagerComputation(outermost)) return;
|
|
12476
12806
|
if (outermost !== expression && (readsReceiver(outermost) || buildsLocalizedText(outermost) || !closesOverNothing(outermost, enclosing))) {
|
|
12477
12807
|
return;
|
|
12478
12808
|
}
|
|
@@ -12492,7 +12822,7 @@ var prefer_module_level_schema_default = createRule({
|
|
|
12492
12822
|
// src/rules/prefer-module-level-refined-schema.ts
|
|
12493
12823
|
import {
|
|
12494
12824
|
AST_NODE_TYPES as AST_NODE_TYPES53,
|
|
12495
|
-
ASTUtils as
|
|
12825
|
+
ASTUtils as ASTUtils27
|
|
12496
12826
|
} from "@typescript-eslint/utils";
|
|
12497
12827
|
var BENCHMARK_PATH_RE = /(^|[/\\])(?:benchmarks?|bench)[/\\]/;
|
|
12498
12828
|
var FACTORIES = /* @__PURE__ */ new Set([
|
|
@@ -12613,7 +12943,8 @@ var PREFER_MODULE_LEVEL_REFINED_SCHEMA_DOCUMENTATION = {
|
|
|
12613
12943
|
limitations: [
|
|
12614
12944
|
"Composite object/record/tuple/union schemas are owned by prefer-module-level-schema.",
|
|
12615
12945
|
"Schemas that depend on function-local or mutable state, localized text, receiver state, lazy construction, or recognized memoization are excluded.",
|
|
12616
|
-
"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."
|
|
12617
12948
|
],
|
|
12618
12949
|
examples: [
|
|
12619
12950
|
{
|
|
@@ -12655,7 +12986,7 @@ function collectReferences2(scope, output) {
|
|
|
12655
12986
|
output.push(...scope.references);
|
|
12656
12987
|
for (const child of scope.childScopes) collectReferences2(child, output);
|
|
12657
12988
|
}
|
|
12658
|
-
function subtreeSome2(root, predicate) {
|
|
12989
|
+
function subtreeSome2(root, predicate, skipDeferredFunctions = false) {
|
|
12659
12990
|
let found = false;
|
|
12660
12991
|
const visit = (value) => {
|
|
12661
12992
|
if (found || value === null || typeof value !== "object") return;
|
|
@@ -12665,6 +12996,7 @@ function subtreeSome2(root, predicate) {
|
|
|
12665
12996
|
}
|
|
12666
12997
|
const candidate2 = value;
|
|
12667
12998
|
if (typeof candidate2.type !== "string") return;
|
|
12999
|
+
if (skipDeferredFunctions && FUNCTION_TYPES9.has(candidate2.type)) return;
|
|
12668
13000
|
if (predicate(candidate2)) {
|
|
12669
13001
|
found = true;
|
|
12670
13002
|
return;
|
|
@@ -12763,7 +13095,7 @@ var prefer_module_level_refined_schema_default = createRule({
|
|
|
12763
13095
|
return {};
|
|
12764
13096
|
const zodBindings = /* @__PURE__ */ new Set();
|
|
12765
13097
|
function resolvedBinding(identifier) {
|
|
12766
|
-
return
|
|
13098
|
+
return ASTUtils27.findVariable(
|
|
12767
13099
|
context.sourceCode.getScope(identifier),
|
|
12768
13100
|
identifier.name
|
|
12769
13101
|
);
|
|
@@ -12785,6 +13117,15 @@ var prefer_module_level_refined_schema_default = createRule({
|
|
|
12785
13117
|
return names[1] ?? null;
|
|
12786
13118
|
return null;
|
|
12787
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
|
+
}
|
|
12788
13129
|
function isSharedEnumDomain(node, factory) {
|
|
12789
13130
|
if (factory !== "enum") return false;
|
|
12790
13131
|
const [argument] = node.arguments;
|
|
@@ -12853,7 +13194,7 @@ var prefer_module_level_refined_schema_default = createRule({
|
|
|
12853
13194
|
const enclosing = outermostEnclosingFunction2(node);
|
|
12854
13195
|
if (enclosing === void 0) return;
|
|
12855
13196
|
const expression = schemaExpression2(node);
|
|
12856
|
-
if (readsReceiver2(expression) || buildsLocalizedText2(expression) || !closesOverNothing(expression, enclosing))
|
|
13197
|
+
if (hasEagerComputation(expression) || readsReceiver2(expression) || buildsLocalizedText2(expression) || !closesOverNothing(expression, enclosing))
|
|
12857
13198
|
return;
|
|
12858
13199
|
context.report({ node, messageId: "hoistRefinedSchema" });
|
|
12859
13200
|
}
|
|
@@ -12864,7 +13205,7 @@ var prefer_module_level_refined_schema_default = createRule({
|
|
|
12864
13205
|
// src/rules/prefer-multi-value-zod-literal.ts
|
|
12865
13206
|
import {
|
|
12866
13207
|
AST_NODE_TYPES as AST_NODE_TYPES54,
|
|
12867
|
-
ASTUtils as
|
|
13208
|
+
ASTUtils as ASTUtils28
|
|
12868
13209
|
} from "@typescript-eslint/utils";
|
|
12869
13210
|
var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
|
|
12870
13211
|
summary: "Use the Zod 4 multi-value literal API instead of a union of literal schemas.",
|
|
@@ -12884,7 +13225,7 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
|
|
|
12884
13225
|
outcome: "no-match",
|
|
12885
13226
|
files: [{
|
|
12886
13227
|
path: "src/schema.ts",
|
|
12887
|
-
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]);"
|
|
12888
13229
|
}],
|
|
12889
13230
|
focusPath: "src/schema.ts",
|
|
12890
13231
|
expectedCount: 0,
|
|
@@ -12896,7 +13237,7 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
|
|
|
12896
13237
|
outcome: "match",
|
|
12897
13238
|
files: [{
|
|
12898
13239
|
path: "src/schema.ts",
|
|
12899
|
-
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)]);"
|
|
12900
13241
|
}],
|
|
12901
13242
|
focusPath: "src/schema.ts",
|
|
12902
13243
|
expectedCount: 1,
|
|
@@ -12911,7 +13252,7 @@ function isStaticPrimitive(node, context) {
|
|
|
12911
13252
|
if (node.type === AST_NODE_TYPES54.TemplateLiteral && node.expressions.length === 0)
|
|
12912
13253
|
return true;
|
|
12913
13254
|
if (node.type === AST_NODE_TYPES54.Identifier && node.name === "undefined") {
|
|
12914
|
-
const binding =
|
|
13255
|
+
const binding = ASTUtils28.findVariable(
|
|
12915
13256
|
context.sourceCode.getScope(node),
|
|
12916
13257
|
node.name
|
|
12917
13258
|
);
|
|
@@ -12948,7 +13289,7 @@ var prefer_multi_value_zod_literal_default = createRule({
|
|
|
12948
13289
|
const zodBindings = /* @__PURE__ */ new Set();
|
|
12949
13290
|
const zod4Bindings = /* @__PURE__ */ new Set();
|
|
12950
13291
|
function resolvedBinding(identifier) {
|
|
12951
|
-
return
|
|
13292
|
+
return ASTUtils28.findVariable(
|
|
12952
13293
|
context.sourceCode.getScope(identifier),
|
|
12953
13294
|
identifier.name
|
|
12954
13295
|
);
|
|
@@ -13055,11 +13396,11 @@ import { AST_NODE_TYPES as AST_NODE_TYPES56 } from "@typescript-eslint/utils";
|
|
|
13055
13396
|
var PREFER_NAMED_COMPLEX_RETURN_TYPE_DOCUMENTATION = {
|
|
13056
13397
|
summary: "Prefer a named contract for structurally complex function return types.",
|
|
13057
13398
|
rationale: "A large inline return annotation hides a reusable domain concept and makes signatures difficult to scan.",
|
|
13058
|
-
remediation: "
|
|
13399
|
+
remediation: "Name the complex nested shape while preserving its generic wrappers and type parameters; reference the named contract from the return annotation.",
|
|
13059
13400
|
category: "maintainability",
|
|
13060
13401
|
limitations: [
|
|
13061
13402
|
"Only explicit object types with at least three members and unions with at least three object variants are reported.",
|
|
13062
|
-
"
|
|
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."
|
|
13063
13404
|
],
|
|
13064
13405
|
examples: [
|
|
13065
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 },
|
|
@@ -13110,7 +13451,7 @@ var prefer_named_complex_return_type_default = createRule({
|
|
|
13110
13451
|
});
|
|
13111
13452
|
|
|
13112
13453
|
// src/rules/prefer-native-random-uuid.ts
|
|
13113
|
-
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";
|
|
13114
13455
|
var PREFER_NATIVE_RANDOM_UUID_DOCUMENTATION = {
|
|
13115
13456
|
summary: "Prefer `globalThis.crypto.randomUUID()` over resolved zero-argument UUID v4 bindings from the `uuid` package.",
|
|
13116
13457
|
rationale: "The platform implementation avoids an unnecessary dependency for standard random UUID generation.",
|
|
@@ -13146,14 +13487,14 @@ var prefer_native_random_uuid_default = createRule({
|
|
|
13146
13487
|
const directBindings = /* @__PURE__ */ new Set();
|
|
13147
13488
|
const namespaceBindings = /* @__PURE__ */ new Set();
|
|
13148
13489
|
function resolve2(identifier) {
|
|
13149
|
-
return
|
|
13490
|
+
return ASTUtils29.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
13150
13491
|
}
|
|
13151
13492
|
function record(identifier, destination) {
|
|
13152
13493
|
const variable = resolve2(identifier);
|
|
13153
13494
|
if (variable !== null) destination.add(variable);
|
|
13154
13495
|
}
|
|
13155
13496
|
function report2(node) {
|
|
13156
|
-
const globalBinding =
|
|
13497
|
+
const globalBinding = ASTUtils29.findVariable(context.sourceCode.getScope(node), "globalThis");
|
|
13157
13498
|
const canSuggest = (globalBinding?.defs.length ?? 0) === 0 && context.sourceCode.getCommentsInside(node).length === 0;
|
|
13158
13499
|
context.report({
|
|
13159
13500
|
node,
|
|
@@ -13211,16 +13552,18 @@ var prefer_native_random_uuid_default = createRule({
|
|
|
13211
13552
|
});
|
|
13212
13553
|
|
|
13213
13554
|
// src/rules/prefer-node-crypto-hash.ts
|
|
13214
|
-
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";
|
|
13215
13556
|
var PREFER_NODE_CRYPTO_HASH_DOCUMENTATION = {
|
|
13216
13557
|
summary: "Prefer the modern one-shot node:crypto hash API when streaming state is unnecessary.",
|
|
13217
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.",
|
|
13218
|
-
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.",
|
|
13219
13560
|
category: "performance",
|
|
13220
13561
|
limitations: [
|
|
13221
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.",
|
|
13222
|
-
"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."
|
|
13223
13565
|
],
|
|
13566
|
+
references: ["https://nodejs.org/api/crypto.html#cryptohashalgorithm-data-options"],
|
|
13224
13567
|
examples: [
|
|
13225
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 },
|
|
13226
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 }
|
|
@@ -13267,7 +13610,7 @@ var prefer_node_crypto_hash_default = createRule({
|
|
|
13267
13610
|
const directBindings = /* @__PURE__ */ new Set();
|
|
13268
13611
|
const namespaceBindings = /* @__PURE__ */ new Set();
|
|
13269
13612
|
function resolve2(identifier) {
|
|
13270
|
-
return
|
|
13613
|
+
return ASTUtils30.findVariable(
|
|
13271
13614
|
context.sourceCode.getScope(identifier),
|
|
13272
13615
|
identifier.name
|
|
13273
13616
|
);
|
|
@@ -13341,7 +13684,7 @@ function isCreateHashCall(node, directBindings, namespaceBindings, resolve2) {
|
|
|
13341
13684
|
}
|
|
13342
13685
|
|
|
13343
13686
|
// src/rules/prefer-node-fs-promises.ts
|
|
13344
|
-
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";
|
|
13345
13688
|
var PREFER_NODE_FS_PROMISES_DOCUMENTATION = {
|
|
13346
13689
|
summary: "Prefer promise-based Node.js filesystem APIs over synchronous calls in production modules.",
|
|
13347
13690
|
rationale: "Synchronous filesystem work blocks the event loop and can stall unrelated daemon, server, and worker tasks.",
|
|
@@ -13349,6 +13692,7 @@ var PREFER_NODE_FS_PROMISES_DOCUMENTATION = {
|
|
|
13349
13692
|
category: "performance",
|
|
13350
13693
|
limitations: [
|
|
13351
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.",
|
|
13352
13696
|
"ESLint rule implementations under src/rules are excluded because visitor creation and execution are synchronous by contract.",
|
|
13353
13697
|
"Only statically identifiable node:fs loads are inspected; filesystem objects passed through arbitrary functions or assignments require type-aware analysis."
|
|
13354
13698
|
],
|
|
@@ -13367,14 +13711,14 @@ function memberName5(node) {
|
|
|
13367
13711
|
function unwrapAwait2(node) {
|
|
13368
13712
|
return node.type === AST_NODE_TYPES59.AwaitExpression ? node.argument : node;
|
|
13369
13713
|
}
|
|
13370
|
-
function isFsLoader(node) {
|
|
13714
|
+
function isFsLoader(node, isGlobal) {
|
|
13371
13715
|
const expression = unwrapAwait2(node);
|
|
13372
13716
|
if (expression.type === AST_NODE_TYPES59.ImportExpression) return isFsSpecifier(expression.source);
|
|
13373
13717
|
if (expression.type !== AST_NODE_TYPES59.CallExpression || expression.arguments.length !== 1) return false;
|
|
13374
13718
|
const [argument] = expression.arguments;
|
|
13375
13719
|
if (argument === void 0 || argument.type === AST_NODE_TYPES59.SpreadElement || !isFsSpecifier(argument)) return false;
|
|
13376
|
-
if (expression.callee.type === AST_NODE_TYPES59.Identifier) return expression.callee.name === "require";
|
|
13377
|
-
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";
|
|
13378
13722
|
}
|
|
13379
13723
|
function isFsSpecifier(node) {
|
|
13380
13724
|
return node.type === AST_NODE_TYPES59.Literal && (node.value === "node:fs" || node.value === "fs");
|
|
@@ -13401,13 +13745,26 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13401
13745
|
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text) || normalizedFilename.includes("src/rules/"))
|
|
13402
13746
|
return {};
|
|
13403
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
|
+
};
|
|
13404
13761
|
return {
|
|
13405
13762
|
ImportDeclaration(node) {
|
|
13406
13763
|
if (node.source.value !== "node:fs" && node.source.value !== "fs") return;
|
|
13407
13764
|
const synchronousImports = [];
|
|
13408
13765
|
for (const specifier of node.specifiers) {
|
|
13409
13766
|
if (specifier.type === AST_NODE_TYPES59.ImportNamespaceSpecifier || specifier.type === AST_NODE_TYPES59.ImportDefaultSpecifier) {
|
|
13410
|
-
|
|
13767
|
+
recordNamespace(specifier.local);
|
|
13411
13768
|
continue;
|
|
13412
13769
|
}
|
|
13413
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);
|
|
@@ -13421,10 +13778,10 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13421
13778
|
}
|
|
13422
13779
|
},
|
|
13423
13780
|
VariableDeclarator(node) {
|
|
13424
|
-
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)))
|
|
13425
13782
|
return;
|
|
13426
13783
|
if (node.id.type === AST_NODE_TYPES59.Identifier) {
|
|
13427
|
-
|
|
13784
|
+
recordNamespace(node.id);
|
|
13428
13785
|
return;
|
|
13429
13786
|
}
|
|
13430
13787
|
if (node.id.type !== AST_NODE_TYPES59.ObjectPattern) return;
|
|
@@ -13445,7 +13802,7 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13445
13802
|
const name = memberName5(node);
|
|
13446
13803
|
if (name?.endsWith("Sync") !== true) return;
|
|
13447
13804
|
const object = unwrapAwait2(node.object);
|
|
13448
|
-
if (object.type === AST_NODE_TYPES59.Identifier &&
|
|
13805
|
+
if (object.type === AST_NODE_TYPES59.Identifier && isNamespace(object) || isFsLoader(object, isGlobal)) {
|
|
13449
13806
|
context.report({ node, messageId: "preferAsyncFs", data: { name } });
|
|
13450
13807
|
}
|
|
13451
13808
|
}
|
|
@@ -13454,13 +13811,13 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13454
13811
|
});
|
|
13455
13812
|
|
|
13456
13813
|
// src/rules/prefer-non-nullable-collection.ts
|
|
13457
|
-
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";
|
|
13458
13815
|
var PREFER_NON_NULLABLE_COLLECTION_DOCUMENTATION = {
|
|
13459
|
-
summary: "Suggest
|
|
13816
|
+
summary: "Suggest reviewing nullish arrays that use local empty-array defaults or a shared null-or-empty guard.",
|
|
13460
13817
|
rationale: "A redundant nullish collection state spreads defaults and guards through consumers without carrying information.",
|
|
13461
13818
|
remediation: "Use a non-null collection type and normalize omitted input to an empty collection at the boundary.",
|
|
13462
13819
|
category: "maintainability",
|
|
13463
|
-
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."],
|
|
13464
13821
|
examples: [
|
|
13465
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 },
|
|
13466
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 }
|
|
@@ -13529,22 +13886,6 @@ function sameAccess(node, access) {
|
|
|
13529
13886
|
}
|
|
13530
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;
|
|
13531
13888
|
}
|
|
13532
|
-
function isNullGuard(node, access) {
|
|
13533
|
-
if (node.type === AST_NODE_TYPES60.UnaryExpression && node.operator === "!" && (sameAccess(node.argument, access) || optionalMemberLengthOf(node.argument, access))) return true;
|
|
13534
|
-
if (node.type !== AST_NODE_TYPES60.BinaryExpression || !["==", "==="].includes(node.operator)) {
|
|
13535
|
-
return false;
|
|
13536
|
-
}
|
|
13537
|
-
const nullish = (value) => value.type === AST_NODE_TYPES60.Literal && value.value === null || value.type === AST_NODE_TYPES60.Identifier && value.name === "undefined";
|
|
13538
|
-
return sameAccess(node.left, access) && nullish(node.right) || sameAccess(node.right, access) && nullish(node.left);
|
|
13539
|
-
}
|
|
13540
|
-
function isEmptyGuard(node, access) {
|
|
13541
|
-
if (node.type === AST_NODE_TYPES60.UnaryExpression && node.operator === "!" && memberLengthOf(node.argument, access)) return true;
|
|
13542
|
-
if (node.type !== AST_NODE_TYPES60.BinaryExpression || !["==", "===", "<="].includes(node.operator)) {
|
|
13543
|
-
return false;
|
|
13544
|
-
}
|
|
13545
|
-
const zero = (value) => value.type === AST_NODE_TYPES60.Literal && value.value === 0;
|
|
13546
|
-
return memberLengthOf(node.left, access) && zero(node.right) || memberLengthOf(node.right, access) && zero(node.left);
|
|
13547
|
-
}
|
|
13548
13889
|
function memberLengthOf(node, access) {
|
|
13549
13890
|
const target = node.type === AST_NODE_TYPES60.ChainExpression ? node.expression : node;
|
|
13550
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);
|
|
@@ -13559,7 +13900,24 @@ function hasEquivalentLeadingGuard(fn, access, visitorKeys) {
|
|
|
13559
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);
|
|
13560
13901
|
if (!terminating) return false;
|
|
13561
13902
|
if (contains(first.consequent, visitorKeys, (node) => sameAccess(node, access))) return false;
|
|
13562
|
-
|
|
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);
|
|
13563
13921
|
}
|
|
13564
13922
|
function contains(node, visitorKeys, predicate) {
|
|
13565
13923
|
if (predicate(node)) return true;
|
|
@@ -13584,20 +13942,20 @@ function directlyCoalesced(node) {
|
|
|
13584
13942
|
return parent?.type === AST_NODE_TYPES60.LogicalExpression && parent.left === node && (parent.operator === "??" || parent.operator === "||") && emptyArray(parent.right);
|
|
13585
13943
|
}
|
|
13586
13944
|
function identifierIsOnlyCoalesced(context, binding, fn) {
|
|
13587
|
-
const variable =
|
|
13945
|
+
const variable = ASTUtils32.findVariable(context.sourceCode.getScope(binding), binding.name);
|
|
13588
13946
|
if (variable === null || variable.references.length === 0) return false;
|
|
13589
13947
|
return variable.references.every(
|
|
13590
13948
|
(reference) => belongsToFunction(reference.identifier, fn) && directlyCoalesced(reference.identifier)
|
|
13591
13949
|
);
|
|
13592
13950
|
}
|
|
13593
13951
|
function memberIsOnlyCoalesced(context, object, property, fn) {
|
|
13594
|
-
const variable =
|
|
13952
|
+
const variable = ASTUtils32.findVariable(context.sourceCode.getScope(object), object.name);
|
|
13595
13953
|
if (variable === null) return false;
|
|
13596
13954
|
const accesses = variable.references.flatMap((reference) => {
|
|
13597
13955
|
if (!belongsToFunction(reference.identifier, fn)) return [null];
|
|
13598
13956
|
const parent = reference.identifier.parent;
|
|
13599
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];
|
|
13600
|
-
return [];
|
|
13958
|
+
return parent?.type === AST_NODE_TYPES60.MemberExpression && parent.object === reference.identifier ? [] : [null];
|
|
13601
13959
|
});
|
|
13602
13960
|
return accesses.length > 0 && accesses.every((access) => access !== null && directlyCoalesced(access));
|
|
13603
13961
|
}
|
|
@@ -13607,11 +13965,11 @@ var prefer_non_nullable_collection_default = createRule({
|
|
|
13607
13965
|
meta: {
|
|
13608
13966
|
type: "suggestion",
|
|
13609
13967
|
docs: {
|
|
13610
|
-
description: "Suggest
|
|
13968
|
+
description: "Suggest reviewing nullish arrays that use local empty-array defaults or a shared null-or-empty guard."
|
|
13611
13969
|
},
|
|
13612
13970
|
schema: [],
|
|
13613
13971
|
messages: {
|
|
13614
|
-
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."
|
|
13615
13973
|
}
|
|
13616
13974
|
},
|
|
13617
13975
|
defaultOptions: [],
|
|
@@ -13701,7 +14059,7 @@ var prefer_non_nullable_collection_default = createRule({
|
|
|
13701
14059
|
// src/rules/prefer-nullish-filter-predicate.ts
|
|
13702
14060
|
import {
|
|
13703
14061
|
AST_NODE_TYPES as AST_NODE_TYPES61,
|
|
13704
|
-
ASTUtils as
|
|
14062
|
+
ASTUtils as ASTUtils33,
|
|
13705
14063
|
ESLintUtils as ESLintUtils5
|
|
13706
14064
|
} from "@typescript-eslint/utils";
|
|
13707
14065
|
import ts3 from "typescript";
|
|
@@ -13747,7 +14105,7 @@ var PREFER_NULLISH_FILTER_PREDICATE_DOCUMENTATION = {
|
|
|
13747
14105
|
]
|
|
13748
14106
|
};
|
|
13749
14107
|
function isUnshadowedBoolean(node, context) {
|
|
13750
|
-
const variable =
|
|
14108
|
+
const variable = ASTUtils33.findVariable(context.sourceCode.getScope(node), node.name);
|
|
13751
14109
|
return variable === null || variable.defs.length === 0;
|
|
13752
14110
|
}
|
|
13753
14111
|
function isBuiltinArrayFilter(node, services) {
|
|
@@ -13806,7 +14164,7 @@ function isProvablyTruthy(type, checker) {
|
|
|
13806
14164
|
}
|
|
13807
14165
|
function availableParameterName(node, context) {
|
|
13808
14166
|
for (const name of ["value", "item", "element", "candidate"]) {
|
|
13809
|
-
if (
|
|
14167
|
+
if (ASTUtils33.findVariable(context.sourceCode.getScope(node), name) === null) return name;
|
|
13810
14168
|
}
|
|
13811
14169
|
return null;
|
|
13812
14170
|
}
|
|
@@ -13860,7 +14218,7 @@ var prefer_nullish_filter_predicate_default = createRule({
|
|
|
13860
14218
|
|
|
13861
14219
|
// src/rules/prefer-await-in-async-return.ts
|
|
13862
14220
|
import {
|
|
13863
|
-
ASTUtils as
|
|
14221
|
+
ASTUtils as ASTUtils34,
|
|
13864
14222
|
ESLintUtils as ESLintUtils6,
|
|
13865
14223
|
AST_NODE_TYPES as AST_NODE_TYPES62
|
|
13866
14224
|
} from "@typescript-eslint/utils";
|
|
@@ -13868,12 +14226,12 @@ import * as ts4 from "typescript";
|
|
|
13868
14226
|
var PREFER_AWAIT_IN_ASYNC_RETURN_DOCUMENTATION = {
|
|
13869
14227
|
summary: "Prefer explicit `await` when an async function directly returns one typed Promise `.then` transform.",
|
|
13870
14228
|
rationale: "Mixing a directly returned Promise callback into otherwise async control flow makes sequencing and failures harder to read.",
|
|
13871
|
-
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.",
|
|
13872
14230
|
category: "maintainability",
|
|
13873
14231
|
since: "15.6.3",
|
|
13874
14232
|
limitations: [
|
|
13875
14233
|
"Only a single directly returned `.then` call with an inline callback is checked.",
|
|
13876
|
-
"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.",
|
|
13877
14235
|
"Direct loader callbacks passed to resolved `React.lazy` and `next/dynamic` imports are excluded because returning the module Promise is their framework contract."
|
|
13878
14236
|
],
|
|
13879
14237
|
examples: [
|
|
@@ -13974,13 +14332,13 @@ var prefer_await_in_async_return_default = createRule({
|
|
|
13974
14332
|
if (services === null) return {};
|
|
13975
14333
|
const frameworkLoaders = /* @__PURE__ */ new Set();
|
|
13976
14334
|
const rememberFrameworkLoader = (identifier) => {
|
|
13977
|
-
const variable =
|
|
14335
|
+
const variable = ASTUtils34.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
13978
14336
|
if (variable !== null) frameworkLoaders.add(variable);
|
|
13979
14337
|
};
|
|
13980
14338
|
const isFrameworkLoaderCallback = (owner) => {
|
|
13981
14339
|
const parent = owner.parent;
|
|
13982
14340
|
if (parent.type !== AST_NODE_TYPES62.CallExpression || parent.arguments[0] !== owner || parent.callee.type !== AST_NODE_TYPES62.Identifier) return false;
|
|
13983
|
-
const variable =
|
|
14341
|
+
const variable = ASTUtils34.findVariable(context.sourceCode.getScope(parent.callee), parent.callee.name);
|
|
13984
14342
|
return variable !== null && frameworkLoaders.has(variable);
|
|
13985
14343
|
};
|
|
13986
14344
|
return {
|
|
@@ -14010,16 +14368,21 @@ var prefer_await_in_async_return_default = createRule({
|
|
|
14010
14368
|
});
|
|
14011
14369
|
|
|
14012
14370
|
// src/rules/prefer-schema-for-api-payload.ts
|
|
14013
|
-
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";
|
|
14014
14372
|
var PREFER_SCHEMA_FOR_API_PAYLOAD_DOCUMENTATION = {
|
|
14015
14373
|
summary: "Require Zod (or similar) schema validation on `response.json()` / `JSON.parse()` results before property access.",
|
|
14016
14374
|
rationale: "External JSON is untrusted at runtime even when its expected TypeScript shape is known statically.",
|
|
14017
|
-
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.",
|
|
14018
14376
|
category: "correctness",
|
|
14019
|
-
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
|
+
],
|
|
14020
14383
|
examples: [
|
|
14021
|
-
{ 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 },
|
|
14022
|
-
{ 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 }
|
|
14023
14386
|
]
|
|
14024
14387
|
};
|
|
14025
14388
|
var unwrap6 = (node) => {
|
|
@@ -14044,7 +14407,7 @@ var isSchemaParseReference = (node) => {
|
|
|
14044
14407
|
const inner = unwrap6(node);
|
|
14045
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");
|
|
14046
14409
|
};
|
|
14047
|
-
var isRawPayloadSource = (node, isKnownLocalText) => {
|
|
14410
|
+
var isRawPayloadSource = (node, context, isKnownLocalText) => {
|
|
14048
14411
|
let current = unwrap6(node);
|
|
14049
14412
|
if (current === null) return false;
|
|
14050
14413
|
if (current.type === AST_NODE_TYPES63.AwaitExpression) {
|
|
@@ -14062,13 +14425,31 @@ var isRawPayloadSource = (node, isKnownLocalText) => {
|
|
|
14062
14425
|
return false;
|
|
14063
14426
|
}
|
|
14064
14427
|
if (property.name === "json") {
|
|
14065
|
-
return
|
|
14428
|
+
return !callee.computed && current.arguments.length === 0 && isResponseSource(callee.object, context);
|
|
14066
14429
|
}
|
|
14067
14430
|
if (PROMISE_CHAIN_METHODS.has(property.name)) {
|
|
14068
|
-
return !current.arguments.some(isSchemaParseReference) && isRawPayloadSource(callee.object);
|
|
14431
|
+
return !current.arguments.some(isSchemaParseReference) && isRawPayloadSource(callee.object, context, isKnownLocalText);
|
|
14069
14432
|
}
|
|
14070
14433
|
const object = unwrap6(callee.object);
|
|
14071
|
-
|
|
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);
|
|
14072
14453
|
};
|
|
14073
14454
|
var FILE_READ_RE = /^(readFile|readFileSync|readJson|readJsonSync|readJSON)$/;
|
|
14074
14455
|
var isDirectLocalFileRead = (node) => {
|
|
@@ -14334,7 +14715,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14334
14715
|
},
|
|
14335
14716
|
schema: [],
|
|
14336
14717
|
messages: {
|
|
14337
|
-
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."
|
|
14338
14719
|
}
|
|
14339
14720
|
},
|
|
14340
14721
|
defaultOptions: [],
|
|
@@ -14345,6 +14726,40 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14345
14726
|
const unvalidatedVariables = /* @__PURE__ */ new Set();
|
|
14346
14727
|
const aliasGroups = /* @__PURE__ */ new Map();
|
|
14347
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
|
+
};
|
|
14348
14763
|
const localFileTextRef = (node, scope) => {
|
|
14349
14764
|
const unwrapped = unwrap6(node);
|
|
14350
14765
|
if (unwrapped?.type !== AST_NODE_TYPES63.Identifier) return null;
|
|
@@ -14361,6 +14776,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14361
14776
|
};
|
|
14362
14777
|
const clearBinding = (variable) => {
|
|
14363
14778
|
unvalidatedVariables.delete(variable);
|
|
14779
|
+
namedGuards.delete(variable);
|
|
14364
14780
|
const group = aliasGroups.get(variable);
|
|
14365
14781
|
aliasGroups.delete(variable);
|
|
14366
14782
|
group?.delete(variable);
|
|
@@ -14395,10 +14811,24 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14395
14811
|
};
|
|
14396
14812
|
const isFullyNarrowedPattern = (declarator) => {
|
|
14397
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
|
+
};
|
|
14398
14825
|
return declared.length > 0 && declared.every(
|
|
14399
|
-
(variable) => variable.references.some(
|
|
14400
|
-
|
|
14401
|
-
|
|
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
|
+
})
|
|
14402
14832
|
);
|
|
14403
14833
|
};
|
|
14404
14834
|
const trackInitializer = (declarator, scope) => {
|
|
@@ -14406,7 +14836,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14406
14836
|
const variable = declaredVars[0];
|
|
14407
14837
|
if (variable === void 0) return;
|
|
14408
14838
|
const localText = (candidate2) => localFileTextRef(candidate2, scope) !== null;
|
|
14409
|
-
if (isRawPayloadSource(declarator.init, localText)) {
|
|
14839
|
+
if (isRawPayloadSource(declarator.init, context, localText)) {
|
|
14410
14840
|
trackRawBinding(variable);
|
|
14411
14841
|
return;
|
|
14412
14842
|
}
|
|
@@ -14427,6 +14857,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14427
14857
|
if (node.id.type === AST_NODE_TYPES63.ObjectPattern || node.id.type === AST_NODE_TYPES63.ArrayPattern) {
|
|
14428
14858
|
if (isRawPayloadSource(
|
|
14429
14859
|
node.init,
|
|
14860
|
+
context,
|
|
14430
14861
|
(candidate2) => localFileTextRef(candidate2, scope) !== null
|
|
14431
14862
|
)) {
|
|
14432
14863
|
if (!isFullyNarrowedPattern(node)) {
|
|
@@ -14446,7 +14877,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14446
14877
|
if (variable === null) return;
|
|
14447
14878
|
const isLocalText = (candidate2) => localFileTextRef(candidate2, scope) !== null;
|
|
14448
14879
|
updateLocalFileText(variable, node.right, scope);
|
|
14449
|
-
if (isRawPayloadSource(node.right, isLocalText)) {
|
|
14880
|
+
if (isRawPayloadSource(node.right, context, isLocalText)) {
|
|
14450
14881
|
trackRawBinding(variable);
|
|
14451
14882
|
} else {
|
|
14452
14883
|
const source = unvalidatedVariableRef(node.right, scope, unvalidatedVariables);
|
|
@@ -14458,6 +14889,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14458
14889
|
if (node.left.type === AST_NODE_TYPES63.ObjectPattern || node.left.type === AST_NODE_TYPES63.ArrayPattern) {
|
|
14459
14890
|
if (isRawPayloadSource(
|
|
14460
14891
|
node.right,
|
|
14892
|
+
context,
|
|
14461
14893
|
(candidate2) => localFileTextRef(candidate2, scope) !== null
|
|
14462
14894
|
)) {
|
|
14463
14895
|
context.report({
|
|
@@ -14487,7 +14919,13 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14487
14919
|
continue;
|
|
14488
14920
|
}
|
|
14489
14921
|
const variable = findVariable2(scope, unwrapped.name);
|
|
14490
|
-
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
|
+
}
|
|
14491
14929
|
}
|
|
14492
14930
|
},
|
|
14493
14931
|
MemberExpression(node) {
|
|
@@ -14497,6 +14935,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14497
14935
|
const obj = unwrap6(node.object);
|
|
14498
14936
|
if (isRawPayloadSource(
|
|
14499
14937
|
obj,
|
|
14938
|
+
context,
|
|
14500
14939
|
(candidate2) => localFileTextRef(candidate2, scope) !== null
|
|
14501
14940
|
)) {
|
|
14502
14941
|
const parent = node.parent;
|
|
@@ -14508,6 +14947,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14508
14947
|
}
|
|
14509
14948
|
const variable = obj?.type === AST_NODE_TYPES63.Identifier ? unvalidatedVariableRef(obj, scope, unvalidatedVariables) : null;
|
|
14510
14949
|
if (variable !== null && obj?.type === AST_NODE_TYPES63.Identifier) {
|
|
14950
|
+
if (namedGuards.get(variable)?.some((call) => guardDominates(node, call))) return;
|
|
14511
14951
|
if (isUseWithinValidatedBranch(node, obj.name)) {
|
|
14512
14952
|
return;
|
|
14513
14953
|
}
|
|
@@ -14527,13 +14967,13 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14527
14967
|
});
|
|
14528
14968
|
|
|
14529
14969
|
// src/rules/prefer-shared-zod-enum.ts
|
|
14530
|
-
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";
|
|
14531
14971
|
var PREFER_SHARED_ZOD_ENUM_DOCUMENTATION = {
|
|
14532
14972
|
summary: "Give literal Zod enum domains one reusable module-level schema.",
|
|
14533
14973
|
rationale: "Inline or repeated literal domains hide a reusable contract and allow equivalent fields to drift independently.",
|
|
14534
14974
|
remediation: "Declare a module-level named Zod enum schema and reuse it at each field or contract site.",
|
|
14535
14975
|
category: "maintainability",
|
|
14536
|
-
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."],
|
|
14537
14977
|
examples: [
|
|
14538
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 },
|
|
14539
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 }
|
|
@@ -14575,16 +15015,22 @@ var prefer_shared_zod_enum_default = createRule({
|
|
|
14575
15015
|
create(context) {
|
|
14576
15016
|
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
14577
15017
|
const zodBindings = /* @__PURE__ */ new Set();
|
|
15018
|
+
const bindingOf = (node) => ASTUtils36.findVariable(context.sourceCode.getScope(node), node.name);
|
|
14578
15019
|
const seen = /* @__PURE__ */ new Set();
|
|
14579
15020
|
return {
|
|
14580
15021
|
ImportDeclaration(node) {
|
|
14581
15022
|
if (!isZodModule(node.source.value)) return;
|
|
14582
15023
|
for (const specifier of node.specifiers) {
|
|
14583
|
-
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
|
+
}
|
|
14584
15028
|
}
|
|
14585
15029
|
},
|
|
14586
15030
|
CallExpression(node) {
|
|
14587
|
-
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;
|
|
14588
15034
|
const domain = literalDomain(node);
|
|
14589
15035
|
if (domain === null) return;
|
|
14590
15036
|
const key = JSON.stringify(domain);
|
|
@@ -15211,7 +15657,7 @@ var prefer_semantic_colors_default = createRule({
|
|
|
15211
15657
|
});
|
|
15212
15658
|
|
|
15213
15659
|
// src/rules/prefer-server-actions.ts
|
|
15214
|
-
import { ASTUtils as
|
|
15660
|
+
import { ASTUtils as ASTUtils37 } from "@typescript-eslint/utils";
|
|
15215
15661
|
var PREFER_SERVER_ACTIONS_DOCUMENTATION = {
|
|
15216
15662
|
summary: "Prefer Next.js Server Actions over same-origin API mutations.",
|
|
15217
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.",
|
|
@@ -15243,7 +15689,7 @@ function resolvesToGlobalFetch(context, identifier) {
|
|
|
15243
15689
|
function resolveNode(node, context) {
|
|
15244
15690
|
if (!node) return null;
|
|
15245
15691
|
if (node.type !== "Identifier") return node;
|
|
15246
|
-
const variable =
|
|
15692
|
+
const variable = ASTUtils37.findVariable(getScope(context, node), node.name);
|
|
15247
15693
|
const definition = variable?.defs.length === 1 ? variable.defs[0] : void 0;
|
|
15248
15694
|
if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init === null || variable?.references.some((reference) => reference.isWrite() && reference.init !== true)) return node;
|
|
15249
15695
|
if (definition.node.init.type === "ObjectExpression" && variable?.references.some((reference) => reference.identifier !== node && reference.init !== true)) return node;
|
|
@@ -15252,7 +15698,7 @@ function resolveNode(node, context) {
|
|
|
15252
15698
|
function isAxiosClient(node, context, seen = /* @__PURE__ */ new Set()) {
|
|
15253
15699
|
if (node.type !== "Identifier" || seen.has(node)) return false;
|
|
15254
15700
|
seen.add(node);
|
|
15255
|
-
const variable =
|
|
15701
|
+
const variable = ASTUtils37.findVariable(getScope(context, node), node.name);
|
|
15256
15702
|
const definition = variable?.defs.length === 1 ? variable.defs[0] : void 0;
|
|
15257
15703
|
if (definition === void 0 || variable?.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
15258
15704
|
if (variable?.references.some((reference) => {
|
|
@@ -15455,7 +15901,7 @@ var prefer_server_actions_default = createRule({
|
|
|
15455
15901
|
});
|
|
15456
15902
|
|
|
15457
15903
|
// src/rules/prefer-whole-object-assertion.ts
|
|
15458
|
-
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";
|
|
15459
15905
|
var MERGEABLE_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
|
|
15460
15906
|
var SYNTHETIC_LITERAL_MATCHERS = /* @__PURE__ */ new Map([
|
|
15461
15907
|
["toBeNull", "null"],
|
|
@@ -15562,6 +16008,13 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
15562
16008
|
return null;
|
|
15563
16009
|
}
|
|
15564
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;
|
|
15565
16018
|
if (actual === void 0 || actual.type !== AST_NODE_TYPES67.MemberExpression || actual.optional) {
|
|
15566
16019
|
return null;
|
|
15567
16020
|
}
|
|
@@ -15721,14 +16174,14 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
15721
16174
|
});
|
|
15722
16175
|
|
|
15723
16176
|
// src/rules/repeated-static-call-cases.ts
|
|
15724
|
-
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";
|
|
15725
16178
|
var REPEATED_STATIC_CALL_CASES_DOCUMENTATION = {
|
|
15726
|
-
summary: "
|
|
15727
|
-
rationale: "Copy-pasted cases obscure the input table and stop later cases from being reported
|
|
15728
|
-
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.",
|
|
15729
16182
|
category: "testing",
|
|
15730
16183
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
15731
|
-
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."],
|
|
15732
16185
|
examples: [
|
|
15733
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 },
|
|
15734
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 }
|
|
@@ -15747,7 +16200,7 @@ function staticMemberName5(node) {
|
|
|
15747
16200
|
return null;
|
|
15748
16201
|
}
|
|
15749
16202
|
function importedName6(identifier, context, modules) {
|
|
15750
|
-
const variable =
|
|
16203
|
+
const variable = ASTUtils39.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
15751
16204
|
if (variable === null || variable.defs.length === 0) return identifier.name;
|
|
15752
16205
|
for (const definition of variable.defs) {
|
|
15753
16206
|
if (definition.node.type !== AST_NODE_TYPES68.ImportSpecifier) continue;
|
|
@@ -15809,7 +16262,7 @@ function staticShape(node) {
|
|
|
15809
16262
|
return "dynamic";
|
|
15810
16263
|
}
|
|
15811
16264
|
}
|
|
15812
|
-
function assertionShape(statement, context) {
|
|
16265
|
+
function assertionShape(statement, context, callback) {
|
|
15813
16266
|
if (statement.type !== AST_NODE_TYPES68.ExpressionStatement || statement.expression.type !== AST_NODE_TYPES68.CallExpression) return null;
|
|
15814
16267
|
const matcherCall = statement.expression;
|
|
15815
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;
|
|
@@ -15821,6 +16274,10 @@ function assertionShape(statement, context) {
|
|
|
15821
16274
|
const expected = matcherCall.arguments[0];
|
|
15822
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;
|
|
15823
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;
|
|
15824
16281
|
const values = [...observed.arguments, expected].map((item) => context.sourceCode.getText(item)).join("\0");
|
|
15825
16282
|
return { statement, skeleton, values };
|
|
15826
16283
|
}
|
|
@@ -15840,9 +16297,9 @@ var repeated_static_call_cases_default = createRule({
|
|
|
15840
16297
|
documentation: REPEATED_STATIC_CALL_CASES_DOCUMENTATION,
|
|
15841
16298
|
meta: {
|
|
15842
16299
|
type: "suggestion",
|
|
15843
|
-
docs: { description:
|
|
16300
|
+
docs: { description: REPEATED_STATIC_CALL_CASES_DOCUMENTATION.summary },
|
|
15844
16301
|
schema: [],
|
|
15845
|
-
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." }
|
|
15846
16303
|
},
|
|
15847
16304
|
defaultOptions: [],
|
|
15848
16305
|
create(context) {
|
|
@@ -15877,7 +16334,7 @@ var repeated_static_call_cases_default = createRule({
|
|
|
15877
16334
|
run = [];
|
|
15878
16335
|
};
|
|
15879
16336
|
for (const statement of node.body.body) {
|
|
15880
|
-
const shape = assertionShape(statement, context);
|
|
16337
|
+
const shape = assertionShape(statement, context, node);
|
|
15881
16338
|
if (shape === null || run.length > 0 && run[0]?.skeleton !== shape.skeleton) flush();
|
|
15882
16339
|
if (shape !== null) run.push(shape);
|
|
15883
16340
|
}
|
|
@@ -16757,7 +17214,7 @@ var require_assert_never_default = createRule({
|
|
|
16757
17214
|
});
|
|
16758
17215
|
|
|
16759
17216
|
// src/rules/require-fetch-timeout.ts
|
|
16760
|
-
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";
|
|
16761
17218
|
var REQUIRE_FETCH_TIMEOUT_DOCUMENTATION = {
|
|
16762
17219
|
summary: "Require an explicit abort signal on locally analyzable global fetch calls.",
|
|
16763
17220
|
rationale: "An unbounded request can occupy work indefinitely when an upstream stalls.",
|
|
@@ -16839,7 +17296,7 @@ var require_fetch_timeout_default = createRule({
|
|
|
16839
17296
|
}
|
|
16840
17297
|
function resolvesToGlobal(identifier) {
|
|
16841
17298
|
const scope = context.sourceCode.getScope(identifier);
|
|
16842
|
-
const variable =
|
|
17299
|
+
const variable = ASTUtils40.findVariable(scope, identifier.name);
|
|
16843
17300
|
return variable === null || variable.defs.length === 0;
|
|
16844
17301
|
}
|
|
16845
17302
|
function isGlobalFetchCall2(callee) {
|
|
@@ -16849,7 +17306,7 @@ var require_fetch_timeout_default = createRule({
|
|
|
16849
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);
|
|
16850
17307
|
}
|
|
16851
17308
|
function localConstInitProvablyLacksSignal(identifier) {
|
|
16852
|
-
const variable =
|
|
17309
|
+
const variable = ASTUtils40.findVariable(
|
|
16853
17310
|
context.sourceCode.getScope(identifier),
|
|
16854
17311
|
identifier.name
|
|
16855
17312
|
);
|
|
@@ -16871,7 +17328,7 @@ var require_fetch_timeout_default = createRule({
|
|
|
16871
17328
|
function isForwardedRequest(argument) {
|
|
16872
17329
|
let value = argument;
|
|
16873
17330
|
if (value.type === AST_NODE_TYPES71.Identifier) {
|
|
16874
|
-
const binding =
|
|
17331
|
+
const binding = ASTUtils40.findVariable(context.sourceCode.getScope(value), value.name);
|
|
16875
17332
|
const definition = binding?.defs.length === 1 ? binding.defs[0] : void 0;
|
|
16876
17333
|
if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init === null || binding?.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
16877
17334
|
value = definition.node.init;
|
|
@@ -16899,10 +17356,12 @@ var require_fetch_timeout_default = createRule({
|
|
|
16899
17356
|
import { AST_NODE_TYPES as AST_NODE_TYPES72 } from "@typescript-eslint/utils";
|
|
16900
17357
|
var REQUIRE_INTERFACE_FOR_EXPORTED_CLASS_DOCUMENTATION = {
|
|
16901
17358
|
summary: "Require exported concrete classes with public behavior to declare a contract.",
|
|
16902
|
-
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.",
|
|
16903
17360
|
remediation: "Declare a focused interface and add an implements clause, or inherit from an intentional base contract.",
|
|
16904
17361
|
category: "architecture",
|
|
16905
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.",
|
|
16906
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.",
|
|
16907
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.",
|
|
16908
17367
|
"Static factories and data-only classes without public instance methods are outside the contract requirement."
|
|
@@ -16974,7 +17433,7 @@ var require_interface_for_exported_class_default = createRule({
|
|
|
16974
17433
|
},
|
|
16975
17434
|
defaultOptions: [],
|
|
16976
17435
|
create(context) {
|
|
16977
|
-
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 {};
|
|
16978
17437
|
return {
|
|
16979
17438
|
"Program:exit"(program) {
|
|
16980
17439
|
const classes = /* @__PURE__ */ new Map();
|
|
@@ -17581,7 +18040,7 @@ var require_port_for_service_default = createRule({
|
|
|
17581
18040
|
});
|
|
17582
18041
|
|
|
17583
18042
|
// src/rules/require-sql-access-class.ts
|
|
17584
|
-
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";
|
|
17585
18044
|
var DIRECT_EXECUTION_METHODS = /* @__PURE__ */ new Set([
|
|
17586
18045
|
"all",
|
|
17587
18046
|
"batch",
|
|
@@ -17635,12 +18094,13 @@ var MODEL_EXECUTION_METHODS = /* @__PURE__ */ new Set([
|
|
|
17635
18094
|
var DATABASE_NAMES = /^(?:db|database|connection|pool|prisma|query|transaction|tx)$/iu;
|
|
17636
18095
|
var REQUIRE_SQL_ACCESS_CLASS_DOCUMENTATION = {
|
|
17637
18096
|
summary: "Keep SQL reads and writes inside a class that receives its database dependency.",
|
|
17638
|
-
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.",
|
|
17639
18098
|
remediation: "Move the query into a repository or store class and inject the pool, connection, transaction, or typed database binding through its constructor.",
|
|
17640
18099
|
category: "architecture",
|
|
17641
18100
|
limitations: [
|
|
17642
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.",
|
|
17643
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.",
|
|
17644
18104
|
"Constructor injection inherited from a base class or transformed through a wrapper is not inferred by this syntax-only rule."
|
|
17645
18105
|
],
|
|
17646
18106
|
examples: [
|
|
@@ -17846,11 +18306,23 @@ var require_sql_access_class_default = createRule({
|
|
|
17846
18306
|
create(context) {
|
|
17847
18307
|
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text))
|
|
17848
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
|
+
}
|
|
17849
18320
|
return {
|
|
17850
18321
|
CallExpression(node) {
|
|
17851
18322
|
if (node.callee.type !== AST_NODE_TYPES74.MemberExpression)
|
|
17852
18323
|
return;
|
|
17853
18324
|
const method = memberName6(node.callee);
|
|
18325
|
+
if (knownNonDatabase(node.callee.object)) return;
|
|
17854
18326
|
if (method === null || !isDatabaseOperation(method, node.callee.object))
|
|
17855
18327
|
return;
|
|
17856
18328
|
const owner = owningClass2(node);
|
|
@@ -17871,6 +18343,8 @@ var REQUIRE_STATIC_NEXT_MATCHER_DOCUMENTATION = {
|
|
|
17871
18343
|
rationale: "Next.js must statically analyze matcher values at build time; computed values are ignored.",
|
|
17872
18344
|
remediation: "Write matcher strings, arrays, and object fields as literals in the exported config.",
|
|
17873
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"],
|
|
17874
18348
|
examples: [
|
|
17875
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 },
|
|
17876
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 }
|
|
@@ -17918,7 +18392,7 @@ var require_static_next_matcher_default = createRule({
|
|
|
17918
18392
|
},
|
|
17919
18393
|
schema: [],
|
|
17920
18394
|
messages: {
|
|
17921
|
-
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."
|
|
17922
18396
|
}
|
|
17923
18397
|
},
|
|
17924
18398
|
defaultOptions: [],
|
|
@@ -17954,7 +18428,7 @@ var require_static_next_matcher_default = createRule({
|
|
|
17954
18428
|
});
|
|
17955
18429
|
|
|
17956
18430
|
// src/rules/require-use-form-default-values.ts
|
|
17957
|
-
import { ASTUtils as
|
|
18431
|
+
import { ASTUtils as ASTUtils42 } from "@typescript-eslint/utils";
|
|
17958
18432
|
var REQUIRE_USE_FORM_DEFAULT_VALUES_DOCUMENTATION = {
|
|
17959
18433
|
summary: "react-hook-form useForm call without explicit initial or reactive values",
|
|
17960
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.",
|
|
@@ -18008,13 +18482,13 @@ var require_use_form_default_values_default = createRule({
|
|
|
18008
18482
|
if (node.source.value !== "react-hook-form") return;
|
|
18009
18483
|
for (const specifier of node.specifiers) {
|
|
18010
18484
|
if (specifier.type !== "ImportSpecifier" || (specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value) !== "useForm") continue;
|
|
18011
|
-
const variable =
|
|
18485
|
+
const variable = ASTUtils42.findVariable(context.sourceCode.getScope(specifier.local), specifier.local.name);
|
|
18012
18486
|
if (variable) importedHooks.add(variable);
|
|
18013
18487
|
}
|
|
18014
18488
|
},
|
|
18015
18489
|
CallExpression(node) {
|
|
18016
18490
|
if (node.callee.type !== "Identifier") return;
|
|
18017
|
-
const variable =
|
|
18491
|
+
const variable = ASTUtils42.findVariable(context.sourceCode.getScope(node.callee), node.callee.name);
|
|
18018
18492
|
const options = node.arguments[0];
|
|
18019
18493
|
if (!variable || !importedHooks.has(variable) || options !== void 0 && options.type !== "ObjectExpression" || hasInitializationOrUnknownOptions(options)) return;
|
|
18020
18494
|
context.report({ node, messageId: "requireUseFormDefaultValues" });
|
|
@@ -18093,7 +18567,7 @@ var require_use_server_in_actions_file_default = createRule({
|
|
|
18093
18567
|
// src/rules/require-zod-form-validation.ts
|
|
18094
18568
|
import {
|
|
18095
18569
|
AST_NODE_TYPES as AST_NODE_TYPES76,
|
|
18096
|
-
ASTUtils as
|
|
18570
|
+
ASTUtils as ASTUtils43
|
|
18097
18571
|
} from "@typescript-eslint/utils";
|
|
18098
18572
|
var REQUIRE_ZOD_FORM_VALIDATION_DOCUMENTATION = {
|
|
18099
18573
|
summary: "Require Zod validation (`Schema.parse(...)` / `Schema.safeParse(...)`) when reading values out of a `FormData` object.",
|
|
@@ -18102,7 +18576,8 @@ var REQUIRE_ZOD_FORM_VALIDATION_DOCUMENTATION = {
|
|
|
18102
18576
|
category: "security",
|
|
18103
18577
|
limitations: [
|
|
18104
18578
|
"Tests are excluded; imported schema-shaped names are trusted when their implementation is outside the linted file.",
|
|
18105
|
-
"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."
|
|
18106
18581
|
],
|
|
18107
18582
|
examples: [
|
|
18108
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 },
|
|
@@ -18161,7 +18636,7 @@ var require_zod_form_validation_default = createRule({
|
|
|
18161
18636
|
return {};
|
|
18162
18637
|
}
|
|
18163
18638
|
const zodBindings = /* @__PURE__ */ new Set();
|
|
18164
|
-
const resolvedBinding = (identifier) =>
|
|
18639
|
+
const resolvedBinding = (identifier) => ASTUtils43.findVariable(
|
|
18165
18640
|
context.sourceCode.getScope(identifier),
|
|
18166
18641
|
identifier.name
|
|
18167
18642
|
);
|
|
@@ -18217,6 +18692,15 @@ var require_zod_form_validation_default = createRule({
|
|
|
18217
18692
|
let parent = node.parent;
|
|
18218
18693
|
while (parent !== null && parent !== void 0) {
|
|
18219
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;
|
|
18220
18704
|
parent = parent.parent;
|
|
18221
18705
|
}
|
|
18222
18706
|
return null;
|
|
@@ -18396,13 +18880,14 @@ var require_zod_form_validation_default = createRule({
|
|
|
18396
18880
|
// src/rules/store-insert-requires-on-conflict.ts
|
|
18397
18881
|
import "@typescript-eslint/utils";
|
|
18398
18882
|
var STORE_INSERT_REQUIRES_ON_CONFLICT_DOCUMENTATION = {
|
|
18399
|
-
summary: "
|
|
18400
|
-
rationale: "
|
|
18401
|
-
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.",
|
|
18402
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."],
|
|
18403
18888
|
examples: [
|
|
18404
|
-
{ id: "conflict-safe-insert", title: "
|
|
18405
|
-
{ 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 }
|
|
18406
18891
|
]
|
|
18407
18892
|
};
|
|
18408
18893
|
var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
|
|
@@ -18414,14 +18899,18 @@ function owningCallableName(node) {
|
|
|
18414
18899
|
return current.id?.name ?? null;
|
|
18415
18900
|
}
|
|
18416
18901
|
if (current.type === "MethodDefinition") {
|
|
18417
|
-
return current.key.type === "Identifier" ? current.key.name : null;
|
|
18902
|
+
return !current.computed && current.key.type === "Identifier" ? current.key.name : null;
|
|
18418
18903
|
}
|
|
18419
18904
|
if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "VariableDeclarator" && current.parent.id.type === "Identifier") {
|
|
18420
18905
|
return current.parent.id.name;
|
|
18421
18906
|
}
|
|
18422
|
-
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") {
|
|
18423
18908
|
return current.parent.key.name;
|
|
18424
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
|
+
}
|
|
18425
18914
|
}
|
|
18426
18915
|
return null;
|
|
18427
18916
|
}
|
|
@@ -18432,11 +18921,11 @@ var store_insert_requires_on_conflict_default = createRule({
|
|
|
18432
18921
|
meta: {
|
|
18433
18922
|
type: "problem",
|
|
18434
18923
|
docs: {
|
|
18435
|
-
description: "
|
|
18924
|
+
description: "Review conflict handling for embedded inserts in replay-named callables."
|
|
18436
18925
|
},
|
|
18437
18926
|
schema: [],
|
|
18438
18927
|
messages: {
|
|
18439
|
-
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."
|
|
18440
18929
|
}
|
|
18441
18930
|
},
|
|
18442
18931
|
defaultOptions: [],
|
|
@@ -18449,7 +18938,7 @@ var store_insert_requires_on_conflict_default = createRule({
|
|
|
18449
18938
|
return;
|
|
18450
18939
|
}
|
|
18451
18940
|
const owner = owningCallableName(node);
|
|
18452
|
-
if (owner
|
|
18941
|
+
if (owner === null || !REPLAY_CONTRACT_NAME.test(owner)) {
|
|
18453
18942
|
return;
|
|
18454
18943
|
}
|
|
18455
18944
|
context.report({ node, messageId: "storeInsertRequiresOnConflict" });
|
|
@@ -18458,19 +18947,17 @@ var store_insert_requires_on_conflict_default = createRule({
|
|
|
18458
18947
|
});
|
|
18459
18948
|
|
|
18460
18949
|
// src/rules/stepdown.ts
|
|
18461
|
-
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";
|
|
18462
18951
|
var STEPDOWN_DOCUMENTATION = {
|
|
18463
18952
|
summary: "Place a private helper below its sole direct same-scope caller.",
|
|
18464
18953
|
rationale: "Caller-first ordering lets a reader follow the main flow before descending into implementation details.",
|
|
18465
|
-
remediation: "
|
|
18954
|
+
remediation: "Consider moving the private helper below its sole caller after reviewing initialization and reflection dependencies.",
|
|
18466
18955
|
category: "maintainability",
|
|
18467
|
-
autofix: "safe",
|
|
18468
18956
|
limitations: [
|
|
18469
18957
|
"Generated and test files, cycles, dynamic references, overload targets, and helpers with multiple callers are excluded.",
|
|
18470
|
-
"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.",
|
|
18471
18959
|
"Runtime class-field, static-block, computed-member, and decorator barriers are never crossed.",
|
|
18472
|
-
"
|
|
18473
|
-
"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."
|
|
18474
18961
|
],
|
|
18475
18962
|
examples: [
|
|
18476
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 },
|
|
@@ -18480,7 +18967,7 @@ var STEPDOWN_DOCUMENTATION = {
|
|
|
18480
18967
|
function isFunction(node) {
|
|
18481
18968
|
return node.type === AST_NODE_TYPES77.ArrowFunctionExpression || node.type === AST_NODE_TYPES77.FunctionDeclaration || node.type === AST_NODE_TYPES77.FunctionExpression;
|
|
18482
18969
|
}
|
|
18483
|
-
function reportMisordered(context, candidates, scopeDefinitions, calls, pinned, canMove = () => true
|
|
18970
|
+
function reportMisordered(context, candidates, scopeDefinitions, calls, pinned, canMove = () => true) {
|
|
18484
18971
|
const byName = new Map(scopeDefinitions.map((definition) => [definition.name, definition]));
|
|
18485
18972
|
const cycles = cycleComponents(calls);
|
|
18486
18973
|
const callers = /* @__PURE__ */ new Map();
|
|
@@ -18499,12 +18986,10 @@ function reportMisordered(context, candidates, scopeDefinitions, calls, pinned,
|
|
|
18499
18986
|
if (callerName2 === void 0 || cycles.has(helper.name) && cycles.get(helper.name) === cycles.get(callerName2)) continue;
|
|
18500
18987
|
const caller = byName.get(callerName2);
|
|
18501
18988
|
if (caller === void 0 || helper.node.range[0] >= caller.node.range[0] || !canMove(helper, caller)) continue;
|
|
18502
|
-
const fix = makeFix?.(helper, caller);
|
|
18503
18989
|
context.report({
|
|
18504
18990
|
node: helper.node,
|
|
18505
18991
|
messageId: "helperAboveOnlyCaller",
|
|
18506
|
-
data: { helper: helper.name, caller: callerName2 }
|
|
18507
|
-
...fix === void 0 ? {} : { fix }
|
|
18992
|
+
data: { helper: helper.name, caller: callerName2 }
|
|
18508
18993
|
});
|
|
18509
18994
|
}
|
|
18510
18995
|
}
|
|
@@ -18663,7 +19148,7 @@ function methodName(node) {
|
|
|
18663
19148
|
return !node.computed && node.key.type === AST_NODE_TYPES77.Identifier ? node.key.name : null;
|
|
18664
19149
|
}
|
|
18665
19150
|
function referencedMethod(context, node, classVariables) {
|
|
18666
|
-
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;
|
|
18667
19152
|
const isClassReference = objectVariable !== null && classVariables.has(objectVariable);
|
|
18668
19153
|
if (node.object.type !== AST_NODE_TYPES77.ThisExpression && !isClassReference) return null;
|
|
18669
19154
|
if (node.property.type === AST_NODE_TYPES77.PrivateIdentifier) return `#${node.property.name}`;
|
|
@@ -18716,11 +19201,11 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18716
19201
|
const pinned = /* @__PURE__ */ new Set();
|
|
18717
19202
|
const classVariables = /* @__PURE__ */ new Set();
|
|
18718
19203
|
if (node.id !== null) {
|
|
18719
|
-
const internal =
|
|
19204
|
+
const internal = ASTUtils44.findVariable(context.sourceCode.getScope(node), node.id.name);
|
|
18720
19205
|
if (internal !== null) classVariables.add(internal);
|
|
18721
19206
|
}
|
|
18722
19207
|
if (node.type === AST_NODE_TYPES77.ClassExpression && node.parent.type === AST_NODE_TYPES77.VariableDeclarator && node.parent.id.type === AST_NODE_TYPES77.Identifier) {
|
|
18723
|
-
const outer =
|
|
19208
|
+
const outer = ASTUtils44.findVariable(context.sourceCode.getScope(node.parent), node.parent.id.name);
|
|
18724
19209
|
if (outer !== null) classVariables.add(outer);
|
|
18725
19210
|
}
|
|
18726
19211
|
for (const method of methods) {
|
|
@@ -18756,7 +19241,7 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18756
19241
|
return;
|
|
18757
19242
|
}
|
|
18758
19243
|
if (binding.type !== AST_NODE_TYPES77.Identifier) return;
|
|
18759
|
-
const variable =
|
|
19244
|
+
const variable = ASTUtils44.findVariable(context.sourceCode.getScope(binding), binding.name);
|
|
18760
19245
|
if (variable !== null) {
|
|
18761
19246
|
methodClassVariables.add(variable);
|
|
18762
19247
|
methodAliases.add(variable);
|
|
@@ -18786,7 +19271,7 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18786
19271
|
return;
|
|
18787
19272
|
}
|
|
18788
19273
|
if (!privateNames.has(target)) return;
|
|
18789
|
-
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;
|
|
18790
19275
|
if (objectVariable !== null && methodAliases.has(objectVariable)) {
|
|
18791
19276
|
pinned.add(target);
|
|
18792
19277
|
return;
|
|
@@ -18831,37 +19316,7 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18831
19316
|
if (helperIndex === void 0 || callerIndex === void 0) return false;
|
|
18832
19317
|
return runtimeBarrierPrefix[callerIndex + 1] === runtimeBarrierPrefix[helperIndex + 1];
|
|
18833
19318
|
};
|
|
18834
|
-
|
|
18835
|
-
for (const [caller, callees] of calls) {
|
|
18836
|
-
for (const callee of callees) {
|
|
18837
|
-
if (callee === caller) continue;
|
|
18838
|
-
const callers = incoming.get(callee) ?? /* @__PURE__ */ new Set();
|
|
18839
|
-
callers.add(caller);
|
|
18840
|
-
incoming.set(callee, callers);
|
|
18841
|
-
}
|
|
18842
|
-
}
|
|
18843
|
-
reportMisordered(context, definitions, scopeDefinitions, calls, pinned, canMove, (helper, caller) => {
|
|
18844
|
-
if (!canMove(helper, caller)) return void 0;
|
|
18845
|
-
const helperCallsAnother = [...calls.get(helper.name) ?? []].some((callee) => callee !== helper.name);
|
|
18846
|
-
const callerIsAnotherHelper = [...incoming.get(caller.name) ?? []].some((name) => name !== helper.name);
|
|
18847
|
-
if (helperCallsAnother || callerIsAnotherHelper) return void 0;
|
|
18848
|
-
const helperMember = helper.node;
|
|
18849
|
-
const callerMember = caller.node;
|
|
18850
|
-
const helperIndex = memberIndexes.get(helperMember);
|
|
18851
|
-
if (helperIndex === void 0) return void 0;
|
|
18852
|
-
const next = node.body.body[helperIndex + 1];
|
|
18853
|
-
const suffixEnd = next?.range[0] ?? node.body.range[1] - 1;
|
|
18854
|
-
const suffix = context.sourceCode.text.slice(helperMember.range[1], suffixEnd);
|
|
18855
|
-
if (!/^\s*$/u.test(suffix)) return void 0;
|
|
18856
|
-
const previous = node.body.body[helperIndex - 1];
|
|
18857
|
-
const prefixStart = previous?.range[1] ?? node.body.range[0] + 1;
|
|
18858
|
-
if (!/^\s*$/u.test(context.sourceCode.text.slice(prefixStart, helperMember.range[0]))) return void 0;
|
|
18859
|
-
const helperText = context.sourceCode.getText(helperMember);
|
|
18860
|
-
return (fixer) => [
|
|
18861
|
-
fixer.removeRange([helperMember.range[0], suffixEnd]),
|
|
18862
|
-
fixer.insertTextAfter(callerMember, `${suffix}${helperText}`)
|
|
18863
|
-
];
|
|
18864
|
-
});
|
|
19319
|
+
reportMisordered(context, definitions, scopeDefinitions, calls, pinned, canMove);
|
|
18865
19320
|
}
|
|
18866
19321
|
function isClassRuntimeBarrier(member) {
|
|
18867
19322
|
switch (member.type) {
|
|
@@ -18883,7 +19338,6 @@ var stepdown_default = createRule({
|
|
|
18883
19338
|
type: "suggestion",
|
|
18884
19339
|
docs: { description: "Place a private helper below its sole direct same-scope caller." },
|
|
18885
19340
|
schema: [],
|
|
18886
|
-
fixable: "code",
|
|
18887
19341
|
messages: {
|
|
18888
19342
|
helperAboveOnlyCaller: "Private helper `{{helper}}` is defined above its only caller `{{caller}}`; move it below the caller."
|
|
18889
19343
|
}
|
|
@@ -18912,7 +19366,7 @@ var stepdown_default = createRule({
|
|
|
18912
19366
|
});
|
|
18913
19367
|
|
|
18914
19368
|
// src/rules/source-coupled-test.ts
|
|
18915
|
-
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";
|
|
18916
19370
|
var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|jsonc|py|[cm]?[jt]s)$/iu;
|
|
18917
19371
|
var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
|
|
18918
19372
|
var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
|
|
@@ -18956,7 +19410,7 @@ var SOURCE_COUPLED_TEST_DOCUMENTATION = {
|
|
|
18956
19410
|
remediation: "Parse the artifact, execute its validator, or assert on another runtime contract.",
|
|
18957
19411
|
category: "testing",
|
|
18958
19412
|
limitations: [
|
|
18959
|
-
"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.",
|
|
18960
19414
|
"When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."
|
|
18961
19415
|
],
|
|
18962
19416
|
examples: [
|
|
@@ -18995,6 +19449,11 @@ function stringValue(node) {
|
|
|
18995
19449
|
const current = unwrap7(node);
|
|
18996
19450
|
if (current.type === AST_NODE_TYPES78.Literal && typeof current.value === "string") return current.value;
|
|
18997
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
|
+
}
|
|
18998
19457
|
return null;
|
|
18999
19458
|
}
|
|
19000
19459
|
function importSource(node) {
|
|
@@ -19024,14 +19483,19 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19024
19483
|
const scopes = [newScope()];
|
|
19025
19484
|
const reportedOrigins = /* @__PURE__ */ new Set();
|
|
19026
19485
|
const currentScope = () => scopes.at(-1) ?? scopes[0];
|
|
19027
|
-
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;
|
|
19028
19490
|
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
19029
19491
|
const scope = scopes[index];
|
|
19030
19492
|
if (scope.declared.has(name2)) return scope[kind].has(name2);
|
|
19031
19493
|
}
|
|
19032
19494
|
return false;
|
|
19033
19495
|
};
|
|
19034
|
-
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();
|
|
19035
19499
|
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
19036
19500
|
const scope = scopes[index];
|
|
19037
19501
|
if (scope.declared.has(name2)) return scope.rawOrigins.get(name2) ?? /* @__PURE__ */ new Set();
|
|
@@ -19042,15 +19506,14 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19042
19506
|
const current = unwrap7(node);
|
|
19043
19507
|
const value = stringValue(current);
|
|
19044
19508
|
if (value !== null) return sourceSuffixRe.test(value);
|
|
19045
|
-
if (current.type === AST_NODE_TYPES78.Identifier) return visible("paths", current
|
|
19046
|
-
if (current.type === AST_NODE_TYPES78.BinaryExpression && current.operator === "+") {
|
|
19047
|
-
return sourcePath(current.left) || sourcePath(current.right);
|
|
19048
|
-
}
|
|
19049
|
-
if (current.type === AST_NODE_TYPES78.TemplateLiteral) return current.expressions.some(sourcePath);
|
|
19509
|
+
if (current.type === AST_NODE_TYPES78.Identifier) return visible("paths", current);
|
|
19050
19510
|
if (current.type === AST_NODE_TYPES78.CallExpression || current.type === AST_NODE_TYPES78.NewExpression) {
|
|
19051
|
-
|
|
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);
|
|
19052
19516
|
}
|
|
19053
|
-
if (current.type === AST_NODE_TYPES78.MemberExpression) return sourcePath(current.object);
|
|
19054
19517
|
return false;
|
|
19055
19518
|
};
|
|
19056
19519
|
const rawRead = (node) => {
|
|
@@ -19058,16 +19521,16 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19058
19521
|
if (current.type !== AST_NODE_TYPES78.CallExpression || current.arguments.length === 0) return false;
|
|
19059
19522
|
const callee = unwrap7(current.callee);
|
|
19060
19523
|
if (callee.type === AST_NODE_TYPES78.Identifier) {
|
|
19061
|
-
return visible("fsReaders", callee
|
|
19524
|
+
return visible("fsReaders", callee) && sourcePath(current.arguments[0]);
|
|
19062
19525
|
}
|
|
19063
19526
|
if (callee.type !== AST_NODE_TYPES78.MemberExpression) return false;
|
|
19064
19527
|
const name2 = staticMemberName7(callee);
|
|
19065
19528
|
const object = unwrap7(callee.object);
|
|
19066
|
-
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]);
|
|
19067
19530
|
};
|
|
19068
19531
|
const rawOrigins = (node) => {
|
|
19069
19532
|
const current = unwrap7(node);
|
|
19070
|
-
if (current.type === AST_NODE_TYPES78.Identifier) return visibleRawOrigins(current
|
|
19533
|
+
if (current.type === AST_NODE_TYPES78.Identifier) return visibleRawOrigins(current);
|
|
19071
19534
|
if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
|
|
19072
19535
|
if (current.type === AST_NODE_TYPES78.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
|
|
19073
19536
|
if (current.type === AST_NODE_TYPES78.MemberExpression && staticMemberName7(current) === "length") return rawOrigins(current.object);
|
|
@@ -19108,14 +19571,9 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19108
19571
|
if (receiver.type !== AST_NODE_TYPES78.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
|
|
19109
19572
|
return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES78.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
19110
19573
|
};
|
|
19111
|
-
const
|
|
19112
|
-
const
|
|
19113
|
-
if (
|
|
19114
|
-
const argument = node.arguments[0];
|
|
19115
|
-
if (argument?.type !== AST_NODE_TYPES78.Literal || !(argument.value instanceof RegExp)) return /* @__PURE__ */ new Set();
|
|
19116
|
-
return rawOrigins(callee.object);
|
|
19117
|
-
};
|
|
19118
|
-
const declare = (name2, state) => {
|
|
19574
|
+
const declare = (node, state) => {
|
|
19575
|
+
const name2 = bindingOf(node);
|
|
19576
|
+
if (name2 === null) return;
|
|
19119
19577
|
const scope = currentScope();
|
|
19120
19578
|
scope.declared.add(name2);
|
|
19121
19579
|
scope.collections.delete(name2);
|
|
@@ -19135,18 +19593,8 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19135
19593
|
const current = unwrap7(node);
|
|
19136
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));
|
|
19137
19595
|
};
|
|
19138
|
-
const
|
|
19139
|
-
const current = unwrap7(node);
|
|
19140
|
-
if (current.type === AST_NODE_TYPES78.Identifier) return [current.name];
|
|
19141
|
-
if (current.type === AST_NODE_TYPES78.AssignmentPattern) return declaredNames2(current.left);
|
|
19142
|
-
if (current.type === AST_NODE_TYPES78.RestElement) return declaredNames2(current.argument);
|
|
19143
|
-
if (current.type === AST_NODE_TYPES78.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
|
|
19144
|
-
if (current.type === AST_NODE_TYPES78.ObjectPattern) return current.properties.flatMap((property) => property.type === AST_NODE_TYPES78.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
|
|
19145
|
-
return [];
|
|
19146
|
-
};
|
|
19147
|
-
const enterFunction = (node) => {
|
|
19596
|
+
const enterFunction = () => {
|
|
19148
19597
|
scopes.push(newScope());
|
|
19149
|
-
for (const parameter of node.params) for (const name2 of declaredNames2(parameter)) declare(name2, {});
|
|
19150
19598
|
};
|
|
19151
19599
|
const exitFunction = () => {
|
|
19152
19600
|
scopes.pop();
|
|
@@ -19158,9 +19606,9 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19158
19606
|
for (const specifier of node.specifiers) {
|
|
19159
19607
|
if (specifier.type === AST_NODE_TYPES78.ImportSpecifier) {
|
|
19160
19608
|
const imported = specifier.imported.type === AST_NODE_TYPES78.Identifier ? specifier.imported.name : String(specifier.imported.value);
|
|
19161
|
-
if (FS_READERS.has(imported)) declare(specifier.local
|
|
19609
|
+
if (FS_READERS.has(imported)) declare(specifier.local, { fsReader: true });
|
|
19162
19610
|
} else {
|
|
19163
|
-
declare(specifier.local
|
|
19611
|
+
declare(specifier.local, { fsObject: true });
|
|
19164
19612
|
}
|
|
19165
19613
|
}
|
|
19166
19614
|
},
|
|
@@ -19169,35 +19617,34 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19169
19617
|
VariableDeclarator(node) {
|
|
19170
19618
|
if (node.init === null) return;
|
|
19171
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;
|
|
19172
19622
|
if (required !== null && FS_MODULES.has(required) && node.id.type === AST_NODE_TYPES78.Identifier) {
|
|
19173
|
-
declare(node.id
|
|
19623
|
+
declare(node.id, { fsObject: true });
|
|
19174
19624
|
return;
|
|
19175
19625
|
}
|
|
19176
19626
|
if (node.id.type === AST_NODE_TYPES78.ObjectPattern && required !== null && FS_MODULES.has(required)) {
|
|
19177
19627
|
for (const property of node.id.properties) {
|
|
19178
19628
|
if (property.type !== AST_NODE_TYPES78.Property || property.value.type !== AST_NODE_TYPES78.Identifier) continue;
|
|
19179
19629
|
const key = property.key.type === AST_NODE_TYPES78.Identifier ? property.key.name : property.key.type === AST_NODE_TYPES78.Literal ? String(property.key.value) : "";
|
|
19180
|
-
if (FS_READERS.has(key)) declare(property.value
|
|
19630
|
+
if (FS_READERS.has(key)) declare(property.value, { fsReader: true });
|
|
19181
19631
|
}
|
|
19182
19632
|
return;
|
|
19183
19633
|
}
|
|
19184
19634
|
if (node.id.type !== AST_NODE_TYPES78.Identifier) return;
|
|
19185
|
-
declare(node.id
|
|
19635
|
+
declare(node.id, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
|
|
19186
19636
|
},
|
|
19187
19637
|
AssignmentExpression(node) {
|
|
19188
|
-
if (node.left.type === AST_NODE_TYPES78.Identifier) declare(node.left
|
|
19638
|
+
if (node.left.type === AST_NODE_TYPES78.Identifier) declare(node.left, {});
|
|
19189
19639
|
},
|
|
19190
19640
|
ForOfStatement(node) {
|
|
19191
19641
|
const right = unwrap7(node.right);
|
|
19192
|
-
const collection = right.type === AST_NODE_TYPES78.Identifier && visible("collections", right
|
|
19642
|
+
const collection = right.type === AST_NODE_TYPES78.Identifier && visible("collections", right);
|
|
19193
19643
|
const left = node.left.type === AST_NODE_TYPES78.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
|
|
19194
|
-
if (collection && left?.type === AST_NODE_TYPES78.Identifier) declare(left
|
|
19644
|
+
if (collection && left?.type === AST_NODE_TYPES78.Identifier) declare(left, { path: true });
|
|
19195
19645
|
},
|
|
19196
19646
|
CallExpression(node) {
|
|
19197
|
-
const origins =
|
|
19198
|
-
...rawAssertionOrigins(node),
|
|
19199
|
-
...rawRegexExtractionOrigins(node)
|
|
19200
|
-
]);
|
|
19647
|
+
const origins = rawAssertionOrigins(node);
|
|
19201
19648
|
if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
|
|
19202
19649
|
for (const origin of origins) reportedOrigins.add(origin);
|
|
19203
19650
|
context.report({ node, messageId: "rawSourceOracle" });
|
|
@@ -19340,8 +19787,8 @@ var IAC_SOURCE_COUPLED_TEST_DOCUMENTATION = {
|
|
|
19340
19787
|
remediation: "Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
|
|
19341
19788
|
category: "testing",
|
|
19342
19789
|
limitations: [
|
|
19343
|
-
"The rule follows lexical
|
|
19344
|
-
"
|
|
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."
|
|
19345
19792
|
],
|
|
19346
19793
|
examples: [
|
|
19347
19794
|
{
|
|
@@ -19373,7 +19820,7 @@ var iac_source_coupled_test_default = createSourceCoupledRule(
|
|
|
19373
19820
|
// src/rules/require-pascal-case-zod-schema-name.ts
|
|
19374
19821
|
import {
|
|
19375
19822
|
AST_NODE_TYPES as AST_NODE_TYPES80,
|
|
19376
|
-
ASTUtils as
|
|
19823
|
+
ASTUtils as ASTUtils46
|
|
19377
19824
|
} from "@typescript-eslint/utils";
|
|
19378
19825
|
var REQUIRE_PASCAL_CASE_ZOD_SCHEMA_NAME_DOCUMENTATION = {
|
|
19379
19826
|
summary: "Require confirmed module-level Zod schema contracts to use PascalCase with a `Schema` suffix.",
|
|
@@ -19574,7 +20021,7 @@ var require_pascal_case_zod_schema_name_default = createRule({
|
|
|
19574
20021
|
const zodBindings = /* @__PURE__ */ new Set();
|
|
19575
20022
|
const schemaBindings = /* @__PURE__ */ new Set();
|
|
19576
20023
|
function resolvedBinding(identifier) {
|
|
19577
|
-
return
|
|
20024
|
+
return ASTUtils46.findVariable(
|
|
19578
20025
|
context.sourceCode.getScope(identifier),
|
|
19579
20026
|
identifier.name
|
|
19580
20027
|
);
|
|
@@ -19819,7 +20266,7 @@ var RULES = {
|
|
|
19819
20266
|
};
|
|
19820
20267
|
var meta = {
|
|
19821
20268
|
name: "@sarj/eslint-plugin",
|
|
19822
|
-
version: "15.17.
|
|
20269
|
+
version: "15.17.11"
|
|
19823
20270
|
};
|
|
19824
20271
|
var APPLICATION_ONLY_RULES = [];
|
|
19825
20272
|
var LIBRARY_IMPORT_POLICY = ["error", {
|