@sarj/eslint-plugin 15.17.9 → 15.17.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +846 -467
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +934 -555
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -678,10 +678,11 @@ var TEST_MODULES = /* @__PURE__ */ new Set(["@jest/globals", "@playwright/test",
|
|
|
678
678
|
var DUPLICATE_TEST_BODY_DOCUMENTATION = {
|
|
679
679
|
summary: "Disallow substantial sibling tests with the same body shape; express their differing inputs as a parameterized case table.",
|
|
680
680
|
rationale: "Copy-pasted test bodies hide the cases that differ and allow equivalent assertions to drift independently.",
|
|
681
|
-
remediation: "
|
|
681
|
+
remediation: "Consider a case table with one named test or subtest per case; preserve setup lifetime, test modifiers, and each case's assertions rather than deleting coverage.",
|
|
682
682
|
category: "testing",
|
|
683
683
|
limitations: [
|
|
684
|
-
"The rule compares substantial sibling tests within one suite and skips inline snapshots and materially different comments."
|
|
684
|
+
"The rule compares substantial sibling tests within one suite and skips inline snapshots and materially different comments.",
|
|
685
|
+
"Matching normalized body shapes do not prove runtime equivalence or independent setup; parameterization is a manual review, not an automatic deletion."
|
|
685
686
|
],
|
|
686
687
|
examples: [
|
|
687
688
|
{
|
|
@@ -866,6 +867,10 @@ function isDuplicateTestFrameworkIdentifier(identifier, sourceCode) {
|
|
|
866
867
|
const variable = import_utils3.ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
867
868
|
if (variable === null || variable.defs.length === 0) return true;
|
|
868
869
|
return variable.defs.some((definition) => {
|
|
870
|
+
if (definition.node.type === import_utils3.AST_NODE_TYPES.ImportDefaultSpecifier) return definition.node.parent.source.value === "node:test";
|
|
871
|
+
if (definition.node.type !== import_utils3.AST_NODE_TYPES.ImportSpecifier) return false;
|
|
872
|
+
const imported = definition.node.imported;
|
|
873
|
+
if (!TEST_CALLERS.has(imported.type === import_utils3.AST_NODE_TYPES.Identifier ? imported.name : String(imported.value))) return false;
|
|
869
874
|
let current = definition.node;
|
|
870
875
|
while (current != null && current.type !== import_utils3.AST_NODE_TYPES.ImportDeclaration) current = current.parent;
|
|
871
876
|
return current?.type === import_utils3.AST_NODE_TYPES.ImportDeclaration && typeof current.source.value === "string" && TEST_MODULES.has(current.source.value);
|
|
@@ -2454,7 +2459,7 @@ var no_duplicate_lifecycle_refresh_listeners_default = createRule({
|
|
|
2454
2459
|
VariableDeclarator(node) {
|
|
2455
2460
|
if (node.id.type !== import_utils10.AST_NODE_TYPES.Identifier) return;
|
|
2456
2461
|
const variable = import_utils10.ASTUtils.findVariable(context.sourceCode.getScope(node.id), node.id.name);
|
|
2457
|
-
if (variable === null) return;
|
|
2462
|
+
if (variable === null || variable.references.some((reference) => reference.isWrite() && !reference.init)) return;
|
|
2458
2463
|
if (node.init?.type === import_utils10.AST_NODE_TYPES.ArrowFunctionExpression || node.init?.type === import_utils10.AST_NODE_TYPES.FunctionExpression) {
|
|
2459
2464
|
functionCallbacks.set(node.init, variable);
|
|
2460
2465
|
}
|
|
@@ -2465,7 +2470,7 @@ var no_duplicate_lifecycle_refresh_listeners_default = createRule({
|
|
|
2465
2470
|
FunctionDeclaration(node) {
|
|
2466
2471
|
if (node.id === null) return;
|
|
2467
2472
|
const variable = import_utils10.ASTUtils.findVariable(context.sourceCode.getScope(node.id), node.id.name);
|
|
2468
|
-
if (variable !== null) functionCallbacks.set(node, variable);
|
|
2473
|
+
if (variable !== null && !variable.references.some((reference) => reference.isWrite() && !reference.init)) functionCallbacks.set(node, variable);
|
|
2469
2474
|
},
|
|
2470
2475
|
CallExpression(node) {
|
|
2471
2476
|
const item = registration(context.sourceCode, node);
|
|
@@ -2609,12 +2614,33 @@ var import_utils13 = require("@typescript-eslint/utils");
|
|
|
2609
2614
|
// src/rules/_sql.ts
|
|
2610
2615
|
var import_utils12 = require("@typescript-eslint/utils");
|
|
2611
2616
|
function stripSqlNoise(text) {
|
|
2612
|
-
|
|
2617
|
+
return scanSqlNoise(text);
|
|
2618
|
+
}
|
|
2619
|
+
function sqlSingleQuotedRanges(text) {
|
|
2620
|
+
const ranges = [];
|
|
2621
|
+
scanSqlNoise(text, (start, end) => ranges.push([start, end]));
|
|
2622
|
+
return ranges;
|
|
2623
|
+
}
|
|
2624
|
+
function scanSqlNoise(text, onSingleQuoted) {
|
|
2625
|
+
const out = text.split("");
|
|
2613
2626
|
const n = text.length;
|
|
2614
2627
|
let i = 0;
|
|
2615
2628
|
while (i < n) {
|
|
2616
2629
|
const ch = text[i];
|
|
2630
|
+
if (ch === "$" && !/[\w$]/u.test(text[i - 1] ?? "")) {
|
|
2631
|
+
const delimiter = /^\$(?:[A-Za-z_][A-Za-z_0-9]*)?\$/u.exec(text.slice(i))?.[0];
|
|
2632
|
+
if (delimiter !== void 0) {
|
|
2633
|
+
const closing = text.indexOf(delimiter, i + delimiter.length);
|
|
2634
|
+
const end = closing < 0 ? n : closing + delimiter.length;
|
|
2635
|
+
while (i < end) {
|
|
2636
|
+
if (text[i] !== "\n") out[i] = " ";
|
|
2637
|
+
i += 1;
|
|
2638
|
+
}
|
|
2639
|
+
continue;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2617
2642
|
if (ch === "'" || ch === '"') {
|
|
2643
|
+
const start = i;
|
|
2618
2644
|
out[i] = " ";
|
|
2619
2645
|
i += 1;
|
|
2620
2646
|
while (i < n) {
|
|
@@ -2628,6 +2654,7 @@ function stripSqlNoise(text) {
|
|
|
2628
2654
|
}
|
|
2629
2655
|
out[i] = " ";
|
|
2630
2656
|
i += 1;
|
|
2657
|
+
if (ch === "'") onSingleQuoted?.(start, i);
|
|
2631
2658
|
break;
|
|
2632
2659
|
}
|
|
2633
2660
|
if (c !== "\n") {
|
|
@@ -2648,17 +2675,20 @@ function stripSqlNoise(text) {
|
|
|
2648
2675
|
out[i] = " ";
|
|
2649
2676
|
out[i + 1] = " ";
|
|
2650
2677
|
i += 2;
|
|
2651
|
-
|
|
2678
|
+
let depth = 1;
|
|
2679
|
+
while (i < n && depth > 0) {
|
|
2680
|
+
if (text[i] === "/" && text[i + 1] === "*" || text[i] === "*" && text[i + 1] === "/") {
|
|
2681
|
+
depth += text[i] === "/" ? 1 : -1;
|
|
2682
|
+
out[i] = " ";
|
|
2683
|
+
out[i + 1] = " ";
|
|
2684
|
+
i += 2;
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2652
2687
|
if (text[i] !== "\n") {
|
|
2653
2688
|
out[i] = " ";
|
|
2654
2689
|
}
|
|
2655
2690
|
i += 1;
|
|
2656
2691
|
}
|
|
2657
|
-
if (i < n) {
|
|
2658
|
-
out[i] = " ";
|
|
2659
|
-
out[i + 1] = " ";
|
|
2660
|
-
i += 2;
|
|
2661
|
-
}
|
|
2662
2692
|
continue;
|
|
2663
2693
|
}
|
|
2664
2694
|
i += 1;
|
|
@@ -2756,8 +2786,9 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
|
|
|
2756
2786
|
remediation: "Use SQL placeholders and pass runtime values through the driver's binding API.",
|
|
2757
2787
|
category: "security",
|
|
2758
2788
|
limitations: [
|
|
2759
|
-
"
|
|
2760
|
-
"
|
|
2789
|
+
"Only single-quoted SQL values are inspected. Double-quoted identifiers, comments, dollar strings, and unquoted fragments are excluded; this is not a general SQL injection detector.",
|
|
2790
|
+
"Literal fragments, legacy uppercase fragment names, and parameterizing tagged templates are exempt; uppercase spelling does not prove a value is static.",
|
|
2791
|
+
"The bounded lexer recognizes doubled quotes, comments, and PostgreSQL dollar strings; dialect-specific escape modes and SQL generated through other APIs require separate security review."
|
|
2761
2792
|
],
|
|
2762
2793
|
examples: [
|
|
2763
2794
|
{
|
|
@@ -2798,15 +2829,23 @@ function isStaticFragment(expression) {
|
|
|
2798
2829
|
return false;
|
|
2799
2830
|
}
|
|
2800
2831
|
function runtimeInterpolations(template) {
|
|
2832
|
+
const parts = template.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw);
|
|
2833
|
+
const ranges = sqlSingleQuotedRanges(parts.join(RUNTIME_MARKER));
|
|
2834
|
+
let offset = 0;
|
|
2801
2835
|
return template.expressions.filter(
|
|
2802
|
-
(expression, index) =>
|
|
2836
|
+
(expression, index) => {
|
|
2837
|
+
offset += parts[index]?.length ?? 0;
|
|
2838
|
+
const inValue = ranges.some(([start, end]) => start < offset && offset < end);
|
|
2839
|
+
offset += RUNTIME_MARKER.length;
|
|
2840
|
+
return inValue && !isStaticFragment(expression) && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "");
|
|
2841
|
+
}
|
|
2803
2842
|
);
|
|
2804
2843
|
}
|
|
2805
2844
|
function endsWithSqlQuote(text) {
|
|
2806
|
-
return /
|
|
2845
|
+
return /'\s*$/u.test(text);
|
|
2807
2846
|
}
|
|
2808
2847
|
function startsWithSqlQuote(text) {
|
|
2809
|
-
return /^\s*
|
|
2848
|
+
return /^\s*'/u.test(text);
|
|
2810
2849
|
}
|
|
2811
2850
|
function staticLiteralText(node) {
|
|
2812
2851
|
if (node.type === import_utils13.AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
@@ -2828,7 +2867,13 @@ function runtimeConcatOperands(node) {
|
|
|
2828
2867
|
if (!hasStringLiteral) {
|
|
2829
2868
|
return [];
|
|
2830
2869
|
}
|
|
2870
|
+
const parts = operands.map((operand) => staticLiteralText(operand) ?? RUNTIME_MARKER);
|
|
2871
|
+
const ranges = sqlSingleQuotedRanges(parts.join(""));
|
|
2872
|
+
let offset = 0;
|
|
2831
2873
|
return operands.filter((operand, index) => {
|
|
2874
|
+
const inValue = ranges.some(([start, end]) => start < offset && offset < end);
|
|
2875
|
+
offset += parts[index]?.length ?? 0;
|
|
2876
|
+
if (!inValue) return false;
|
|
2832
2877
|
if (isStaticFragment(operand)) return false;
|
|
2833
2878
|
const before = operands[index - 1];
|
|
2834
2879
|
const after = operands[index + 1];
|
|
@@ -2921,9 +2966,11 @@ var no_dynamic_sql_default = createRule({
|
|
|
2921
2966
|
var import_utils14 = require("@typescript-eslint/utils");
|
|
2922
2967
|
var NO_ENUM_DOCUMENTATION = {
|
|
2923
2968
|
summary: "Disallow TypeScript `enum`; use string-literal unions or `as const` objects instead.",
|
|
2924
|
-
rationale: "
|
|
2925
|
-
remediation: "
|
|
2969
|
+
rationale: "Literal unions keep type-only domains explicit, while constant objects make runtime values deliberate. This policy also avoids compiler-dependent const-enum inlining contracts.",
|
|
2970
|
+
remediation: "Use a literal union or an `as const` object after checking runtime member access, numeric reverse mappings, serialized values, and public consumers.",
|
|
2926
2971
|
category: "maintainability",
|
|
2972
|
+
limitations: ["This is an explicit style policy for regular and const enums, not a claim that every enum emits an object. Generated files and configured exclusions are preserved; migration is manual."],
|
|
2973
|
+
references: ["https://www.typescriptlang.org/docs/handbook/enums.html"],
|
|
2927
2974
|
examples: [
|
|
2928
2975
|
{
|
|
2929
2976
|
id: "string-literal-union",
|
|
@@ -3003,17 +3050,17 @@ var no_enum_default = createRule({
|
|
|
3003
3050
|
// src/rules/no-fat-try-blocks.ts
|
|
3004
3051
|
var import_utils15 = require("@typescript-eslint/utils");
|
|
3005
3052
|
var NO_FAT_TRY_BLOCKS_DOCUMENTATION = {
|
|
3006
|
-
summary: "
|
|
3053
|
+
summary: "Review try blocks exceeding the configured count of syntactically selected operations.",
|
|
3007
3054
|
rationale: "A broad `try` block obscures which operation failed and encourages one catch clause to recover from unrelated errors.",
|
|
3008
3055
|
remediation: "Keep only the operations that share one recovery policy inside the `try` block and move other work outside it.",
|
|
3009
3056
|
category: "correctness",
|
|
3010
3057
|
limitations: [
|
|
3011
|
-
"The
|
|
3058
|
+
"The default threshold is three selected top-level operations, not a proof of every possible throw. A shared recovery policy may legitimately cover several operations; generated files, catchless finally blocks, rethrows, and terminal error boundaries are excluded."
|
|
3012
3059
|
],
|
|
3013
3060
|
examples: [
|
|
3014
3061
|
{
|
|
3015
3062
|
id: "focused-try-block",
|
|
3016
|
-
title: "
|
|
3063
|
+
title: "Three selected operations stay within the default threshold",
|
|
3017
3064
|
outcome: "no-match",
|
|
3018
3065
|
files: [{
|
|
3019
3066
|
path: "src/load.ts",
|
|
@@ -3025,7 +3072,7 @@ var NO_FAT_TRY_BLOCKS_DOCUMENTATION = {
|
|
|
3025
3072
|
},
|
|
3026
3073
|
{
|
|
3027
3074
|
id: "broad-try-block",
|
|
3028
|
-
title: "
|
|
3075
|
+
title: "Review whether four selected operations share one recovery policy",
|
|
3029
3076
|
outcome: "match",
|
|
3030
3077
|
files: [{
|
|
3031
3078
|
path: "src/load.ts",
|
|
@@ -3395,7 +3442,7 @@ var no_fat_try_blocks_default = createRule({
|
|
|
3395
3442
|
meta: {
|
|
3396
3443
|
type: "problem",
|
|
3397
3444
|
docs: {
|
|
3398
|
-
description: "
|
|
3445
|
+
description: "Review try blocks exceeding the configured count of syntactically selected operations."
|
|
3399
3446
|
},
|
|
3400
3447
|
schema: [
|
|
3401
3448
|
{
|
|
@@ -3407,7 +3454,7 @@ var no_fat_try_blocks_default = createRule({
|
|
|
3407
3454
|
}
|
|
3408
3455
|
],
|
|
3409
3456
|
messages: {
|
|
3410
|
-
fatTryBlock: "This `try` block has {{count}}
|
|
3457
|
+
fatTryBlock: "This `try` block has {{count}} syntactically selected operations (max {{max}}). Review whether they share one recovery policy; move unrelated work outside the boundary."
|
|
3411
3458
|
}
|
|
3412
3459
|
},
|
|
3413
3460
|
defaultOptions: [{ max: MAX_TRY_BODY_STATEMENTS }],
|
|
@@ -3454,7 +3501,8 @@ var NO_HAND_ROLLED_SLEEP_DOCUMENTATION = {
|
|
|
3454
3501
|
remediation: "Use `node:timers/promises` with an abort signal for delays, or pass `AbortSignal.timeout(...)` to the timed operation.",
|
|
3455
3502
|
category: "correctness",
|
|
3456
3503
|
limitations: [
|
|
3457
|
-
"The rule skips tests, scripts, generated files, and client modules by default, and supports explicit path exemptions."
|
|
3504
|
+
"The rule skips tests, scripts, generated files, and client modules by default, and supports explicit path exemptions.",
|
|
3505
|
+
"Locally shadowed constructors/timers and value-returning timers are excluded. Only recognized browser markers are excluded; choose a runtime-compatible cancellation API for other browser modules."
|
|
3458
3506
|
],
|
|
3459
3507
|
examples: [
|
|
3460
3508
|
{
|
|
@@ -3598,6 +3646,21 @@ var no_hand_rolled_sleep_default = createRule({
|
|
|
3598
3646
|
return {};
|
|
3599
3647
|
}
|
|
3600
3648
|
const checkClientModules = optionsArg?.checkClientModules ?? false;
|
|
3649
|
+
const bindingOf = (identifier) => import_utils16.ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
3650
|
+
const isGlobal = (identifier) => (bindingOf(identifier)?.defs.length ?? 0) === 0;
|
|
3651
|
+
const isBuiltinTimer = (callee) => {
|
|
3652
|
+
if (!isSetTimeoutCallee(callee)) return false;
|
|
3653
|
+
if (callee.type === import_utils16.AST_NODE_TYPES.MemberExpression && callee.object.type === import_utils16.AST_NODE_TYPES.Identifier) return isGlobal(callee.object);
|
|
3654
|
+
if (callee.type !== import_utils16.AST_NODE_TYPES.Identifier) return false;
|
|
3655
|
+
const binding = bindingOf(callee);
|
|
3656
|
+
return binding === null || binding.defs.length === 0 || binding.defs.every((definition) => definition.node.type === import_utils16.AST_NODE_TYPES.ImportSpecifier && definition.node.imported.type === import_utils16.AST_NODE_TYPES.Identifier && definition.node.imported.name === "setTimeout" && definition.node.parent.type === import_utils16.AST_NODE_TYPES.ImportDeclaration && ["node:timers", "timers"].includes(String(definition.node.parent.source.value)));
|
|
3657
|
+
};
|
|
3658
|
+
const settlesParameter = (callback, executor, index) => {
|
|
3659
|
+
const parameter = executor.params[index];
|
|
3660
|
+
if (parameter?.type !== import_utils16.AST_NODE_TYPES.Identifier) return false;
|
|
3661
|
+
const callee = callback.type === import_utils16.AST_NODE_TYPES.Identifier ? callback : callback.type === import_utils16.AST_NODE_TYPES.ArrowFunctionExpression || callback.type === import_utils16.AST_NODE_TYPES.FunctionExpression ? soleCall(callback)?.callee : null;
|
|
3662
|
+
return callee?.type === import_utils16.AST_NODE_TYPES.Identifier && bindingOf(callee) === bindingOf(parameter);
|
|
3663
|
+
};
|
|
3601
3664
|
function isClientModule2() {
|
|
3602
3665
|
if (/\.[cm]?[jt]sx$/.test(filename)) {
|
|
3603
3666
|
return true;
|
|
@@ -3623,7 +3686,7 @@ var no_hand_rolled_sleep_default = createRule({
|
|
|
3623
3686
|
};
|
|
3624
3687
|
return {
|
|
3625
3688
|
NewExpression(node) {
|
|
3626
|
-
if (node.callee.type !== import_utils16.AST_NODE_TYPES.Identifier || node.callee.name !== "Promise") {
|
|
3689
|
+
if (node.callee.type !== import_utils16.AST_NODE_TYPES.Identifier || node.callee.name !== "Promise" || !isGlobal(node.callee)) {
|
|
3627
3690
|
return;
|
|
3628
3691
|
}
|
|
3629
3692
|
const executor = node.arguments[0];
|
|
@@ -3631,7 +3694,7 @@ var no_hand_rolled_sleep_default = createRule({
|
|
|
3631
3694
|
return;
|
|
3632
3695
|
}
|
|
3633
3696
|
const call = soleCall(executor);
|
|
3634
|
-
if (call === null || !
|
|
3697
|
+
if (call === null || !isBuiltinTimer(call.callee)) {
|
|
3635
3698
|
return;
|
|
3636
3699
|
}
|
|
3637
3700
|
const [callback, delay] = call.arguments;
|
|
@@ -3639,14 +3702,14 @@ var no_hand_rolled_sleep_default = createRule({
|
|
|
3639
3702
|
return;
|
|
3640
3703
|
}
|
|
3641
3704
|
const resolveName = parameterName(executor, 0);
|
|
3642
|
-
if (resolveName !== null && settlesWithoutValue(callback, resolveName)) {
|
|
3705
|
+
if (resolveName !== null && call.arguments.length === 2 && settlesWithoutValue(callback, resolveName) && settlesParameter(callback, executor, 0)) {
|
|
3643
3706
|
if (reportsSleepHere()) {
|
|
3644
3707
|
context.report({ node, messageId: "handRolledSleep" });
|
|
3645
3708
|
}
|
|
3646
3709
|
return;
|
|
3647
3710
|
}
|
|
3648
3711
|
const rejectName = parameterName(executor, 1);
|
|
3649
|
-
if (rejectName !== null && isRaceArm(node) && rejectsInCallback(callback, rejectName)) {
|
|
3712
|
+
if (rejectName !== null && isRaceArm(node) && settlesParameter(callback, executor, 1) && rejectsInCallback(callback, rejectName)) {
|
|
3650
3713
|
context.report({ node, messageId: "handRolledTimeoutRace" });
|
|
3651
3714
|
}
|
|
3652
3715
|
}
|
|
@@ -3745,7 +3808,7 @@ var NO_INSECURE_RANDOM_ID_DOCUMENTATION = {
|
|
|
3745
3808
|
rationale: "Math.random is predictable and lacks the entropy required for security-sensitive values.",
|
|
3746
3809
|
remediation: "Generate the value with crypto.randomUUID or crypto.getRandomValues.",
|
|
3747
3810
|
category: "security",
|
|
3748
|
-
limitations: ["
|
|
3811
|
+
limitations: ["Names select security-sensitive bindings heuristically; they do not prove sensitivity. Sampling in unrelated bindings or branch tests, locally shadowed Math objects, ambiguous identifiers and test files are excluded. This is not interprocedural data-flow analysis."],
|
|
3749
3812
|
examples: [
|
|
3750
3813
|
{ id: "cryptographic-id", title: "Use the Web Crypto API", outcome: "no-match", files: [{ path: "src/session.ts", source: "const sessionToken = crypto.randomUUID();" }], focusPath: "src/session.ts", expectedCount: 0, public: true },
|
|
3751
3814
|
{ id: "predictable-token", title: "Do not derive a token from Math.random", outcome: "match", files: [{ path: "src/session.ts", source: "const sessionToken = Math.random();" }], focusPath: "src/session.ts", expectedCount: 1, public: true }
|
|
@@ -3826,6 +3889,7 @@ function findEnclosingNames(node) {
|
|
|
3826
3889
|
if (directBinding && parent.id.type === "Identifier") {
|
|
3827
3890
|
names.push(parent.id.name);
|
|
3828
3891
|
}
|
|
3892
|
+
return names;
|
|
3829
3893
|
}
|
|
3830
3894
|
if (parent.type === "Property" && parent.value === current) {
|
|
3831
3895
|
const key = parent.key;
|
|
@@ -3860,7 +3924,7 @@ function findEnclosingNames(node) {
|
|
|
3860
3924
|
if (directBinding && parent.id !== null) names.push(parent.id.name);
|
|
3861
3925
|
return names;
|
|
3862
3926
|
}
|
|
3863
|
-
if (parent.type === "ExpressionStatement") {
|
|
3927
|
+
if (parent.type === "ExpressionStatement" || parent.type === "IfStatement" || parent.type === "ForStatement" || parent.type === "WhileStatement" || parent.type === "DoWhileStatement" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression") {
|
|
3864
3928
|
return names;
|
|
3865
3929
|
}
|
|
3866
3930
|
current = parent;
|
|
@@ -3952,6 +4016,7 @@ var no_insecure_random_id_default = createRule({
|
|
|
3952
4016
|
if (!isMathRandomCall(node)) {
|
|
3953
4017
|
return;
|
|
3954
4018
|
}
|
|
4019
|
+
if ((import_utils18.ASTUtils.findVariable(context.sourceCode.getScope(node), "Math")?.defs.length ?? 0) > 0) return;
|
|
3955
4020
|
const names = findEnclosingNames(node);
|
|
3956
4021
|
if (names.some(isStrongSecurityName)) {
|
|
3957
4022
|
context.report({ node, messageId: "insecureRandomId" });
|
|
@@ -4475,105 +4540,9 @@ var ts = __toESM(require("typescript"), 1);
|
|
|
4475
4540
|
|
|
4476
4541
|
// src/rules/_class-private.ts
|
|
4477
4542
|
var import_utils21 = require("@typescript-eslint/utils");
|
|
4478
|
-
function symbolAt(services, checker, node) {
|
|
4479
|
-
return checker.getSymbolAtLocation(services.esTreeNodeToTSNodeMap.get(node));
|
|
4480
|
-
}
|
|
4481
|
-
function sameSymbol(left, right) {
|
|
4482
|
-
return left !== void 0 && right !== void 0 && left === right;
|
|
4483
|
-
}
|
|
4484
|
-
function enclosingClass(node) {
|
|
4485
|
-
let current = node.parent;
|
|
4486
|
-
while (current !== void 0) {
|
|
4487
|
-
if (current.type === import_utils21.AST_NODE_TYPES.ClassDeclaration || current.type === import_utils21.AST_NODE_TYPES.ClassExpression) {
|
|
4488
|
-
return current;
|
|
4489
|
-
}
|
|
4490
|
-
current = current.parent;
|
|
4491
|
-
}
|
|
4492
|
-
return null;
|
|
4493
|
-
}
|
|
4494
4543
|
function memberName2(member) {
|
|
4495
4544
|
return !member.computed && member.key.type === import_utils21.AST_NODE_TYPES.Identifier ? member.key.name : null;
|
|
4496
4545
|
}
|
|
4497
|
-
function privateMemberFixes(context, services, owner, members, removePrivateKeyword) {
|
|
4498
|
-
const first = members[0];
|
|
4499
|
-
const name = first === void 0 ? null : memberName2(first);
|
|
4500
|
-
if (first === void 0 || name === null || members.some((member) => member.static || member.decorators.length > 0)) {
|
|
4501
|
-
return void 0;
|
|
4502
|
-
}
|
|
4503
|
-
if (owner.body.body.some((member) => {
|
|
4504
|
-
if (member.type !== import_utils21.AST_NODE_TYPES.MethodDefinition && member.type !== import_utils21.AST_NODE_TYPES.PropertyDefinition && member.type !== import_utils21.AST_NODE_TYPES.AccessorProperty) return false;
|
|
4505
|
-
return member.key.type === import_utils21.AST_NODE_TYPES.PrivateIdentifier && member.key.name === name;
|
|
4506
|
-
})) return void 0;
|
|
4507
|
-
const selectedMembers = new Set(members);
|
|
4508
|
-
if (owner.body.body.some((member) => {
|
|
4509
|
-
if (member.type !== import_utils21.AST_NODE_TYPES.MethodDefinition && member.type !== import_utils21.AST_NODE_TYPES.PropertyDefinition && member.type !== import_utils21.AST_NODE_TYPES.AccessorProperty) return false;
|
|
4510
|
-
return memberName2(member) === name && !selectedMembers.has(member);
|
|
4511
|
-
})) return void 0;
|
|
4512
|
-
const checker = services.program.getTypeChecker();
|
|
4513
|
-
const symbols = members.map((member) => symbolAt(services, checker, member.key)).filter(
|
|
4514
|
-
(symbol) => symbol !== void 0
|
|
4515
|
-
);
|
|
4516
|
-
if (symbols.length === 0) return void 0;
|
|
4517
|
-
const references = [];
|
|
4518
|
-
let unsafe = false;
|
|
4519
|
-
walk(context.sourceCode.ast, context.sourceCode.visitorKeys, (node) => {
|
|
4520
|
-
if (node.type === import_utils21.AST_NODE_TYPES.Literal && node.value === name) {
|
|
4521
|
-
unsafe = true;
|
|
4522
|
-
return;
|
|
4523
|
-
}
|
|
4524
|
-
if (node.type !== import_utils21.AST_NODE_TYPES.MemberExpression) return;
|
|
4525
|
-
const propertyName6 = node.property.type === import_utils21.AST_NODE_TYPES.Identifier || node.property.type === import_utils21.AST_NODE_TYPES.PrivateIdentifier ? node.property.name : node.property.type === import_utils21.AST_NODE_TYPES.Literal && typeof node.property.value === "string" ? node.property.value : null;
|
|
4526
|
-
if (propertyName6 !== name) return;
|
|
4527
|
-
const propertySymbol = symbolAt(services, checker, node.property);
|
|
4528
|
-
if (node.computed || node.property.type !== import_utils21.AST_NODE_TYPES.Identifier || node.object.type !== import_utils21.AST_NODE_TYPES.ThisExpression || enclosingClass(node) !== owner || !symbols.some((symbol) => sameSymbol(symbol, propertySymbol))) {
|
|
4529
|
-
unsafe = true;
|
|
4530
|
-
return;
|
|
4531
|
-
}
|
|
4532
|
-
references.push(node);
|
|
4533
|
-
});
|
|
4534
|
-
if (unsafe) return void 0;
|
|
4535
|
-
const privateKeywordRanges = /* @__PURE__ */ new Map();
|
|
4536
|
-
if (removePrivateKeyword) {
|
|
4537
|
-
const comments = context.sourceCode.getAllComments();
|
|
4538
|
-
for (const member of members) {
|
|
4539
|
-
const keyword = context.sourceCode.getTokens(member).find((token) => token.value === "private");
|
|
4540
|
-
const next = keyword === void 0 ? void 0 : context.sourceCode.getTokenAfter(keyword);
|
|
4541
|
-
if (keyword === void 0 || next === null || next === void 0) return void 0;
|
|
4542
|
-
if (comments.some((comment) => comment.range[0] >= keyword.range[1] && comment.range[1] <= next.range[0])) {
|
|
4543
|
-
return void 0;
|
|
4544
|
-
}
|
|
4545
|
-
privateKeywordRanges.set(member, [keyword.range[0], next.range[0]]);
|
|
4546
|
-
}
|
|
4547
|
-
}
|
|
4548
|
-
return (fixer) => {
|
|
4549
|
-
const fixes = [];
|
|
4550
|
-
for (const member of members) {
|
|
4551
|
-
fixes.push(fixer.replaceText(member.key, `#${name}`));
|
|
4552
|
-
if (removePrivateKeyword) {
|
|
4553
|
-
const range = privateKeywordRanges.get(member);
|
|
4554
|
-
if (range === void 0) return [];
|
|
4555
|
-
fixes.push(fixer.removeRange(range));
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
for (const reference of references) fixes.push(fixer.replaceText(reference.property, `#${name}`));
|
|
4559
|
-
return fixes;
|
|
4560
|
-
};
|
|
4561
|
-
}
|
|
4562
|
-
function walk(node, visitorKeys, visit) {
|
|
4563
|
-
visit(node);
|
|
4564
|
-
for (const key of visitorKeys[node.type] ?? []) {
|
|
4565
|
-
const child = node[key];
|
|
4566
|
-
if (Array.isArray(child)) {
|
|
4567
|
-
for (const item of child) {
|
|
4568
|
-
if (typeof item === "object" && item !== null && "type" in item) {
|
|
4569
|
-
walk(item, visitorKeys, visit);
|
|
4570
|
-
}
|
|
4571
|
-
}
|
|
4572
|
-
} else if (typeof child === "object" && child !== null && "type" in child) {
|
|
4573
|
-
walk(child, visitorKeys, visit);
|
|
4574
|
-
}
|
|
4575
|
-
}
|
|
4576
|
-
}
|
|
4577
4546
|
function convertibleMemberName(member) {
|
|
4578
4547
|
if (member.type !== import_utils21.AST_NODE_TYPES.MethodDefinition && member.type !== import_utils21.AST_NODE_TYPES.PropertyDefinition && member.type !== import_utils21.AST_NODE_TYPES.AccessorProperty) return null;
|
|
4579
4548
|
return memberName2(member);
|
|
@@ -4912,6 +4881,39 @@ var no_log_only_catch_default = createRule({
|
|
|
4912
4881
|
const matcher = createLogMatcher(loggingOptions);
|
|
4913
4882
|
const filename = context.filename;
|
|
4914
4883
|
const sourceCode = context.sourceCode;
|
|
4884
|
+
function hasCoercionValidation(node) {
|
|
4885
|
+
const owner = node.parent;
|
|
4886
|
+
const statement = owner.block.body[0];
|
|
4887
|
+
if (node.body.body.length !== 0 || owner.finalizer !== null || owner.block.body.length !== 1 || statement?.type !== import_utils24.AST_NODE_TYPES.ExpressionStatement || statement.expression.type !== import_utils24.AST_NODE_TYPES.AssignmentExpression || statement.expression.operator !== "=") return false;
|
|
4888
|
+
const { left, right } = statement.expression;
|
|
4889
|
+
if (left.type !== import_utils24.AST_NODE_TYPES.MemberExpression || left.computed || left.object.type !== import_utils24.AST_NODE_TYPES.Identifier || right.type !== import_utils24.AST_NODE_TYPES.CallExpression || right.optional || right.callee.type !== import_utils24.AST_NODE_TYPES.Identifier || !["String", "Number", "Boolean", "BigInt"].includes(right.callee.name) || right.arguments.length !== 1) return false;
|
|
4890
|
+
const argument = right.arguments[0];
|
|
4891
|
+
if (argument === void 0 || sourceCode.getText(left) !== sourceCode.getText(argument)) return false;
|
|
4892
|
+
const global = import_utils24.ASTUtils.findVariable(sourceCode.getScope(right.callee), right.callee.name);
|
|
4893
|
+
if (global !== null && global.defs.length > 0) return false;
|
|
4894
|
+
const root = import_utils24.ASTUtils.findVariable(sourceCode.getScope(left.object), left.object.name);
|
|
4895
|
+
if (root === null || root.references.some((reference) => reference.isWrite() && !reference.init)) return false;
|
|
4896
|
+
let current = owner;
|
|
4897
|
+
let slot = statementSlot(current);
|
|
4898
|
+
while (slot === null && current.parent !== void 0 && !FUNCTION_TYPES2.has(current.parent.type)) {
|
|
4899
|
+
current = current.parent;
|
|
4900
|
+
slot = statementSlot(current);
|
|
4901
|
+
}
|
|
4902
|
+
let next = slot?.list[slot.index + 1];
|
|
4903
|
+
let target = sourceCode.getText(left);
|
|
4904
|
+
if (next?.type === import_utils24.AST_NODE_TYPES.VariableDeclaration && next.kind === "const" && next.declarations.length === 1) {
|
|
4905
|
+
const alias = next.declarations[0];
|
|
4906
|
+
if (alias?.id.type !== import_utils24.AST_NODE_TYPES.Identifier || alias.init === null || sourceCode.getText(alias.init) !== target) return false;
|
|
4907
|
+
target = alias.id.name;
|
|
4908
|
+
next = slot?.list[slot.index + 2];
|
|
4909
|
+
}
|
|
4910
|
+
if (next?.type !== import_utils24.AST_NODE_TYPES.IfStatement) return false;
|
|
4911
|
+
let condition = next.test;
|
|
4912
|
+
while (condition.type === import_utils24.AST_NODE_TYPES.LogicalExpression && condition.operator === "&&") condition = condition.left;
|
|
4913
|
+
if (condition.type !== import_utils24.AST_NODE_TYPES.BinaryExpression || !["==", "==="].includes(condition.operator)) return false;
|
|
4914
|
+
const test = condition.left;
|
|
4915
|
+
return test.type === import_utils24.AST_NODE_TYPES.UnaryExpression && test.operator === "typeof" && sourceCode.getText(test.argument) === target && condition.right.type === import_utils24.AST_NODE_TYPES.Literal && condition.right.value === right.callee.name.toLowerCase();
|
|
4916
|
+
}
|
|
4915
4917
|
function isLoggingCallStatement(statement) {
|
|
4916
4918
|
if (statement.type !== "ExpressionStatement") {
|
|
4917
4919
|
return false;
|
|
@@ -4938,17 +4940,11 @@ var no_log_only_catch_default = createRule({
|
|
|
4938
4940
|
CatchClause(node) {
|
|
4939
4941
|
const statements = node.body.body;
|
|
4940
4942
|
const isDocumented = sourceCode.getCommentsInside(node.body).length > 0 || hasAdjacentRationale(node);
|
|
4941
|
-
if (
|
|
4942
|
-
if (isDocumented) {
|
|
4943
|
-
return;
|
|
4944
|
-
}
|
|
4945
|
-
if (fallbackFollowsTry(node.parent) || seededFallbackHandled(node.parent, sourceCode.getScope(node))) {
|
|
4946
|
-
return;
|
|
4947
|
-
}
|
|
4948
|
-
context.report({ node, messageId: "emptyCatch" });
|
|
4943
|
+
if (isDocumented || hasCoercionValidation(node) || fallbackFollowsTry(node.parent) || seededFallbackHandled(node.parent, sourceCode.getScope(node))) {
|
|
4949
4944
|
return;
|
|
4950
4945
|
}
|
|
4951
|
-
if (
|
|
4946
|
+
if (statements.length === 0) {
|
|
4947
|
+
context.report({ node, messageId: "emptyCatch" });
|
|
4952
4948
|
return;
|
|
4953
4949
|
}
|
|
4954
4950
|
const everyStatementIsLogging = statements.every(
|
|
@@ -4966,7 +4962,7 @@ var no_log_only_catch_default = createRule({
|
|
|
4966
4962
|
var import_utils25 = require("@typescript-eslint/utils");
|
|
4967
4963
|
var NO_BARE_RETURN_FROM_TEST_CATCH_DOCUMENTATION = {
|
|
4968
4964
|
summary: "Disallow a bare return from a test catch block when it skips a later assertion.",
|
|
4969
|
-
rationale: "
|
|
4965
|
+
rationale: "An unasserted catch return can swallow a failure and skip later assertions; the complete test result also depends on other assertions and hooks.",
|
|
4970
4966
|
remediation: "Rethrow the error, assert on it, or use the runner's explicit skip mechanism when the capability is optional.",
|
|
4971
4967
|
category: "testing",
|
|
4972
4968
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
@@ -5053,7 +5049,7 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
5053
5049
|
type: "problem",
|
|
5054
5050
|
docs: { description: "Disallow a bare return from a test catch block when it skips a later assertion." },
|
|
5055
5051
|
schema: [],
|
|
5056
|
-
messages: { bareReturnFromTestCatch: "This bare return
|
|
5052
|
+
messages: { bareReturnFromTestCatch: "This bare return can swallow the caught failure and skips a later assertion. Rethrow, assert on the error, or explicitly skip the test." }
|
|
5057
5053
|
},
|
|
5058
5054
|
defaultOptions: [],
|
|
5059
5055
|
create(context) {
|
|
@@ -5072,6 +5068,23 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
5072
5068
|
if (current === null || current === void 0) break;
|
|
5073
5069
|
}
|
|
5074
5070
|
if (catchClause === null || catchClause.parent.finalizer !== null) return;
|
|
5071
|
+
const parameter = catchClause.param;
|
|
5072
|
+
if (parameter?.type === import_utils25.AST_NODE_TYPES.Identifier && node.parent === catchClause.body) {
|
|
5073
|
+
const errorBinding = import_utils25.ASTUtils.findVariable(context.sourceCode.getScope(parameter), parameter.name);
|
|
5074
|
+
const assertedError = catchClause.body.body.some((statement) => {
|
|
5075
|
+
if (statement.range[1] >= node.range[0] || statement.type !== import_utils25.AST_NODE_TYPES.ExpressionStatement) return false;
|
|
5076
|
+
const expression = statement.expression;
|
|
5077
|
+
if (expression.type !== import_utils25.AST_NODE_TYPES.CallExpression || !isAssertion(expression, context)) return false;
|
|
5078
|
+
const root = rootIdentifier2(expression.callee);
|
|
5079
|
+
if (root === null) return false;
|
|
5080
|
+
const assertionName = importedName3(root, context, ASSERTION_MODULES);
|
|
5081
|
+
let operand = expression.callee.type === import_utils25.AST_NODE_TYPES.MemberExpression ? expression.callee.object : null;
|
|
5082
|
+
if (operand?.type === import_utils25.AST_NODE_TYPES.MemberExpression && staticMemberName2(operand) === "not") operand = operand.object;
|
|
5083
|
+
if (assertionName !== "assert" && (assertionName !== "expect" || operand?.type !== import_utils25.AST_NODE_TYPES.CallExpression || operand.callee !== root)) return false;
|
|
5084
|
+
return walkOwnScope(expression, (current) => current.type === import_utils25.AST_NODE_TYPES.Identifier && errorBinding?.references.some((reference) => reference.identifier === current) === true);
|
|
5085
|
+
});
|
|
5086
|
+
if (assertedError) return;
|
|
5087
|
+
}
|
|
5075
5088
|
if (walkOwnScope(catchClause.body, (current) => current.type === import_utils25.AST_NODE_TYPES.ThrowStatement || isExplicitSkip(current, context))) return;
|
|
5076
5089
|
if (!walkOwnScope(owner.body, (current) => current.range[0] > node.range[1] && isAssertion(current, context))) return;
|
|
5077
5090
|
context.report({ node, messageId: "bareReturnFromTestCatch" });
|
|
@@ -5083,13 +5096,13 @@ var no_bare_return_from_test_catch_default = createRule({
|
|
|
5083
5096
|
// src/rules/no-bespoke-api-case-conversion.ts
|
|
5084
5097
|
var import_utils26 = require("@typescript-eslint/utils");
|
|
5085
5098
|
var NO_BESPOKE_API_CASE_CONVERSION_DOCUMENTATION = {
|
|
5086
|
-
summary: "
|
|
5087
|
-
rationale: "
|
|
5099
|
+
summary: "Review direct snake_case/camelCase mirror mappings on explicitly API-typed adapter values.",
|
|
5100
|
+
rationale: "Duplicating wire-name translation can drift from an API client contract. When the SDK owns application-facing names, centralizing conversion avoids maintaining another mirror by hand.",
|
|
5088
5101
|
remediation: "Move wire-name ownership and case conversion into the generated SDK/model layer; keep application adapters on the generated typed surface.",
|
|
5089
5102
|
category: "architecture",
|
|
5090
5103
|
autofix: "none",
|
|
5091
5104
|
limitations: [
|
|
5092
|
-
"Only
|
|
5105
|
+
"Only adapter/adapters files and receivers explicitly annotated with a scope-resolved type imported from an API/client/SDK/contract/generated module are checked. Unrelated imports, local type shadows, reassigned receivers and inferred receiver types are excluded.",
|
|
5093
5106
|
"Only object properties that directly translate the same identifier between snake_case and lowerCamelCase are reported.",
|
|
5094
5107
|
"Quoted/computed protocol keys, generated/vendor code, tests, fixtures, and indirect conversions are intentionally excluded."
|
|
5095
5108
|
],
|
|
@@ -5161,7 +5174,7 @@ var no_bespoke_api_case_conversion_default = createRule({
|
|
|
5161
5174
|
docs: { description: NO_BESPOKE_API_CASE_CONVERSION_DOCUMENTATION.summary },
|
|
5162
5175
|
schema: [],
|
|
5163
5176
|
messages: {
|
|
5164
|
-
noBespokeApiCaseConversion: "This API adapter
|
|
5177
|
+
noBespokeApiCaseConversion: "This API-typed adapter value mirrors `{{wireName}}` and `{{applicationName}}`. If the SDK owns application-facing names, move this conversion to its model boundary."
|
|
5165
5178
|
}
|
|
5166
5179
|
},
|
|
5167
5180
|
defaultOptions: [],
|
|
@@ -5171,16 +5184,33 @@ var no_bespoke_api_case_conversion_default = createRule({
|
|
|
5171
5184
|
if (!ADAPTER_BASENAME_RE.test(basename) || isGeneratedFile(filename, context.sourceCode.text) || isTestFile(filename, ["fixtureTree"])) {
|
|
5172
5185
|
return {};
|
|
5173
5186
|
}
|
|
5174
|
-
const
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5187
|
+
const hasApiReceiver = (value) => {
|
|
5188
|
+
let current = value;
|
|
5189
|
+
while (true) {
|
|
5190
|
+
if (current.type === import_utils26.AST_NODE_TYPES.MemberExpression) current = current.object;
|
|
5191
|
+
else if (current.type === import_utils26.AST_NODE_TYPES.TSAsExpression || current.type === import_utils26.AST_NODE_TYPES.TSNonNullExpression || current.type === import_utils26.AST_NODE_TYPES.TSTypeAssertion) current = current.expression;
|
|
5192
|
+
else break;
|
|
5193
|
+
}
|
|
5194
|
+
if (current.type !== import_utils26.AST_NODE_TYPES.Identifier) return false;
|
|
5195
|
+
const binding = import_utils26.ASTUtils.findVariable(context.sourceCode.getScope(current), current.name);
|
|
5196
|
+
if (binding?.defs.length !== 1 || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
5197
|
+
const identifier = binding.defs[0]?.name;
|
|
5198
|
+
if (identifier?.type !== import_utils26.AST_NODE_TYPES.Identifier) return false;
|
|
5199
|
+
const annotation = identifier.typeAnnotation?.typeAnnotation;
|
|
5200
|
+
if (annotation?.type !== import_utils26.AST_NODE_TYPES.TSTypeReference) return false;
|
|
5201
|
+
let typeName = annotation.typeName;
|
|
5202
|
+
while (typeName.type === import_utils26.AST_NODE_TYPES.TSQualifiedName) typeName = typeName.left;
|
|
5203
|
+
if (typeName.type !== import_utils26.AST_NODE_TYPES.Identifier) return false;
|
|
5204
|
+
const typeBinding = import_utils26.ASTUtils.findVariable(context.sourceCode.getScope(typeName), typeName.name);
|
|
5205
|
+
return typeBinding?.defs.length === 1 && typeBinding.defs[0]?.type === "ImportBinding" && typeBinding.defs[0].parent.type === import_utils26.AST_NODE_TYPES.ImportDeclaration && API_BOUNDARY_IMPORT_RE.test(typeBinding.defs[0].parent.source.value);
|
|
5206
|
+
};
|
|
5178
5207
|
return {
|
|
5179
5208
|
Property(node) {
|
|
5180
5209
|
if (node.computed || node.method || node.shorthand) return;
|
|
5181
5210
|
const key = propertyName(node.key);
|
|
5182
5211
|
const value = memberName3(node.value);
|
|
5183
5212
|
if (key === null || value === null || !isDirectCaseTranslation(key, value)) return;
|
|
5213
|
+
if (!hasApiReceiver(node.value)) return;
|
|
5184
5214
|
const wireName = SNAKE_CASE_RE.test(key) ? key : value;
|
|
5185
5215
|
const applicationName = wireName === key ? value : key;
|
|
5186
5216
|
context.report({
|
|
@@ -5303,7 +5333,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
|
|
|
5303
5333
|
limitations: [
|
|
5304
5334
|
"Only ESLint disable comments and TypeScript expect-error directives are checked.",
|
|
5305
5335
|
"The rule uses a small anchored vocabulary and does not score prose quality generally.",
|
|
5306
|
-
"Generated files and descriptions
|
|
5336
|
+
"Generated files and descriptions outside that exact vocabulary are excluded; an unflagged reason is not proof of a justified suppression. Missing descriptions remain owned by the upstream description requirement."
|
|
5307
5337
|
],
|
|
5308
5338
|
examples: [
|
|
5309
5339
|
{
|
|
@@ -5313,7 +5343,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
|
|
|
5313
5343
|
files: [
|
|
5314
5344
|
{
|
|
5315
5345
|
path: "src/adapter.ts",
|
|
5316
|
-
source: "// @ts-expect-error -- vendor types omit the runtime requestId field\
|
|
5346
|
+
source: "function requestId(response: object) {\n // @ts-expect-error -- vendor types omit the runtime requestId field\n return response.requestId;\n}"
|
|
5317
5347
|
}
|
|
5318
5348
|
],
|
|
5319
5349
|
focusPath: "src/adapter.ts",
|
|
@@ -5327,7 +5357,7 @@ var NO_VAGUE_SUPPRESSION_DESCRIPTION_DOCUMENTATION = {
|
|
|
5327
5357
|
files: [
|
|
5328
5358
|
{
|
|
5329
5359
|
path: "src/adapter.ts",
|
|
5330
|
-
source: "// @ts-expect-error -- false positive\
|
|
5360
|
+
source: "function requestId(response: object) {\n // @ts-expect-error -- false positive\n return response.requestId;\n}"
|
|
5331
5361
|
}
|
|
5332
5362
|
],
|
|
5333
5363
|
focusPath: "src/adapter.ts",
|
|
@@ -5378,9 +5408,9 @@ var NO_GENERIC_SINGLE_EXPORT_MODULE_DOCUMENTATION = {
|
|
|
5378
5408
|
rationale: "A generic filename hides the sole exported responsibility and makes navigation less descriptive.",
|
|
5379
5409
|
remediation: "Choose a responsibility-bearing module name or colocate the export with its domain.",
|
|
5380
5410
|
category: "maintainability",
|
|
5381
|
-
limitations: ["Only
|
|
5411
|
+
limitations: ["Only the fixed generic-stem vocabulary with exactly one public runtime export is checked; exported destructuring patterns are excluded rather than undercounted."],
|
|
5382
5412
|
examples: [
|
|
5383
|
-
{ id: "responsibility-named-module", title: "Name the module after its export", outcome: "no-match", files: [{ path: "src/order
|
|
5413
|
+
{ id: "responsibility-named-module", title: "Name the module after its export", outcome: "no-match", files: [{ path: "src/parse-order.ts", source: "export function parseOrder() { return {}; }" }], focusPath: "src/parse-order.ts", expectedCount: 0, public: true },
|
|
5384
5414
|
{ id: "generic-module-name", title: "Do not hide one export in a generic module", outcome: "match", files: [{ path: "src/utils.ts", source: "export function parseOrder() { return {}; }" }], focusPath: "src/utils.ts", expectedCount: 1, public: true }
|
|
5385
5415
|
]
|
|
5386
5416
|
};
|
|
@@ -5441,6 +5471,7 @@ function runtimeExports(program) {
|
|
|
5441
5471
|
}
|
|
5442
5472
|
if (statement.declaration !== null) {
|
|
5443
5473
|
const declaration = statement.declaration;
|
|
5474
|
+
if (declaration.type === import_utils28.AST_NODE_TYPES.VariableDeclaration && declaration.declarations.some((item) => item.id.type !== import_utils28.AST_NODE_TYPES.Identifier)) ambiguous = true;
|
|
5444
5475
|
exports2.push(...declaredNames(declaration).map((name) => ({ key: name, name, node: declaration })));
|
|
5445
5476
|
}
|
|
5446
5477
|
for (const specifier of statement.specifiers) {
|
|
@@ -5559,11 +5590,11 @@ var no_generic_single_export_module_default = createRule({
|
|
|
5559
5590
|
// src/rules/no-offset-pagination.ts
|
|
5560
5591
|
var import_utils29 = require("@typescript-eslint/utils");
|
|
5561
5592
|
var NO_OFFSET_PAGINATION_DOCUMENTATION = {
|
|
5562
|
-
summary: "
|
|
5593
|
+
summary: "Prefer keyset pagination for embedded SQL queries using OFFSET.",
|
|
5563
5594
|
rationale: "Offset pagination scans skipped rows and shifts page boundaries under concurrent writes.",
|
|
5564
|
-
remediation: "
|
|
5595
|
+
remediation: "Consider a keyset cursor that preserves the query's complete ordering, tie-breakers, and filters.",
|
|
5565
5596
|
category: "performance",
|
|
5566
|
-
limitations: ["
|
|
5597
|
+
limitations: ["A SELECT/FROM query shape or adjacent LIMIT/OFFSET fragment is required. Isolated OFFSET fragments and test files are excluded; this lexical context does not prove a database execution sink. Performance and concurrent-write behavior depend on indexes, ordering, isolation, and dialect; bounded pages and random page access can justify OFFSET."],
|
|
5567
5598
|
examples: [
|
|
5568
5599
|
{ id: "keyset-pagination", title: "Page from a stable cursor", outcome: "no-match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT id FROM runs WHERE id > ? ORDER BY id LIMIT ?`).all();" }], focusPath: "src/runs.ts", expectedCount: 0, public: true },
|
|
5569
5600
|
{ id: "offset-pagination", title: "Do not page by offset", outcome: "match", files: [{ path: "src/runs.ts", source: "db.query(`SELECT id FROM runs ORDER BY id LIMIT ? OFFSET ?`);" }], focusPath: "src/runs.ts", expectedCount: 1, public: true }
|
|
@@ -5571,17 +5602,18 @@ var NO_OFFSET_PAGINATION_DOCUMENTATION = {
|
|
|
5571
5602
|
};
|
|
5572
5603
|
var OFFSET_PAGINATION = /\bOFFSET\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
|
|
5573
5604
|
var OFFSET_GATE = /offset/i;
|
|
5605
|
+
var PAGINATION_CONTEXT = /\bSELECT\b[\s\S]*\bFROM\b[\s\S]*\bOFFSET\b|\bLIMIT\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)\s+OFFSET\b/i;
|
|
5574
5606
|
var no_offset_pagination_default = createRule({
|
|
5575
5607
|
name: "no-offset-pagination",
|
|
5576
5608
|
documentation: NO_OFFSET_PAGINATION_DOCUMENTATION,
|
|
5577
5609
|
meta: {
|
|
5578
5610
|
type: "problem",
|
|
5579
5611
|
docs: {
|
|
5580
|
-
description: "
|
|
5612
|
+
description: "Prefer keyset pagination for embedded SQL queries using OFFSET."
|
|
5581
5613
|
},
|
|
5582
5614
|
schema: [],
|
|
5583
5615
|
messages: {
|
|
5584
|
-
noOffsetPagination: "OFFSET pagination
|
|
5616
|
+
noOffsetPagination: "Review OFFSET pagination for large or changing result sets. If a keyset cursor fits the access pattern, preserve the query's complete ordering, tie-breakers, and filters; bounded pages or random page access may justify OFFSET."
|
|
5585
5617
|
}
|
|
5586
5618
|
},
|
|
5587
5619
|
defaultOptions: [],
|
|
@@ -5590,7 +5622,7 @@ var no_offset_pagination_default = createRule({
|
|
|
5590
5622
|
return {};
|
|
5591
5623
|
}
|
|
5592
5624
|
return createSqlListener((sql, node) => {
|
|
5593
|
-
if (!OFFSET_PAGINATION.test(sql)) {
|
|
5625
|
+
if (!PAGINATION_CONTEXT.test(sql) || !OFFSET_PAGINATION.test(sql)) {
|
|
5594
5626
|
return;
|
|
5595
5627
|
}
|
|
5596
5628
|
context.report({ node, messageId: "noOffsetPagination" });
|
|
@@ -5629,7 +5661,7 @@ function tupleReturnType(node, aliases, resolving = /* @__PURE__ */ new Set()) {
|
|
|
5629
5661
|
return argument === void 0 ? null : tupleReturnType(argument, aliases, resolving);
|
|
5630
5662
|
}
|
|
5631
5663
|
if (node.type === import_utils30.AST_NODE_TYPES.TSTypeReference && node.typeName.type === import_utils30.AST_NODE_TYPES.Identifier && !resolving.has(node.typeName.name)) {
|
|
5632
|
-
const target = aliases.get(node.typeName
|
|
5664
|
+
const target = aliases.get(node.typeName);
|
|
5633
5665
|
if (target !== void 0) return tupleReturnType(target, aliases, /* @__PURE__ */ new Set([...resolving, node.typeName.name]));
|
|
5634
5666
|
}
|
|
5635
5667
|
if (node.type === import_utils30.AST_NODE_TYPES.TSTypeOperator && node.operator === "readonly") {
|
|
@@ -5721,20 +5753,27 @@ function exportedTypeNames(program) {
|
|
|
5721
5753
|
}
|
|
5722
5754
|
return names;
|
|
5723
5755
|
}
|
|
5724
|
-
function typeAliases(
|
|
5756
|
+
function typeAliases(sourceCode) {
|
|
5725
5757
|
const aliases = /* @__PURE__ */ new Map();
|
|
5726
|
-
for (const statement of
|
|
5758
|
+
for (const statement of sourceCode.ast.body) {
|
|
5727
5759
|
const declaration = statement.type === import_utils30.AST_NODE_TYPES.ExportNamedDeclaration ? statement.declaration : statement;
|
|
5728
5760
|
if (declaration?.type === import_utils30.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
5729
|
-
aliases.set(declaration.id.name, declaration
|
|
5761
|
+
aliases.set(declaration.id.name, declaration);
|
|
5730
5762
|
}
|
|
5731
5763
|
}
|
|
5732
|
-
return
|
|
5764
|
+
return {
|
|
5765
|
+
get(identifier) {
|
|
5766
|
+
const declaration = aliases.get(identifier.name);
|
|
5767
|
+
if (declaration === void 0) return void 0;
|
|
5768
|
+
const binding = import_utils30.ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
5769
|
+
return binding?.defs.length === 1 && binding.defs[0]?.node === declaration ? declaration.typeAnnotation : void 0;
|
|
5770
|
+
}
|
|
5771
|
+
};
|
|
5733
5772
|
}
|
|
5734
5773
|
function callableReturnType(node, aliases, resolving = /* @__PURE__ */ new Set()) {
|
|
5735
5774
|
if (node.type === import_utils30.AST_NODE_TYPES.TSFunctionType) return node.returnType?.typeAnnotation ?? null;
|
|
5736
5775
|
if (node.type === import_utils30.AST_NODE_TYPES.TSTypeReference && node.typeName.type === import_utils30.AST_NODE_TYPES.Identifier && !resolving.has(node.typeName.name)) {
|
|
5737
|
-
const target = aliases.get(node.typeName
|
|
5776
|
+
const target = aliases.get(node.typeName);
|
|
5738
5777
|
if (target !== void 0) {
|
|
5739
5778
|
return callableReturnType(target, aliases, /* @__PURE__ */ new Set([...resolving, node.typeName.name]));
|
|
5740
5779
|
}
|
|
@@ -5863,7 +5902,7 @@ var no_positional_tuple_return_default = createRule({
|
|
|
5863
5902
|
context.sourceCode.ast,
|
|
5864
5903
|
exportedTypeNames(context.sourceCode.ast)
|
|
5865
5904
|
);
|
|
5866
|
-
const aliases = typeAliases(context.sourceCode
|
|
5905
|
+
const aliases = typeAliases(context.sourceCode);
|
|
5867
5906
|
const reportedFunctions = /* @__PURE__ */ new WeakSet();
|
|
5868
5907
|
const functionStack = [];
|
|
5869
5908
|
const report2 = (annotation, name) => {
|
|
@@ -6403,11 +6442,11 @@ var no_raw_fetch_outside_clients_default = createRule({
|
|
|
6403
6442
|
// src/rules/no-restricted-library-load.ts
|
|
6404
6443
|
var import_utils33 = require("@typescript-eslint/utils");
|
|
6405
6444
|
var NO_RESTRICTED_LIBRARY_LOAD_DOCUMENTATION = {
|
|
6406
|
-
summary: "Apply
|
|
6407
|
-
rationale: "
|
|
6408
|
-
remediation: "
|
|
6445
|
+
summary: "Apply configured library restrictions to literal runtime loads and CommonJS resolution references.",
|
|
6446
|
+
rationale: "Dynamic imports, CommonJS loads, and package resolution checks can bypass library restrictions enforced for static imports.",
|
|
6447
|
+
remediation: "Use the configured replacement for the runtime dependency reference; resolution checks do not themselves load a module.",
|
|
6409
6448
|
category: "architecture",
|
|
6410
|
-
limitations: ["Only literal dynamic imports, unshadowed CommonJS loads, and TypeScript import-equals declarations are checked."],
|
|
6449
|
+
limitations: ["Only literal dynamic imports, unshadowed CommonJS loads/resolution calls, and runtime TypeScript import-equals declarations are checked; erased type imports are excluded. A configured restriction list is required."],
|
|
6411
6450
|
examples: [
|
|
6412
6451
|
{ id: "static-import", title: "Static imports remain the static-import rule's responsibility", outcome: "no-match", files: [{ path: "src/client.ts", source: "import axios from 'axios';" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
|
|
6413
6452
|
{ id: "runtime-load", title: "Do not load a restricted library at runtime", outcome: "match", files: [{ path: "src/client.ts", source: "const client = require('axios');" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
|
|
@@ -6425,7 +6464,7 @@ var no_restricted_library_load_default = createRule({
|
|
|
6425
6464
|
meta: {
|
|
6426
6465
|
type: "problem",
|
|
6427
6466
|
docs: {
|
|
6428
|
-
description:
|
|
6467
|
+
description: NO_RESTRICTED_LIBRARY_LOAD_DOCUMENTATION.summary
|
|
6429
6468
|
},
|
|
6430
6469
|
schema: [
|
|
6431
6470
|
{
|
|
@@ -6451,7 +6490,7 @@ var no_restricted_library_load_default = createRule({
|
|
|
6451
6490
|
}
|
|
6452
6491
|
],
|
|
6453
6492
|
messages: {
|
|
6454
|
-
restrictedLibraryLoad: "{{id}}: Replace runtime
|
|
6493
|
+
restrictedLibraryLoad: "{{id}}: Replace this runtime dependency reference to {{module}} with {{replacement}}.{{note}}"
|
|
6455
6494
|
}
|
|
6456
6495
|
},
|
|
6457
6496
|
defaultOptions: [{ libraries: [] }],
|
|
@@ -6497,6 +6536,7 @@ var no_restricted_library_load_default = createRule({
|
|
|
6497
6536
|
if (source !== null) report2(node.arguments[0], source);
|
|
6498
6537
|
},
|
|
6499
6538
|
TSImportEqualsDeclaration(node) {
|
|
6539
|
+
if (node.importKind === "type") return;
|
|
6500
6540
|
if (node.moduleReference.type !== import_utils33.AST_NODE_TYPES.TSExternalModuleReference) return;
|
|
6501
6541
|
const source = literalModule(node.moduleReference.expression);
|
|
6502
6542
|
if (source !== null) report2(node.moduleReference.expression, source);
|
|
@@ -6613,7 +6653,7 @@ var NO_REPEATED_STRING_LITERAL_DOCUMENTATION = {
|
|
|
6613
6653
|
]
|
|
6614
6654
|
};
|
|
6615
6655
|
function isStructured(value) {
|
|
6616
|
-
return
|
|
6656
|
+
return SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value) || URL_PATH_RE.test(value);
|
|
6617
6657
|
}
|
|
6618
6658
|
function preview(value) {
|
|
6619
6659
|
const oneLine = value.replaceAll("\n", " ").trim();
|
|
@@ -6634,7 +6674,7 @@ function isScaffolding(node) {
|
|
|
6634
6674
|
}
|
|
6635
6675
|
const isNonComputedPropertyKey = (parent.type === import_utils35.AST_NODE_TYPES.Property || parent.type === import_utils35.AST_NODE_TYPES.PropertyDefinition || parent.type === import_utils35.AST_NODE_TYPES.MethodDefinition || parent.type === import_utils35.AST_NODE_TYPES.AccessorProperty) && parent.key === node && !parent.computed;
|
|
6636
6676
|
const isRequireSource = parent.type === import_utils35.AST_NODE_TYPES.CallExpression && parent.callee.type === import_utils35.AST_NODE_TYPES.Identifier && parent.callee.name === "require";
|
|
6637
|
-
return parent.type === import_utils35.AST_NODE_TYPES.ImportDeclaration || parent.type === import_utils35.AST_NODE_TYPES.ImportExpression || parent.type === import_utils35.AST_NODE_TYPES.ExportNamedDeclaration || parent.type === import_utils35.AST_NODE_TYPES.ExportAllDeclaration || parent.type === import_utils35.AST_NODE_TYPES.TSImportType || parent.type === import_utils35.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils35.AST_NODE_TYPES.TSLiteralType || isNonComputedPropertyKey || isRequireSource;
|
|
6677
|
+
return parent.type === import_utils35.AST_NODE_TYPES.ImportDeclaration || parent.type === import_utils35.AST_NODE_TYPES.ImportExpression || parent.type === import_utils35.AST_NODE_TYPES.ExportNamedDeclaration || parent.type === import_utils35.AST_NODE_TYPES.ExportAllDeclaration || parent.type === import_utils35.AST_NODE_TYPES.TSImportType || parent.type === import_utils35.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils35.AST_NODE_TYPES.JSXExpressionContainer && parent.parent.type === import_utils35.AST_NODE_TYPES.JSXAttribute || parent.type === import_utils35.AST_NODE_TYPES.TSLiteralType || isNonComputedPropertyKey || isRequireSource;
|
|
6638
6678
|
}
|
|
6639
6679
|
var no_repeated_string_literal_default = createRule({
|
|
6640
6680
|
name: "no-repeated-string-literal",
|
|
@@ -7554,11 +7594,11 @@ var no_server_env_in_client_component_default = createRule({
|
|
|
7554
7594
|
// src/rules/no-select-star.ts
|
|
7555
7595
|
var import_utils41 = require("@typescript-eslint/utils");
|
|
7556
7596
|
var NO_SELECT_STAR_DOCUMENTATION = {
|
|
7557
|
-
summary: "
|
|
7597
|
+
summary: "Prefer explicit column projections over SELECT * in embedded SQL.",
|
|
7558
7598
|
rationale: "Wildcard projections couple row shape and query cost to unrelated schema changes.",
|
|
7559
7599
|
remediation: "List every required column explicitly in the projection.",
|
|
7560
7600
|
category: "correctness",
|
|
7561
|
-
limitations: ["Only statically visible embedded SQL is checked; function arguments such as COUNT(*) and stars inside EXISTS are excluded."],
|
|
7601
|
+
limitations: ["Only statically visible embedded SQL is checked; quoted strings (including PostgreSQL dollar strings), comments, function arguments such as COUNT(*), and stars inside EXISTS are excluded. This is a bounded lexical scan, not a complete SQL parser."],
|
|
7562
7602
|
examples: [
|
|
7563
7603
|
{ id: "explicit-projection", title: "Select the required columns", outcome: "no-match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT id, status FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 0, public: true },
|
|
7564
7604
|
{ id: "wildcard-projection", title: "Do not select every column", outcome: "match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT * FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 1, public: true }
|
|
@@ -7609,7 +7649,7 @@ var no_select_star_default = createRule({
|
|
|
7609
7649
|
meta: {
|
|
7610
7650
|
type: "problem",
|
|
7611
7651
|
docs: {
|
|
7612
|
-
description: "
|
|
7652
|
+
description: "Prefer explicit column projections over SELECT * in embedded SQL."
|
|
7613
7653
|
},
|
|
7614
7654
|
schema: [],
|
|
7615
7655
|
messages: {
|
|
@@ -7637,7 +7677,7 @@ var NO_SENTINEL_RETURN_ON_CATCH_DOCUMENTATION = {
|
|
|
7637
7677
|
rationale: "An unreported fallback makes operational failure indistinguishable from a legitimate empty result.",
|
|
7638
7678
|
remediation: "Rethrow, report the error before returning, or model expected absence with an explicit predicate, safe-parse, or result contract.",
|
|
7639
7679
|
category: "correctness",
|
|
7640
|
-
limitations: ["Recognized predicate, safe-parse, normal-path sentinel, deliberate parse, generated-client, and configured logging patterns are excluded."],
|
|
7680
|
+
limitations: ["Recognized predicate, safe-parse, normal-path sentinel, deliberate parse, generated-client, and configured logging patterns are excluded. Locally shadowed undefined bindings are not treated as sentinels; recognized handling patterns are not a proof that every control-flow path handles the error."],
|
|
7641
7681
|
examples: [
|
|
7642
7682
|
{ id: "reported-fallback", title: "Report an error before returning a fallback", outcome: "no-match", files: [{ path: "src/load.ts", source: "function load() { try { return read(); } catch (error) { logger.warn('load failed', error); return null; } }" }], focusPath: "src/load.ts", expectedCount: 0, public: true },
|
|
7643
7683
|
{ id: "silent-fallback", title: "Do not turn an unreported error into absence", outcome: "match", files: [{ path: "src/load.ts", source: "function load() { try { return read(); } catch { return null; } }" }], focusPath: "src/load.ts", expectedCount: 1, public: true }
|
|
@@ -8052,6 +8092,8 @@ var no_sentinel_return_on_catch_default = createRule({
|
|
|
8052
8092
|
if (!isSentinelArgument(last.argument)) {
|
|
8053
8093
|
return;
|
|
8054
8094
|
}
|
|
8095
|
+
const returned = unwrapSentinelExpression(last.argument);
|
|
8096
|
+
if (returned?.type === import_utils42.AST_NODE_TYPES.Identifier && returned.name === "undefined" && (import_utils42.ASTUtils.findVariable(context.sourceCode.getScope(returned), returned.name)?.defs.length ?? 0) > 0) return;
|
|
8055
8097
|
if (containsThrow(node.body)) {
|
|
8056
8098
|
return;
|
|
8057
8099
|
}
|
|
@@ -8091,7 +8133,7 @@ var NO_SILENT_PROMISE_CATCH_DOCUMENTATION = {
|
|
|
8091
8133
|
rationale: "A swallowed rejection hides failures and gives callers an indistinguishable fallback value.",
|
|
8092
8134
|
remediation: "Log, rethrow, or explicitly recover from the rejection; explain intentional teardown suppression.",
|
|
8093
8135
|
category: "correctness",
|
|
8094
|
-
limitations: ["Test files, teardown calls, explanatory comments, non-function handlers, and handlers that consume or report the error are excluded."],
|
|
8136
|
+
limitations: ["Test files, teardown calls, explanatory comments, non-function handlers, and handlers that consume or report the error are excluded.", "Recognized imported Zod construction chains and their stable local aliases are excluded. Other untyped catch-like APIs are not proven to be Promises."],
|
|
8095
8137
|
examples: [
|
|
8096
8138
|
{ id: "reported-rejection", title: "Report the rejection", outcome: "no-match", files: [{ path: "src/load.ts", source: "load().catch((error) => logger.error({ error }, 'load failed'));" }], focusPath: "src/load.ts", expectedCount: 0, public: true },
|
|
8097
8139
|
{ id: "silent-rejection", title: "Do not swallow the rejection", outcome: "match", files: [{ path: "src/load.ts", source: "load().catch(() => null);" }], focusPath: "src/load.ts", expectedCount: 1, public: true }
|
|
@@ -8105,6 +8147,43 @@ var BODY_PARSE_METHODS = /* @__PURE__ */ new Set([
|
|
|
8105
8147
|
"json",
|
|
8106
8148
|
"text"
|
|
8107
8149
|
]);
|
|
8150
|
+
var ZOD_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
8151
|
+
"any",
|
|
8152
|
+
"array",
|
|
8153
|
+
"bigint",
|
|
8154
|
+
"boolean",
|
|
8155
|
+
"custom",
|
|
8156
|
+
"date",
|
|
8157
|
+
"enum",
|
|
8158
|
+
"literal",
|
|
8159
|
+
"map",
|
|
8160
|
+
"never",
|
|
8161
|
+
"null",
|
|
8162
|
+
"number",
|
|
8163
|
+
"object",
|
|
8164
|
+
"record",
|
|
8165
|
+
"set",
|
|
8166
|
+
"string",
|
|
8167
|
+
"tuple",
|
|
8168
|
+
"undefined",
|
|
8169
|
+
"union",
|
|
8170
|
+
"unknown"
|
|
8171
|
+
]);
|
|
8172
|
+
var ZOD_CHAIN_METHODS = /* @__PURE__ */ new Set([
|
|
8173
|
+
"array",
|
|
8174
|
+
"catch",
|
|
8175
|
+
"default",
|
|
8176
|
+
"describe",
|
|
8177
|
+
"max",
|
|
8178
|
+
"min",
|
|
8179
|
+
"nullable",
|
|
8180
|
+
"nullish",
|
|
8181
|
+
"optional",
|
|
8182
|
+
"readonly",
|
|
8183
|
+
"refine",
|
|
8184
|
+
"superRefine",
|
|
8185
|
+
"transform"
|
|
8186
|
+
]);
|
|
8108
8187
|
function isBodyParseCall(node) {
|
|
8109
8188
|
return node.type === import_utils43.AST_NODE_TYPES.CallExpression && node.arguments.length === 0 && node.callee.type === import_utils43.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils43.AST_NODE_TYPES.Identifier && BODY_PARSE_METHODS.has(node.callee.property.name);
|
|
8110
8189
|
}
|
|
@@ -8178,6 +8257,26 @@ var no_silent_promise_catch_default = createRule({
|
|
|
8178
8257
|
if (isTestFile(context.filename) || isScriptFile(context.filename)) {
|
|
8179
8258
|
return {};
|
|
8180
8259
|
}
|
|
8260
|
+
function isZodSchema(node, seen = /* @__PURE__ */ new Set()) {
|
|
8261
|
+
if (seen.has(node)) return false;
|
|
8262
|
+
seen.add(node);
|
|
8263
|
+
if (node.type === import_utils43.AST_NODE_TYPES.Identifier) {
|
|
8264
|
+
const binding = import_utils43.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
|
|
8265
|
+
if (binding === null || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
8266
|
+
const [definition] = binding.defs;
|
|
8267
|
+
return binding.defs.length === 1 && definition?.node.type === import_utils43.AST_NODE_TYPES.VariableDeclarator && definition.node.init !== null && isZodSchema(definition.node.init, seen);
|
|
8268
|
+
}
|
|
8269
|
+
if (node.type !== import_utils43.AST_NODE_TYPES.CallExpression || node.callee.type !== import_utils43.AST_NODE_TYPES.MemberExpression || node.callee.computed || node.callee.property.type !== import_utils43.AST_NODE_TYPES.Identifier) return false;
|
|
8270
|
+
const { object, property } = node.callee;
|
|
8271
|
+
if (object.type === import_utils43.AST_NODE_TYPES.Identifier && ZOD_CONSTRUCTORS.has(property.name)) {
|
|
8272
|
+
const binding = import_utils43.ASTUtils.findVariable(context.sourceCode.getScope(object), object.name);
|
|
8273
|
+
if (binding?.defs.some((definition) => {
|
|
8274
|
+
const specifier = definition.node;
|
|
8275
|
+
return (specifier.type === import_utils43.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils43.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils43.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils43.AST_NODE_TYPES.Identifier && specifier.imported.name === "z") && specifier.parent.type === import_utils43.AST_NODE_TYPES.ImportDeclaration && isZodModule(String(specifier.parent.source.value));
|
|
8276
|
+
})) return true;
|
|
8277
|
+
}
|
|
8278
|
+
return ZOD_CHAIN_METHODS.has(property.name) && isZodSchema(object, seen);
|
|
8279
|
+
}
|
|
8181
8280
|
const hasExplanatoryComment = (call, handler) => {
|
|
8182
8281
|
const sourceCode = context.sourceCode;
|
|
8183
8282
|
if (sourceCode.getCommentsInside(handler).some(isExplanatory)) {
|
|
@@ -8202,6 +8301,7 @@ var no_silent_promise_catch_default = createRule({
|
|
|
8202
8301
|
const method = node.callee.property.name;
|
|
8203
8302
|
const handlerIndex = method === "catch" ? 0 : method === "then" ? 1 : null;
|
|
8204
8303
|
if (handlerIndex === null) return;
|
|
8304
|
+
if (method === "catch" && isZodSchema(node.callee.object)) return;
|
|
8205
8305
|
if (isBodyParseCall(node.callee.object)) {
|
|
8206
8306
|
return;
|
|
8207
8307
|
}
|
|
@@ -8236,14 +8336,14 @@ var no_silent_promise_catch_default = createRule({
|
|
|
8236
8336
|
// src/rules/no-sleep-in-test-body.ts
|
|
8237
8337
|
var import_utils44 = require("@typescript-eslint/utils");
|
|
8238
8338
|
var NO_SLEEP_IN_TEST_BODY_DOCUMENTATION = {
|
|
8239
|
-
summary: "
|
|
8339
|
+
summary: "Avoid fixed timed sleeps directly in test bodies; synchronize on observable behavior or use controlled timers.",
|
|
8240
8340
|
rationale: "Wall-clock delays make test correctness depend on scheduler and machine speed.",
|
|
8241
|
-
remediation: "Await the observable signal or advance
|
|
8341
|
+
remediation: "Await the observable signal, or advance fake timers when supported and restore real timers in finally or a teardown hook.",
|
|
8242
8342
|
category: "testing",
|
|
8243
8343
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
8244
|
-
limitations: ["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked; nested fakes
|
|
8344
|
+
limitations: ["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked; nested fakes, parameterized delays, local helper bindings, and Promise executors with additional work or rejection callbacks are excluded."],
|
|
8245
8345
|
examples: [
|
|
8246
|
-
{ id: "fake-timer", title: "Advance time
|
|
8346
|
+
{ id: "fake-timer", title: "Advance controlled time and restore real timers", outcome: "no-match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { vi.useFakeTimers(); try { const result = retry(); await vi.advanceTimersByTimeAsync(50); await result; } finally { vi.useRealTimers(); } });" }], focusPath: "src/retry.test.ts", expectedCount: 0, public: true },
|
|
8247
8347
|
{ id: "fixed-sleep", title: "Do not wait for wall-clock time", outcome: "match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { await sleep(50); expect(done()).toBe(true); });" }], focusPath: "src/retry.test.ts", expectedCount: 1, public: true }
|
|
8248
8348
|
]
|
|
8249
8349
|
};
|
|
@@ -8262,9 +8362,6 @@ var FUNCTION_TYPES5 = /* @__PURE__ */ new Set([
|
|
|
8262
8362
|
function isNonzeroNumericLiteral(node) {
|
|
8263
8363
|
return node?.type === import_utils44.AST_NODE_TYPES.Literal && typeof node.value === "number" && node.value !== 0;
|
|
8264
8364
|
}
|
|
8265
|
-
function isTimedSetTimeout(node) {
|
|
8266
|
-
return node.type === import_utils44.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils44.AST_NODE_TYPES.Identifier && node.callee.name === "setTimeout" && node.arguments.length >= 2 && isNonzeroNumericLiteral(node.arguments[1]);
|
|
8267
|
-
}
|
|
8268
8365
|
function isPromiseSleep(node) {
|
|
8269
8366
|
if (node.callee.type !== import_utils44.AST_NODE_TYPES.Identifier || node.callee.name !== "Promise") {
|
|
8270
8367
|
return false;
|
|
@@ -8274,12 +8371,14 @@ function isPromiseSleep(node) {
|
|
|
8274
8371
|
return false;
|
|
8275
8372
|
}
|
|
8276
8373
|
const body2 = executor.body;
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8374
|
+
const resolve2 = executor.params[0];
|
|
8375
|
+
if (executor.params.length !== 1 || resolve2?.type !== import_utils44.AST_NODE_TYPES.Identifier || resolve2.name === "setTimeout") return false;
|
|
8376
|
+
const statement = body2.type === import_utils44.AST_NODE_TYPES.BlockStatement && body2.body.length === 1 ? body2.body[0] : null;
|
|
8377
|
+
const timer = body2.type !== import_utils44.AST_NODE_TYPES.BlockStatement ? body2 : statement?.type === import_utils44.AST_NODE_TYPES.ExpressionStatement ? statement.expression : null;
|
|
8378
|
+
return timer?.type === import_utils44.AST_NODE_TYPES.CallExpression && isTimedSetTimeout(timer) && timer.arguments[0]?.type === import_utils44.AST_NODE_TYPES.Identifier && timer.arguments[0].name === resolve2.name;
|
|
8379
|
+
}
|
|
8380
|
+
function isTimedSetTimeout(node) {
|
|
8381
|
+
return node.type === import_utils44.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils44.AST_NODE_TYPES.Identifier && node.callee.name === "setTimeout" && node.arguments.length >= 2 && isNonzeroNumericLiteral(node.arguments[1]);
|
|
8283
8382
|
}
|
|
8284
8383
|
function isHelperSleep(node) {
|
|
8285
8384
|
return node.callee.type === import_utils44.AST_NODE_TYPES.Identifier && SLEEP_HELPERS.has(node.callee.name) && node.arguments.length >= 1 && isNonzeroNumericLiteral(node.arguments[0]);
|
|
@@ -8333,11 +8432,11 @@ var no_sleep_in_test_body_default = createRule({
|
|
|
8333
8432
|
meta: {
|
|
8334
8433
|
type: "problem",
|
|
8335
8434
|
docs: {
|
|
8336
|
-
description: "
|
|
8435
|
+
description: "Avoid fixed timed sleeps directly in test bodies; synchronize on observable behavior or use controlled timers."
|
|
8337
8436
|
},
|
|
8338
8437
|
schema: [],
|
|
8339
8438
|
messages: {
|
|
8340
|
-
noSleepInTestBody: "A fixed sleep
|
|
8439
|
+
noSleepInTestBody: "A fixed sleep depends on wall-clock timing and can be flaky under load. Await observable completion or use controlled fake timers, restoring real timers afterward."
|
|
8341
8440
|
}
|
|
8342
8441
|
},
|
|
8343
8442
|
defaultOptions: [],
|
|
@@ -8358,11 +8457,17 @@ var no_sleep_in_test_body_default = createRule({
|
|
|
8358
8457
|
return {
|
|
8359
8458
|
NewExpression(node) {
|
|
8360
8459
|
if (isPromiseSleep(node)) {
|
|
8460
|
+
const constructor = import_utils44.ASTUtils.findVariable(context.sourceCode.getScope(node), "Promise");
|
|
8461
|
+
const timer = import_utils44.ASTUtils.findVariable(context.sourceCode.getScope(node), "setTimeout");
|
|
8462
|
+
if ((constructor?.defs.length ?? 0) > 0 || (timer?.defs.length ?? 0) > 0) return;
|
|
8361
8463
|
report2(node);
|
|
8362
8464
|
}
|
|
8363
8465
|
},
|
|
8364
8466
|
CallExpression(node) {
|
|
8365
8467
|
if (isHelperSleep(node)) {
|
|
8468
|
+
if (node.callee.type !== import_utils44.AST_NODE_TYPES.Identifier) return;
|
|
8469
|
+
const variable = import_utils44.ASTUtils.findVariable(context.sourceCode.getScope(node), node.callee.name);
|
|
8470
|
+
if (variable?.defs.some((definition) => definition.type !== "ImportBinding")) return;
|
|
8366
8471
|
report2(node);
|
|
8367
8472
|
}
|
|
8368
8473
|
}
|
|
@@ -8383,7 +8488,7 @@ var NO_STORAGE_IN_STATELESS_MODULES_DOCUMENTATION = {
|
|
|
8383
8488
|
rationale: "Private storage in a stateless workflow creates another source of truth that can silently diverge.",
|
|
8384
8489
|
remediation: "Read from the system of record or derive state from an artifact the workflow already produces.",
|
|
8385
8490
|
category: "architecture",
|
|
8386
|
-
limitations: ["
|
|
8491
|
+
limitations: ["This opt-in architectural policy requires configured module paths and storage method names. Overloaded `put` requires storage-like receiver evidence; `prepare` requires SQL-shaped literal text or a conventional database receiver for dynamic text. These syntax heuristics do not prove database provenance or identify the system of record."],
|
|
8387
8492
|
examples: [
|
|
8388
8493
|
{ id: "system-of-record", title: "Read from the system of record", outcome: "no-match", files: [{ path: "src/engineer-digest/post.ts", source: "const issues = await linear.listIssues();" }], focusPath: "src/engineer-digest/post.ts", expectedCount: 0, public: true },
|
|
8389
8494
|
{ id: "private-storage", title: "Do not write private state in a stateless module", outcome: "match", files: [{ path: "src/engineer-digest/post.ts", source: "await kv.put('digest:last', timestamp);" }], focusPath: "src/engineer-digest/post.ts", expectedCount: 1, public: true }
|
|
@@ -8417,6 +8522,17 @@ function storageMethodName(node, methods) {
|
|
|
8417
8522
|
if (name === "put" && !isStorageLikeReceiver(callee.object)) {
|
|
8418
8523
|
return null;
|
|
8419
8524
|
}
|
|
8525
|
+
if (name === "prepare") {
|
|
8526
|
+
const argument = node.arguments[0];
|
|
8527
|
+
const text = argument === void 0 ? null : sqlTextOf(argument);
|
|
8528
|
+
if (text !== null) {
|
|
8529
|
+
if (!/^\s*(?:SELECT|WITH|INSERT|UPDATE|DELETE|REPLACE|CREATE|ALTER|DROP|PRAGMA|EXPLAIN)\b/iu.test(stripSqlNoise(text))) return null;
|
|
8530
|
+
} else {
|
|
8531
|
+
const receiver = callee.object;
|
|
8532
|
+
const receiverName = receiver.type === import_utils45.AST_NODE_TYPES.Identifier ? receiver.name : receiver.type === import_utils45.AST_NODE_TYPES.MemberExpression && !receiver.computed && receiver.property.type === import_utils45.AST_NODE_TYPES.Identifier ? receiver.property.name : "";
|
|
8533
|
+
if (!/^(?:db|database|connection)$/iu.test(receiverName)) return null;
|
|
8534
|
+
}
|
|
8535
|
+
}
|
|
8420
8536
|
return name;
|
|
8421
8537
|
}
|
|
8422
8538
|
function isStorageLikeReceiver(node) {
|
|
@@ -8501,12 +8617,13 @@ var no_storage_in_stateless_modules_default = createRule({
|
|
|
8501
8617
|
// src/rules/no-string-concat-in-loop.ts
|
|
8502
8618
|
var import_utils46 = require("@typescript-eslint/utils");
|
|
8503
8619
|
var NO_STRING_CONCAT_IN_LOOP_DOCUMENTATION = {
|
|
8504
|
-
summary: "
|
|
8620
|
+
summary: "Prefer collecting string fragments over repeatedly accumulating a growing string inside a loop.",
|
|
8505
8621
|
rationale: "Repeatedly rebuilding a growing string can copy all prior content on each iteration, making total work grow quadratically.",
|
|
8506
|
-
remediation: "
|
|
8622
|
+
remediation: "Consider collecting fragments and joining once; preserve intermediate observations and coercion timing, and measure hot paths.",
|
|
8507
8623
|
category: "performance",
|
|
8508
8624
|
limitations: [
|
|
8509
|
-
"Only local identifiers initialized with a string or template literal and accumulated in a loop body are inspected."
|
|
8625
|
+
"Only local identifiers initialized with a string or template literal and accumulated in a loop body are inspected.",
|
|
8626
|
+
"Deferred function bodies are excluded except recognized direct forEach callbacks. Syntax does not establish engine-specific string allocation complexity."
|
|
8510
8627
|
],
|
|
8511
8628
|
examples: [
|
|
8512
8629
|
{
|
|
@@ -8624,6 +8741,7 @@ function enclosingLoop(node) {
|
|
|
8624
8741
|
if ((parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") && parent.parent.type === "CallExpression" && parent.parent.arguments[0] === parent && parent.parent.callee.type === "MemberExpression" && !parent.parent.callee.computed && parent.parent.callee.property.type === "Identifier" && parent.parent.callee.property.name === "forEach") {
|
|
8625
8742
|
return parent.parent;
|
|
8626
8743
|
}
|
|
8744
|
+
if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression" || parent.type === "FunctionDeclaration") return null;
|
|
8627
8745
|
if (LOOP_NODE_TYPES.has(parent.type)) {
|
|
8628
8746
|
const loop = parent;
|
|
8629
8747
|
if (loop.body === child) {
|
|
@@ -8635,6 +8753,19 @@ function enclosingLoop(node) {
|
|
|
8635
8753
|
}
|
|
8636
8754
|
return null;
|
|
8637
8755
|
}
|
|
8756
|
+
function immediatelyExitsLoop(node, loop) {
|
|
8757
|
+
if (!LOOP_NODE_TYPES.has(loop.type) || node.parent.type !== "ExpressionStatement") return false;
|
|
8758
|
+
const statement = node.parent;
|
|
8759
|
+
const block = statement.parent;
|
|
8760
|
+
if (block.type !== "BlockStatement") return false;
|
|
8761
|
+
const next = block.body[block.body.indexOf(statement) + 1];
|
|
8762
|
+
if (next?.type !== "BreakStatement" && next?.type !== "ReturnStatement" && next?.type !== "ThrowStatement") return false;
|
|
8763
|
+
if (next.type === "BreakStatement" && next.label !== null) return false;
|
|
8764
|
+
for (let current = block; current !== void 0 && current !== loop; current = current.parent) {
|
|
8765
|
+
if (current.type === "TryStatement" || next.type === "BreakStatement" && current.type === "SwitchStatement") return false;
|
|
8766
|
+
}
|
|
8767
|
+
return true;
|
|
8768
|
+
}
|
|
8638
8769
|
function isSmallStaticForLoop(node) {
|
|
8639
8770
|
if (node.type !== "ForStatement" || node.init?.type !== "VariableDeclaration" || node.init.declarations.length !== 1 || node.test?.type !== "BinaryExpression" || node.test.operator !== "<" && node.test.operator !== "<=" || node.update?.type !== "UpdateExpression" || node.update.operator !== "++") {
|
|
8640
8771
|
return false;
|
|
@@ -8673,12 +8804,12 @@ var no_string_concat_in_loop_default = createRule({
|
|
|
8673
8804
|
meta: {
|
|
8674
8805
|
type: "suggestion",
|
|
8675
8806
|
docs: {
|
|
8676
|
-
description:
|
|
8807
|
+
description: NO_STRING_CONCAT_IN_LOOP_DOCUMENTATION.summary
|
|
8677
8808
|
},
|
|
8678
8809
|
schema: [],
|
|
8679
8810
|
messages: {
|
|
8680
|
-
noStringConcatInLoop:
|
|
8681
|
-
noStringReduce: "
|
|
8811
|
+
noStringConcatInLoop: "This loop repeatedly accumulates a growing string. Consider collecting fragments and joining once; preserve coercion timing and intermediate reads, and measure performance-sensitive paths.",
|
|
8812
|
+
noStringReduce: "This reduce repeatedly accumulates a growing string. Consider mapping fragments and joining once if coercion timing and intermediate observations are unchanged."
|
|
8682
8813
|
}
|
|
8683
8814
|
},
|
|
8684
8815
|
defaultOptions: [],
|
|
@@ -8705,6 +8836,7 @@ var no_string_concat_in_loop_default = createRule({
|
|
|
8705
8836
|
if (loop === null) {
|
|
8706
8837
|
return;
|
|
8707
8838
|
}
|
|
8839
|
+
if (immediatelyExitsLoop(node, loop)) return;
|
|
8708
8840
|
if (isSmallStaticForLoop(loop)) {
|
|
8709
8841
|
return;
|
|
8710
8842
|
}
|
|
@@ -8740,12 +8872,12 @@ var no_string_concat_in_loop_default = createRule({
|
|
|
8740
8872
|
// src/rules/no-tautological-expect.ts
|
|
8741
8873
|
var import_utils47 = require("@typescript-eslint/utils");
|
|
8742
8874
|
var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
|
|
8743
|
-
summary: "Disallow
|
|
8875
|
+
summary: "Disallow supported literal-only assertions that are statically known to pass.",
|
|
8744
8876
|
rationale: "An assertion determined entirely by literals does not observe the code under test and can keep passing after that code is removed.",
|
|
8745
8877
|
remediation: "Assert on a value produced by the behavior under test, or remove the assertion.",
|
|
8746
8878
|
category: "testing",
|
|
8747
8879
|
limitations: [
|
|
8748
|
-
"Only direct supported `expect` matcher calls in recognized test files are inspected."
|
|
8880
|
+
"Only direct supported `expect` matcher calls in recognized test files are inspected. Local expect bindings, regular expressions, and unsupported coercions are excluded; failing constant assertions are not tautologies."
|
|
8749
8881
|
],
|
|
8750
8882
|
examples: [
|
|
8751
8883
|
{
|
|
@@ -8782,11 +8914,11 @@ var NUMERIC_SIGNS = /* @__PURE__ */ new Set(["-", "+"]);
|
|
|
8782
8914
|
function isLiteral(node) {
|
|
8783
8915
|
switch (node.type) {
|
|
8784
8916
|
case import_utils47.AST_NODE_TYPES.Literal:
|
|
8785
|
-
return
|
|
8917
|
+
return !("regex" in node);
|
|
8786
8918
|
case import_utils47.AST_NODE_TYPES.TemplateLiteral:
|
|
8787
8919
|
return node.expressions.length === 0;
|
|
8788
8920
|
case import_utils47.AST_NODE_TYPES.UnaryExpression:
|
|
8789
|
-
return NUMERIC_SIGNS.has(node.operator) &&
|
|
8921
|
+
return NUMERIC_SIGNS.has(node.operator) && node.argument.type === import_utils47.AST_NODE_TYPES.Literal && typeof node.argument.value === "number";
|
|
8790
8922
|
case import_utils47.AST_NODE_TYPES.ArrayExpression:
|
|
8791
8923
|
return node.elements.every((element) => element !== null && isLiteral(element));
|
|
8792
8924
|
case import_utils47.AST_NODE_TYPES.ObjectExpression:
|
|
@@ -8800,6 +8932,43 @@ function isLiteral(node) {
|
|
|
8800
8932
|
function isStructuralLiteral(node) {
|
|
8801
8933
|
return node.type === import_utils47.AST_NODE_TYPES.ArrayExpression || node.type === import_utils47.AST_NODE_TYPES.ObjectExpression;
|
|
8802
8934
|
}
|
|
8935
|
+
function passesZeroArgumentMatcher(node, matcher) {
|
|
8936
|
+
let value;
|
|
8937
|
+
switch (node.type) {
|
|
8938
|
+
case import_utils47.AST_NODE_TYPES.Literal:
|
|
8939
|
+
value = node.value;
|
|
8940
|
+
break;
|
|
8941
|
+
case import_utils47.AST_NODE_TYPES.TemplateLiteral:
|
|
8942
|
+
value = node.quasis[0]?.value.cooked;
|
|
8943
|
+
break;
|
|
8944
|
+
case import_utils47.AST_NODE_TYPES.UnaryExpression:
|
|
8945
|
+
if (node.argument.type !== import_utils47.AST_NODE_TYPES.Literal || typeof node.argument.value !== "number") return false;
|
|
8946
|
+
value = node.operator === "-" ? -node.argument.value : node.argument.value;
|
|
8947
|
+
break;
|
|
8948
|
+
case import_utils47.AST_NODE_TYPES.ArrayExpression:
|
|
8949
|
+
case import_utils47.AST_NODE_TYPES.ObjectExpression:
|
|
8950
|
+
value = {};
|
|
8951
|
+
break;
|
|
8952
|
+
default:
|
|
8953
|
+
return false;
|
|
8954
|
+
}
|
|
8955
|
+
switch (matcher) {
|
|
8956
|
+
case "toBeDefined":
|
|
8957
|
+
return value !== void 0;
|
|
8958
|
+
case "toBeUndefined":
|
|
8959
|
+
return value === void 0;
|
|
8960
|
+
case "toBeNull":
|
|
8961
|
+
return value === null;
|
|
8962
|
+
case "toBeTruthy":
|
|
8963
|
+
return Boolean(value);
|
|
8964
|
+
case "toBeFalsy":
|
|
8965
|
+
return !value;
|
|
8966
|
+
case "toBeNaN":
|
|
8967
|
+
return typeof value === "number" && Number.isNaN(value);
|
|
8968
|
+
default:
|
|
8969
|
+
return false;
|
|
8970
|
+
}
|
|
8971
|
+
}
|
|
8803
8972
|
function expectOperand(callee) {
|
|
8804
8973
|
const receiver = callee.object;
|
|
8805
8974
|
if (receiver.type !== import_utils47.AST_NODE_TYPES.CallExpression || receiver.callee.type !== import_utils47.AST_NODE_TYPES.Identifier || receiver.callee.name !== "expect" || receiver.arguments.length !== 1) {
|
|
@@ -8813,12 +8982,12 @@ var no_tautological_expect_default = createRule({
|
|
|
8813
8982
|
meta: {
|
|
8814
8983
|
type: "problem",
|
|
8815
8984
|
docs: {
|
|
8816
|
-
description: "Disallow
|
|
8985
|
+
description: "Disallow supported literal-only assertions that are statically known to pass."
|
|
8817
8986
|
},
|
|
8818
8987
|
schema: [],
|
|
8819
8988
|
messages: {
|
|
8820
|
-
tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` compares
|
|
8821
|
-
tautologicalMatcher: "`expect({{operand}}).{{matcher}}()`
|
|
8989
|
+
tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` compares an identical literal and does not observe behavior. Assert on a produced value or remove only the redundant assertion, preserving other coverage.",
|
|
8990
|
+
tautologicalMatcher: "`expect({{operand}}).{{matcher}}()` is statically known to pass. Assert on a produced value or remove only the redundant assertion, preserving other coverage."
|
|
8822
8991
|
}
|
|
8823
8992
|
},
|
|
8824
8993
|
defaultOptions: [],
|
|
@@ -8840,11 +9009,20 @@ var no_tautological_expect_default = createRule({
|
|
|
8840
9009
|
return;
|
|
8841
9010
|
}
|
|
8842
9011
|
const matcher = callee.property.name;
|
|
9012
|
+
if (callee.object.type !== import_utils47.AST_NODE_TYPES.CallExpression || callee.object.callee.type !== import_utils47.AST_NODE_TYPES.Identifier) return;
|
|
9013
|
+
const expectIdentifier = callee.object.callee;
|
|
9014
|
+
const variable = import_utils47.ASTUtils.findVariable(context.sourceCode.getScope(expectIdentifier), expectIdentifier.name);
|
|
9015
|
+
if (variable !== null && variable.defs.some((definition) => {
|
|
9016
|
+
if (definition.node.type !== import_utils47.AST_NODE_TYPES.ImportSpecifier) return true;
|
|
9017
|
+
const declaration = definition.node.parent;
|
|
9018
|
+
const imported = definition.node.imported;
|
|
9019
|
+
return declaration.type !== import_utils47.AST_NODE_TYPES.ImportDeclaration || !["vitest", "@jest/globals", "@playwright/test", "bun:test"].includes(String(declaration.source.value)) || (imported.type === import_utils47.AST_NODE_TYPES.Identifier ? imported.name : imported.value) !== "expect";
|
|
9020
|
+
})) return;
|
|
8843
9021
|
const operand = expectOperand(callee);
|
|
8844
9022
|
if (operand === null || !isLiteral(operand)) {
|
|
8845
9023
|
return;
|
|
8846
9024
|
}
|
|
8847
|
-
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0) {
|
|
9025
|
+
if (ZERO_ARG_MATCHERS.has(matcher) && node.arguments.length === 0 && passesZeroArgumentMatcher(operand, matcher)) {
|
|
8848
9026
|
context.report({
|
|
8849
9027
|
node,
|
|
8850
9028
|
messageId: "tautologicalMatcher",
|
|
@@ -10096,15 +10274,16 @@ var MOCK_MODULES = /* @__PURE__ */ new Set([
|
|
|
10096
10274
|
var NO_UNSAFE_MOCK_CASTING_DOCUMENTATION = {
|
|
10097
10275
|
summary: "Disallow casting to mock types like `jest.Mock` or `vi.Mock`. Use `vi.mocked()` or `jest.mocked()` instead.",
|
|
10098
10276
|
rationale: "A type assertion can claim an unmocked value is a mock and bypass checking between the original callable and the mock API.",
|
|
10099
|
-
remediation: "
|
|
10277
|
+
remediation: "Create the mock or spy first, then use the framework's mocked helper to preserve the original value's type. The helper does not create or verify a runtime mock.",
|
|
10100
10278
|
category: "testing",
|
|
10101
|
-
limitations: ["Only mock types imported from Vitest or Jest modules are inspected."],
|
|
10279
|
+
limitations: ["Only mock types imported from Vitest or Jest modules are inspected. mocked is a type helper, not runtime validation or a replacement for mock setup."],
|
|
10280
|
+
references: ["https://vitest.dev/api/vi.html#vi-mocked"],
|
|
10102
10281
|
examples: [
|
|
10103
10282
|
{
|
|
10104
10283
|
id: "typed-mock-helper",
|
|
10105
10284
|
title: "Use the framework helper",
|
|
10106
10285
|
outcome: "no-match",
|
|
10107
|
-
files: [{ path: "src/client.test.ts", source: "const m = vi.mocked(
|
|
10286
|
+
files: [{ path: "src/client.test.ts", source: "import { vi } from 'vitest'; const client = { read: () => 'value' }; vi.spyOn(client, 'read'); const m = vi.mocked(client.read);" }],
|
|
10108
10287
|
focusPath: "src/client.test.ts",
|
|
10109
10288
|
expectedCount: 0,
|
|
10110
10289
|
public: true
|
|
@@ -10130,7 +10309,7 @@ var no_unsafe_mock_casting_default = createRule({
|
|
|
10130
10309
|
},
|
|
10131
10310
|
schema: [],
|
|
10132
10311
|
messages: {
|
|
10133
|
-
unsafeMockCast: "
|
|
10312
|
+
unsafeMockCast: "Avoid a broad Mock cast. After creating the mock or spy, use `vi.mocked(fn)` or `jest.mocked(fn)` to retain its original type; the helper does not create a runtime mock."
|
|
10134
10313
|
}
|
|
10135
10314
|
},
|
|
10136
10315
|
defaultOptions: [],
|
|
@@ -10192,7 +10371,7 @@ var import_utils55 = require("@typescript-eslint/utils");
|
|
|
10192
10371
|
var ts2 = __toESM(require("typescript"), 1);
|
|
10193
10372
|
var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
|
|
10194
10373
|
summary: 'Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum(["a", "b"])` with a string-literal union instead.',
|
|
10195
|
-
rationale: "
|
|
10374
|
+
rationale: "The project prefers literal-first schema definitions. nativeEnum also accepts plain enum-like objects, so this is an explicit declaration policy, not proof that every call duplicates a TypeScript enum's runtime object.",
|
|
10196
10375
|
remediation: "Pass string literals directly to `z.enum` and derive the TypeScript type with `z.infer`.",
|
|
10197
10376
|
category: "maintainability",
|
|
10198
10377
|
autofix: "none",
|
|
@@ -10370,7 +10549,7 @@ var TEST_LOOPS_OVER_LITERAL_CASES_DOCUMENTATION = {
|
|
|
10370
10549
|
remediation: "Create one named parameterized test or runner-aware subtest for each literal case.",
|
|
10371
10550
|
category: "testing",
|
|
10372
10551
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**"],
|
|
10373
|
-
limitations: ["Only inline literal for-of cases containing framework assertions are reported."],
|
|
10552
|
+
limitations: ["Only inline literal for-of cases containing framework assertions are reported. References to setup or parameters owned by the enclosing test, and loops followed by statements in the same or enclosing block, are excluded because they can belong to an ordered scenario. External helper purity is not inferred."],
|
|
10374
10553
|
examples: [
|
|
10375
10554
|
{ id: "parameterized-cases", title: "Use a parameterized test", outcome: "no-match", files: [{ path: "src/parser.test.ts", source: "test.each(['a', 'b'])('parses %s', (value) => { expect(parse(value)).toBe(value); });" }], focusPath: "src/parser.test.ts", expectedCount: 0, public: true },
|
|
10376
10555
|
{ id: "looped-cases", title: "Do not hide cases in a loop", outcome: "match", files: [{ path: "src/parser.test.ts", source: "test('parses', () => { for (const value of ['a', 'b']) { expect(parse(value)).toBe(value); } });" }], focusPath: "src/parser.test.ts", expectedCount: 1, public: true }
|
|
@@ -10517,7 +10696,7 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10517
10696
|
},
|
|
10518
10697
|
schema: [],
|
|
10519
10698
|
messages: {
|
|
10520
|
-
literalCaseLoop: "This loop asserts over {{count}} inline cases
|
|
10699
|
+
literalCaseLoop: "This loop asserts over {{count}} inline cases in one test; a thrown assertion may prevent later cases from running. Create one named test or subtest per independent case; use `test.each(...)` or `it.each(...)` where supported."
|
|
10521
10700
|
}
|
|
10522
10701
|
},
|
|
10523
10702
|
defaultOptions: [],
|
|
@@ -10542,6 +10721,9 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10542
10721
|
if (enclosing === null || !isTestBody2(enclosing, isFrameworkTest)) {
|
|
10543
10722
|
return;
|
|
10544
10723
|
}
|
|
10724
|
+
for (let current = node; current !== void 0 && current !== enclosing; current = current.parent) {
|
|
10725
|
+
if (current.parent?.type === import_utils56.AST_NODE_TYPES.BlockStatement && current.parent.body.at(-1) !== current) return;
|
|
10726
|
+
}
|
|
10545
10727
|
const cases = unwrapExpression(node.right);
|
|
10546
10728
|
const callbackParameters = new Set(
|
|
10547
10729
|
enclosing.params.flatMap((parameter) => parameter.type === import_utils56.AST_NODE_TYPES.Identifier ? [parameter.name] : [])
|
|
@@ -10551,6 +10733,16 @@ var test_loops_over_literal_cases_default = createRule({
|
|
|
10551
10733
|
) || !walkOwnScope2(node.body, (current) => isAssertion2(current, isFrameworkAssertion)) || walkOwnScope2(node.body, (current) => opensSubtest(current, callbackParameters)) || walkOwnScope2(node.body, (current) => LOOP_CARRIED_CONTROL.has(current.type))) {
|
|
10552
10734
|
return;
|
|
10553
10735
|
}
|
|
10736
|
+
const capturesSetup = walkOwnScope2(node.body, (current) => {
|
|
10737
|
+
if (current.type !== import_utils56.AST_NODE_TYPES.Identifier) return false;
|
|
10738
|
+
const variable = import_utils56.ASTUtils.findVariable(context.sourceCode.getScope(current), current.name);
|
|
10739
|
+
if (variable === null || !variable.references.some((reference) => reference.identifier === current)) return false;
|
|
10740
|
+
return variable.defs.some((definition) => {
|
|
10741
|
+
const declaration = definition.name;
|
|
10742
|
+
return declaration.range[0] >= enclosing.range[0] && declaration.range[1] <= enclosing.range[1] && (declaration.range[0] < node.range[0] || declaration.range[1] > node.range[1]);
|
|
10743
|
+
});
|
|
10744
|
+
});
|
|
10745
|
+
if (capturesSetup) return;
|
|
10554
10746
|
context.report({
|
|
10555
10747
|
node,
|
|
10556
10748
|
messageId: "literalCaseLoop",
|
|
@@ -10771,13 +10963,12 @@ var import_utils59 = require("@typescript-eslint/utils");
|
|
|
10771
10963
|
var PREFER_ECMASCRIPT_PRIVATE_MEMBERS_DOCUMENTATION = {
|
|
10772
10964
|
summary: "Prefer ECMAScript `#private` class members over TypeScript-only `private` members.",
|
|
10773
10965
|
rationale: "ECMAScript private names enforce encapsulation at runtime instead of erasing the boundary during compilation.",
|
|
10774
|
-
remediation: "
|
|
10966
|
+
remediation: "Review reflection, instance escape and framework contracts before replacing TypeScript privacy with ECMAScript private names and updating references.",
|
|
10775
10967
|
category: "maintainability",
|
|
10776
|
-
autofix: "
|
|
10968
|
+
autofix: "none",
|
|
10777
10969
|
limitations: [
|
|
10778
10970
|
"Ambient, abstract, computed, decorated, override, parameter-property, and generated declarations are excluded.",
|
|
10779
|
-
"
|
|
10780
|
-
"Overloads, modifier-adjacent comments, reflection, and any potentially cross-file or escaping class remain report-only."
|
|
10971
|
+
"Migration is report-only: type information cannot prove that instances or constructors never escape through this, or that reflection and framework serialization do not observe ordinary private properties."
|
|
10781
10972
|
],
|
|
10782
10973
|
references: ["https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements"],
|
|
10783
10974
|
examples: [
|
|
@@ -10797,19 +10988,11 @@ var PREFER_ECMASCRIPT_PRIVATE_MEMBERS_DOCUMENTATION = {
|
|
|
10797
10988
|
files: [{ path: "src/vault.ts", source: "class Vault { private read() { return 1; } open() { return this.read(); } }" }],
|
|
10798
10989
|
focusPath: "src/vault.ts",
|
|
10799
10990
|
expectedCount: 1,
|
|
10800
|
-
public: true
|
|
10801
|
-
fixedFiles: [{ path: "src/vault.ts", source: "class Vault { #read() { return 1; } open() { return this.#read(); } }" }]
|
|
10991
|
+
public: true
|
|
10802
10992
|
}
|
|
10803
10993
|
]
|
|
10804
10994
|
};
|
|
10805
|
-
function reportClass2(context,
|
|
10806
|
-
const parent = owner.parent;
|
|
10807
|
-
const directlyExported = parent.type === import_utils59.AST_NODE_TYPES.ExportNamedDeclaration || parent.type === import_utils59.AST_NODE_TYPES.ExportDefaultDeclaration;
|
|
10808
|
-
const locallyClosed = !directlyExported && owner.decorators.length === 0 && owner.type === import_utils59.AST_NODE_TYPES.ClassDeclaration && context.sourceCode.getDeclaredVariables(owner).every(
|
|
10809
|
-
(variable) => variable.references.every(
|
|
10810
|
-
(reference) => reference.identifier.range[0] >= owner.range[0] && reference.identifier.range[1] <= owner.range[1]
|
|
10811
|
-
)
|
|
10812
|
-
);
|
|
10995
|
+
function reportClass2(context, owner) {
|
|
10813
10996
|
const groups = /* @__PURE__ */ new Map();
|
|
10814
10997
|
for (const member of owner.body.body) {
|
|
10815
10998
|
if (!isConvertible(member)) continue;
|
|
@@ -10822,18 +11005,10 @@ function reportClass2(context, services, owner) {
|
|
|
10822
11005
|
for (const [name, members] of groups) {
|
|
10823
11006
|
const first = members[0];
|
|
10824
11007
|
if (first === void 0) continue;
|
|
10825
|
-
const fix = locallyClosed ? privateMemberFixes(
|
|
10826
|
-
context,
|
|
10827
|
-
services,
|
|
10828
|
-
owner,
|
|
10829
|
-
members,
|
|
10830
|
-
true
|
|
10831
|
-
) : void 0;
|
|
10832
11008
|
context.report({
|
|
10833
11009
|
node: first.key,
|
|
10834
11010
|
messageId: "preferEcmascriptPrivate",
|
|
10835
|
-
data: { name }
|
|
10836
|
-
...fix === void 0 ? {} : { fix }
|
|
11011
|
+
data: { name }
|
|
10837
11012
|
});
|
|
10838
11013
|
}
|
|
10839
11014
|
}
|
|
@@ -10847,7 +11022,6 @@ var prefer_ecmascript_private_members_default = createRule({
|
|
|
10847
11022
|
meta: {
|
|
10848
11023
|
type: "suggestion",
|
|
10849
11024
|
docs: { description: "Prefer ECMAScript `#private` class members over TypeScript-only `private` members." },
|
|
10850
|
-
fixable: "code",
|
|
10851
11025
|
schema: [],
|
|
10852
11026
|
messages: {
|
|
10853
11027
|
preferEcmascriptPrivate: "TypeScript `private {{name}}` is erased at runtime; use the ECMAScript private name `#{{name}}`."
|
|
@@ -10864,8 +11038,8 @@ var prefer_ecmascript_private_members_default = createRule({
|
|
|
10864
11038
|
}
|
|
10865
11039
|
if (services === null) return {};
|
|
10866
11040
|
return {
|
|
10867
|
-
ClassDeclaration: (node) => reportClass2(context,
|
|
10868
|
-
ClassExpression: (node) => reportClass2(context,
|
|
11041
|
+
ClassDeclaration: (node) => reportClass2(context, node),
|
|
11042
|
+
ClassExpression: (node) => reportClass2(context, node)
|
|
10869
11043
|
};
|
|
10870
11044
|
}
|
|
10871
11045
|
});
|
|
@@ -10875,10 +11049,10 @@ var import_utils60 = require("@typescript-eslint/utils");
|
|
|
10875
11049
|
var import_utils61 = require("@typescript-eslint/utils");
|
|
10876
11050
|
var PREFER_DISCRIMINATED_UNION_DOCUMENTATION = {
|
|
10877
11051
|
summary: "Flag flat result objects with a required positive boolean status and optional success/failure payloads.",
|
|
10878
|
-
rationale: "
|
|
10879
|
-
remediation: "
|
|
11052
|
+
rationale: "When success and failure are mutually exclusive outcomes, a boolean plus optional branch data permits contradictory and incomplete states.",
|
|
11053
|
+
remediation: "If the outcomes are mutually exclusive, represent each branch as a discriminated union member with its required payload.",
|
|
10880
11054
|
category: "correctness",
|
|
10881
|
-
limitations: ["Only local object shapes with recognized
|
|
11055
|
+
limitations: ["Only local object shapes with recognized non-computed status and payload names are inspected. Names do not prove that partial-success outcomes are forbidden; review the domain before changing its representation."],
|
|
10882
11056
|
examples: [
|
|
10883
11057
|
{ id: "explicit-result-branches", title: "Use explicit result branches", outcome: "no-match", files: [{ path: "src/result.ts", source: "type Result = { ok: true; data: string } | { ok: false; error: string };" }], focusPath: "src/result.ts", expectedCount: 0, public: true },
|
|
10884
11058
|
{ id: "optional-result-payloads", title: "Do not make both result payloads optional", outcome: "match", files: [{ path: "src/result.ts", source: "type Result = { ok: boolean; data?: string; error?: string };" }], focusPath: "src/result.ts", expectedCount: 1, public: true }
|
|
@@ -10942,7 +11116,7 @@ function looksLikeMutuallyExclusiveState(typeLiteral) {
|
|
|
10942
11116
|
return statusMemberCount === REQUIRED_STATUS_MEMBER_COUNT && hasFailurePayload && (hasSuccessPayload || !hasUnrecognizedMember);
|
|
10943
11117
|
}
|
|
10944
11118
|
function getMemberName(member) {
|
|
10945
|
-
if (member.type !== import_utils61.AST_NODE_TYPES.TSPropertySignature) {
|
|
11119
|
+
if (member.type !== import_utils61.AST_NODE_TYPES.TSPropertySignature || member.computed) {
|
|
10946
11120
|
return null;
|
|
10947
11121
|
}
|
|
10948
11122
|
const { key } = member;
|
|
@@ -10978,7 +11152,7 @@ var prefer_discriminated_union_default = createRule({
|
|
|
10978
11152
|
},
|
|
10979
11153
|
schema: [],
|
|
10980
11154
|
messages: {
|
|
10981
|
-
preferDiscriminatedUnion: "This object type
|
|
11155
|
+
preferDiscriminatedUnion: "This object type combines a boolean status with optional payloads. If success and failure are mutually exclusive, consider a discriminated union such as `{ ok: true; data: T } | { ok: false; error: E }`."
|
|
10982
11156
|
}
|
|
10983
11157
|
},
|
|
10984
11158
|
defaultOptions: [],
|
|
@@ -11189,7 +11363,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
|
11189
11363
|
category: "correctness",
|
|
11190
11364
|
autofix: "none",
|
|
11191
11365
|
limitations: [
|
|
11192
|
-
"Only direct identifier keys in application-owned z.object/z.strictObject schemas are checked.",
|
|
11366
|
+
"Only direct identifier keys with recognizable numeric Zod leaves in application-owned z.object/z.strictObject schemas are checked; aliases and transformations are not inferred.",
|
|
11193
11367
|
"The rule covers control timings such as timeout, delay, interval, backoff, TTL, lease, heartbeat, debounce, and throttle; observed durations and business-domain periods are excluded.",
|
|
11194
11368
|
"Quoted/computed protocol keys, generated/vendor code, tests, fixtures, and non-Zod schemas are excluded."
|
|
11195
11369
|
],
|
|
@@ -11201,7 +11375,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
|
11201
11375
|
files: [
|
|
11202
11376
|
{
|
|
11203
11377
|
path: "src/request.ts",
|
|
11204
|
-
source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeoutMs: z.number().int().min(
|
|
11378
|
+
source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeoutMs: z.number().int().min(1000).max(300000).default(30000) });"
|
|
11205
11379
|
}
|
|
11206
11380
|
],
|
|
11207
11381
|
focusPath: "src/request.ts",
|
|
@@ -11215,7 +11389,7 @@ var PREFER_MILLISECOND_CONTROL_DURATION_SCHEMA_DOCUMENTATION = {
|
|
|
11215
11389
|
files: [
|
|
11216
11390
|
{
|
|
11217
11391
|
path: "src/request.ts",
|
|
11218
|
-
source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeout_seconds: z.number().int().min(1) });"
|
|
11392
|
+
source: "import { z } from 'zod';\nexport const RequestSchema = z.object({ timeout_seconds: z.number().int().min(1).max(300).default(30) });"
|
|
11219
11393
|
}
|
|
11220
11394
|
],
|
|
11221
11395
|
focusPath: "src/request.ts",
|
|
@@ -11247,6 +11421,7 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11247
11421
|
}
|
|
11248
11422
|
const zodNamespaces = /* @__PURE__ */ new Set();
|
|
11249
11423
|
const objectFactories = /* @__PURE__ */ new Set();
|
|
11424
|
+
const numberFactories = /* @__PURE__ */ new Set();
|
|
11250
11425
|
function binding(identifier) {
|
|
11251
11426
|
return import_utils63.ASTUtils.findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
11252
11427
|
}
|
|
@@ -11266,10 +11441,27 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11266
11441
|
const variable = binding(callee.object);
|
|
11267
11442
|
return variable !== null && zodNamespaces.has(variable);
|
|
11268
11443
|
}
|
|
11444
|
+
function isNumericSchema(node) {
|
|
11445
|
+
if (node.type !== import_utils63.AST_NODE_TYPES.CallExpression) return false;
|
|
11446
|
+
const callee = node.callee;
|
|
11447
|
+
if (callee.type === import_utils63.AST_NODE_TYPES.Identifier) {
|
|
11448
|
+
const variable = binding(callee);
|
|
11449
|
+
return variable !== null && numberFactories.has(variable);
|
|
11450
|
+
}
|
|
11451
|
+
if (callee.type !== import_utils63.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils63.AST_NODE_TYPES.Identifier) return false;
|
|
11452
|
+
if (callee.object.type === import_utils63.AST_NODE_TYPES.Identifier) {
|
|
11453
|
+
const variable = binding(callee.object);
|
|
11454
|
+
return callee.property.name === "number" && variable !== null && zodNamespaces.has(variable);
|
|
11455
|
+
}
|
|
11456
|
+
return ["int", "min", "max", "positive", "nonnegative", "finite", "multipleOf", "optional", "nullable", "nullish", "default", "describe", "brand", "readonly"].includes(callee.property.name) && isNumericSchema(callee.object);
|
|
11457
|
+
}
|
|
11269
11458
|
return {
|
|
11270
11459
|
ImportDeclaration(node) {
|
|
11271
11460
|
if (!isZodModule(node.source.value)) return;
|
|
11272
11461
|
for (const specifier of node.specifiers) {
|
|
11462
|
+
if (specifier.type === import_utils63.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils63.AST_NODE_TYPES.Identifier && specifier.imported.name === "number") {
|
|
11463
|
+
record(numberFactories, specifier.local);
|
|
11464
|
+
}
|
|
11273
11465
|
if (specifier.type === import_utils63.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils63.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils63.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils63.AST_NODE_TYPES.Identifier && specifier.imported.name === "z") {
|
|
11274
11466
|
record(zodNamespaces, specifier.local);
|
|
11275
11467
|
} else if (specifier.type === import_utils63.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils63.AST_NODE_TYPES.Identifier && (specifier.imported.name === "object" || specifier.imported.name === "strictObject")) {
|
|
@@ -11282,7 +11474,7 @@ var prefer_millisecond_control_duration_schema_default = createRule({
|
|
|
11282
11474
|
const shape = node.arguments[0];
|
|
11283
11475
|
if (shape?.type !== import_utils63.AST_NODE_TYPES.ObjectExpression) return;
|
|
11284
11476
|
for (const member of shape.properties) {
|
|
11285
|
-
if (member.type !== import_utils63.AST_NODE_TYPES.Property) continue;
|
|
11477
|
+
if (member.type !== import_utils63.AST_NODE_TYPES.Property || !isNumericSchema(member.value)) continue;
|
|
11286
11478
|
const key = directIdentifierKey(member);
|
|
11287
11479
|
if (key === null || !CONTROL_SECONDS_RE.test(key.name) && !CONTROL_SECONDS_CAMEL_RE.test(key.name)) {
|
|
11288
11480
|
continue;
|
|
@@ -11302,7 +11494,7 @@ var PREFER_IMMUTABLE_MODULE_CONSTANT_DOCUMENTATION = {
|
|
|
11302
11494
|
remediation: "Expose literals with `as const` or a readonly type, and expose Set or Map values through ReadonlySet or ReadonlyMap.",
|
|
11303
11495
|
category: "correctness",
|
|
11304
11496
|
limitations: [
|
|
11305
|
-
"
|
|
11497
|
+
"Generated, test and JavaScript files are skipped. Private constants with observed direct or alias mutation are excluded; exported mutable collections remain advisory candidates. Reassigned aliases are conservatively followed, not flow-proven."
|
|
11306
11498
|
],
|
|
11307
11499
|
examples: [
|
|
11308
11500
|
{
|
|
@@ -11456,7 +11648,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11456
11648
|
}
|
|
11457
11649
|
const exportedNames2 = /* @__PURE__ */ new Set();
|
|
11458
11650
|
const typeAliases2 = /* @__PURE__ */ new Map();
|
|
11459
|
-
const
|
|
11651
|
+
const mutatesThroughAlias = (root) => {
|
|
11460
11652
|
const pending = [root];
|
|
11461
11653
|
const seen = /* @__PURE__ */ new Set();
|
|
11462
11654
|
while (pending.length > 0) {
|
|
@@ -11468,7 +11660,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11468
11660
|
if (identifier.type !== import_utils64.AST_NODE_TYPES.Identifier) continue;
|
|
11469
11661
|
if (referenceMutates(identifier, isUnshadowedGlobal3)) return true;
|
|
11470
11662
|
const declarator = identifier.parent;
|
|
11471
|
-
if (declarator.type !== import_utils64.AST_NODE_TYPES.VariableDeclarator || declarator.init !== identifier || declarator.id.type !== import_utils64.AST_NODE_TYPES.Identifier || declarator.parent.type !== import_utils64.AST_NODE_TYPES.VariableDeclaration
|
|
11663
|
+
if (declarator.type !== import_utils64.AST_NODE_TYPES.VariableDeclarator || declarator.init !== identifier || declarator.id.type !== import_utils64.AST_NODE_TYPES.Identifier || declarator.parent.type !== import_utils64.AST_NODE_TYPES.VariableDeclaration) {
|
|
11472
11664
|
continue;
|
|
11473
11665
|
}
|
|
11474
11666
|
const alias = sourceCode.getDeclaredVariables(declarator)[0];
|
|
@@ -11517,7 +11709,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
11517
11709
|
return;
|
|
11518
11710
|
}
|
|
11519
11711
|
const variable = sourceCode.getDeclaredVariables(node)[0];
|
|
11520
|
-
if (!directlyExported && !exportedNames2.has(node.id.name) && variable !== void 0 &&
|
|
11712
|
+
if (!directlyExported && !exportedNames2.has(node.id.name) && variable !== void 0 && mutatesThroughAlias(variable)) {
|
|
11521
11713
|
return;
|
|
11522
11714
|
}
|
|
11523
11715
|
context.report({
|
|
@@ -11944,7 +12136,7 @@ var PREFER_MODULE_LEVEL_CONSTANT_DOCUMENTATION = {
|
|
|
11944
12136
|
rationale: "Recreating immutable lookup data on every call wastes allocations and obscures its constant nature.",
|
|
11945
12137
|
remediation: "Declare immutable literal collections and non-stateful regular expressions once at module scope.",
|
|
11946
12138
|
category: "performance",
|
|
11947
|
-
limitations: ["
|
|
12139
|
+
limitations: ["Small collections, observed direct or nested mutations, nested aliases, direct escapes, and dependencies on local values are excluded. Directly invoked function expressions are excluded rather than assuming they run repeatedly. Callback effects and indirect escapes are not analyzed interprocedurally; review those before hoisting."],
|
|
11948
12140
|
examples: [
|
|
11949
12141
|
{ id: "hoisted-collection", title: "Hoist a constant collection", outcome: "no-match", files: [{ path: "src/keys.ts", source: "const KEYS = ['a', 'b', 'c'] as const; function isAllowed(key: string) { return KEYS.includes(key); }" }], focusPath: "src/keys.ts", expectedCount: 0, public: true },
|
|
11950
12142
|
{ id: "local-collection", title: "Do not recreate a constant collection", outcome: "match", files: [{ path: "src/keys.ts", source: "function isAllowed(key: string) { const KEYS = ['a', 'b', 'c']; return KEYS.includes(key); }" }], focusPath: "src/keys.ts", expectedCount: 1, public: true }
|
|
@@ -12099,12 +12291,16 @@ var NON_RETAINING_BUILTINS = /* @__PURE__ */ new Map(
|
|
|
12099
12291
|
]
|
|
12100
12292
|
);
|
|
12101
12293
|
function isSafeRead(identifier) {
|
|
12102
|
-
|
|
12294
|
+
let parent = identifier.parent;
|
|
12103
12295
|
if (parent.type === import_utils66.AST_NODE_TYPES.MemberExpression) {
|
|
12104
12296
|
if (parent.object !== identifier) {
|
|
12105
12297
|
return true;
|
|
12106
12298
|
}
|
|
12299
|
+
while (parent.parent.type === import_utils66.AST_NODE_TYPES.MemberExpression && parent.parent.object === parent) {
|
|
12300
|
+
parent = parent.parent;
|
|
12301
|
+
}
|
|
12107
12302
|
const grandparent = parent.parent;
|
|
12303
|
+
if (grandparent.type === import_utils66.AST_NODE_TYPES.VariableDeclarator || grandparent.type === import_utils66.AST_NODE_TYPES.SpreadElement) return false;
|
|
12108
12304
|
if (grandparent.type === import_utils66.AST_NODE_TYPES.AssignmentExpression && grandparent.left === parent) {
|
|
12109
12305
|
return false;
|
|
12110
12306
|
}
|
|
@@ -12114,7 +12310,7 @@ function isSafeRead(identifier) {
|
|
|
12114
12310
|
if (grandparent.type === import_utils66.AST_NODE_TYPES.UnaryExpression && grandparent.operator === "delete") {
|
|
12115
12311
|
return false;
|
|
12116
12312
|
}
|
|
12117
|
-
if (
|
|
12313
|
+
if (grandparent.type === import_utils66.AST_NODE_TYPES.CallExpression && grandparent.callee === parent && (parent.computed ? parent.property.type !== import_utils66.AST_NODE_TYPES.Literal || typeof parent.property.value !== "string" || MUTATING_METHODS2.has(parent.property.value) : parent.property.type === import_utils66.AST_NODE_TYPES.Identifier && MUTATING_METHODS2.has(parent.property.name))) {
|
|
12118
12314
|
return false;
|
|
12119
12315
|
}
|
|
12120
12316
|
return true;
|
|
@@ -12222,9 +12418,15 @@ var prefer_module_level_constant_default = createRule({
|
|
|
12222
12418
|
if (node.id.type !== import_utils66.AST_NODE_TYPES.Identifier || node.init === null) {
|
|
12223
12419
|
return;
|
|
12224
12420
|
}
|
|
12225
|
-
|
|
12421
|
+
const owner = enclosingFunction3(node);
|
|
12422
|
+
if (owner === null) {
|
|
12226
12423
|
return;
|
|
12227
12424
|
}
|
|
12425
|
+
let expression = owner;
|
|
12426
|
+
while (expression.parent !== void 0 && unwrap4(expression.parent) === expression) {
|
|
12427
|
+
expression = expression.parent;
|
|
12428
|
+
}
|
|
12429
|
+
if (expression.parent?.type === import_utils66.AST_NODE_TYPES.CallExpression && expression.parent.callee === expression) return;
|
|
12228
12430
|
const candidate2 = classify(node.init, checkRegex);
|
|
12229
12431
|
if (candidate2 === null) {
|
|
12230
12432
|
return;
|
|
@@ -12249,10 +12451,10 @@ var prefer_module_level_constant_default = createRule({
|
|
|
12249
12451
|
var import_utils67 = require("@typescript-eslint/utils");
|
|
12250
12452
|
var PREFER_MODULE_LEVEL_SCHEMA_DOCUMENTATION = {
|
|
12251
12453
|
summary: "Declare a Zod schema at module scope when it closes over nothing in the enclosing function",
|
|
12252
|
-
rationale: "A
|
|
12454
|
+
rationale: "A schema created inside a function is rebuilt on each call. Module scope can enable reuse across callers; local schemas already support local type inference.",
|
|
12253
12455
|
remediation: "Move the closed schema declaration to module scope and reference it from the function.",
|
|
12254
12456
|
category: "performance",
|
|
12255
|
-
limitations: ["Schemas that depend on local state or are wrapped in a recognized memoization helper are excluded."],
|
|
12457
|
+
limitations: ["Schemas that depend on local state or are wrapped in a recognized memoization helper are excluded.", "Eager calls outside the recognized Zod construction chain and new expressions are excluded. This is manual guidance, not a purity proof: review getters, callback effects, schema identity, error customization, and module initialization order before moving construction."],
|
|
12256
12458
|
examples: [
|
|
12257
12459
|
{ id: "module-schema", title: "Declare the schema once", outcome: "no-match", files: [{ path: "src/handler.ts", source: "import { z } from 'zod'; const ZBody = z.object({ id: z.string(), name: z.string() }); export function handle(raw: unknown) { return ZBody.parse(raw); }" }], focusPath: "src/handler.ts", expectedCount: 0, public: true },
|
|
12258
12460
|
{ id: "local-schema", title: "Do not rebuild a closed schema", outcome: "match", files: [{ path: "src/handler.ts", source: "import { z } from 'zod'; export function handle(raw: unknown) { const ZBody = z.object({ id: z.string(), name: z.string() }); return ZBody.parse(raw); }" }], focusPath: "src/handler.ts", expectedCount: 1, public: true }
|
|
@@ -12269,6 +12471,36 @@ var DEFAULT_FACTORIES = [
|
|
|
12269
12471
|
"union"
|
|
12270
12472
|
];
|
|
12271
12473
|
var DEFAULT_MIN_PROPERTIES = 2;
|
|
12474
|
+
var CONSTRUCTION_FACTORIES = /* @__PURE__ */ new Set([
|
|
12475
|
+
...DEFAULT_FACTORIES,
|
|
12476
|
+
"any",
|
|
12477
|
+
"array",
|
|
12478
|
+
"bigint",
|
|
12479
|
+
"boolean",
|
|
12480
|
+
"custom",
|
|
12481
|
+
"date",
|
|
12482
|
+
"enum",
|
|
12483
|
+
"instanceof",
|
|
12484
|
+
"lazy",
|
|
12485
|
+
"literal",
|
|
12486
|
+
"map",
|
|
12487
|
+
"nan",
|
|
12488
|
+
"nativeEnum",
|
|
12489
|
+
"never",
|
|
12490
|
+
"null",
|
|
12491
|
+
"nullable",
|
|
12492
|
+
"nullish",
|
|
12493
|
+
"number",
|
|
12494
|
+
"optional",
|
|
12495
|
+
"preprocess",
|
|
12496
|
+
"promise",
|
|
12497
|
+
"set",
|
|
12498
|
+
"string",
|
|
12499
|
+
"symbol",
|
|
12500
|
+
"undefined",
|
|
12501
|
+
"unknown",
|
|
12502
|
+
"void"
|
|
12503
|
+
]);
|
|
12272
12504
|
var MEMO_CALLEES = /* @__PURE__ */ new Set([
|
|
12273
12505
|
"lazy",
|
|
12274
12506
|
"memo",
|
|
@@ -12345,7 +12577,7 @@ function outermostEnclosingFunction(node) {
|
|
|
12345
12577
|
}
|
|
12346
12578
|
return outermost;
|
|
12347
12579
|
}
|
|
12348
|
-
function subtreeSome(root, predicate) {
|
|
12580
|
+
function subtreeSome(root, predicate, skipDeferredFunctions = false) {
|
|
12349
12581
|
let found = false;
|
|
12350
12582
|
const visit = (value) => {
|
|
12351
12583
|
if (found || value === null || typeof value !== "object") {
|
|
@@ -12361,6 +12593,7 @@ function subtreeSome(root, predicate) {
|
|
|
12361
12593
|
if (typeof candidate2.type !== "string") {
|
|
12362
12594
|
return;
|
|
12363
12595
|
}
|
|
12596
|
+
if (skipDeferredFunctions && FUNCTION_TYPES8.has(candidate2.type)) return;
|
|
12364
12597
|
if (predicate(candidate2)) {
|
|
12365
12598
|
found = true;
|
|
12366
12599
|
return;
|
|
@@ -12459,6 +12692,15 @@ var prefer_module_level_schema_default = createRule({
|
|
|
12459
12692
|
function isZodCall(node) {
|
|
12460
12693
|
return node.type === import_utils67.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils67.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === import_utils67.AST_NODE_TYPES.Identifier && zodNamespaces.has(node.callee.object.name);
|
|
12461
12694
|
}
|
|
12695
|
+
function isSchemaConstruction(node) {
|
|
12696
|
+
const callee = node.callee;
|
|
12697
|
+
if (callee.type !== import_utils67.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils67.AST_NODE_TYPES.Identifier || TERMINAL_METHODS.has(callee.property.name)) return false;
|
|
12698
|
+
if (callee.object.type === import_utils67.AST_NODE_TYPES.CallExpression) return isSchemaConstruction(callee.object);
|
|
12699
|
+
return isZodCall(node) && CONSTRUCTION_FACTORIES.has(callee.property.name);
|
|
12700
|
+
}
|
|
12701
|
+
function hasEagerComputation(node) {
|
|
12702
|
+
return subtreeSome(node, (inner) => inner.type === import_utils67.AST_NODE_TYPES.NewExpression || inner.type === import_utils67.AST_NODE_TYPES.TaggedTemplateExpression || inner.type === import_utils67.AST_NODE_TYPES.CallExpression && !isSchemaConstruction(inner), true);
|
|
12703
|
+
}
|
|
12462
12704
|
function isCovered(node) {
|
|
12463
12705
|
let current = node.parent ?? void 0;
|
|
12464
12706
|
while (current !== void 0) {
|
|
@@ -12588,6 +12830,7 @@ var prefer_module_level_schema_default = createRule({
|
|
|
12588
12830
|
return;
|
|
12589
12831
|
}
|
|
12590
12832
|
const outermost = outermostSchemaExpression(expression);
|
|
12833
|
+
if (hasEagerComputation(outermost)) return;
|
|
12591
12834
|
if (outermost !== expression && (readsReceiver(outermost) || buildsLocalizedText(outermost) || !closesOverNothing(outermost, enclosing))) {
|
|
12592
12835
|
return;
|
|
12593
12836
|
}
|
|
@@ -12725,7 +12968,8 @@ var PREFER_MODULE_LEVEL_REFINED_SCHEMA_DOCUMENTATION = {
|
|
|
12725
12968
|
limitations: [
|
|
12726
12969
|
"Composite object/record/tuple/union schemas are owned by prefer-module-level-schema.",
|
|
12727
12970
|
"Schemas that depend on function-local or mutable state, localized text, receiver state, lazy construction, or recognized memoization are excluded.",
|
|
12728
|
-
"Literal string z.enum domains are owned by prefer-shared-zod-enum."
|
|
12971
|
+
"Literal string z.enum domains are owned by prefer-shared-zod-enum.",
|
|
12972
|
+
"Eager calls outside recognized Zod construction chains and new expressions are excluded. This is manual guidance, not a purity proof: review getters, callback effects, error customization, schema identity, and module initialization order before moving construction."
|
|
12729
12973
|
],
|
|
12730
12974
|
examples: [
|
|
12731
12975
|
{
|
|
@@ -12767,7 +13011,7 @@ function collectReferences2(scope, output) {
|
|
|
12767
13011
|
output.push(...scope.references);
|
|
12768
13012
|
for (const child of scope.childScopes) collectReferences2(child, output);
|
|
12769
13013
|
}
|
|
12770
|
-
function subtreeSome2(root, predicate) {
|
|
13014
|
+
function subtreeSome2(root, predicate, skipDeferredFunctions = false) {
|
|
12771
13015
|
let found = false;
|
|
12772
13016
|
const visit = (value) => {
|
|
12773
13017
|
if (found || value === null || typeof value !== "object") return;
|
|
@@ -12777,6 +13021,7 @@ function subtreeSome2(root, predicate) {
|
|
|
12777
13021
|
}
|
|
12778
13022
|
const candidate2 = value;
|
|
12779
13023
|
if (typeof candidate2.type !== "string") return;
|
|
13024
|
+
if (skipDeferredFunctions && FUNCTION_TYPES9.has(candidate2.type)) return;
|
|
12780
13025
|
if (predicate(candidate2)) {
|
|
12781
13026
|
found = true;
|
|
12782
13027
|
return;
|
|
@@ -12897,6 +13142,15 @@ var prefer_module_level_refined_schema_default = createRule({
|
|
|
12897
13142
|
return names[1] ?? null;
|
|
12898
13143
|
return null;
|
|
12899
13144
|
}
|
|
13145
|
+
function isSchemaConstruction(node) {
|
|
13146
|
+
const callee = node.callee;
|
|
13147
|
+
if (callee.type !== import_utils68.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils68.AST_NODE_TYPES.Identifier || NON_SCHEMA_TERMINALS.has(callee.property.name)) return false;
|
|
13148
|
+
if (callee.object.type === import_utils68.AST_NODE_TYPES.CallExpression) return isSchemaConstruction(callee.object);
|
|
13149
|
+
return factoryName(node, FACTORIES) !== null || factoryName(node, COMPOSITE_FACTORIES) !== null;
|
|
13150
|
+
}
|
|
13151
|
+
function hasEagerComputation(node) {
|
|
13152
|
+
return subtreeSome2(node, (inner) => inner.type === import_utils68.AST_NODE_TYPES.NewExpression || inner.type === import_utils68.AST_NODE_TYPES.TaggedTemplateExpression || inner.type === import_utils68.AST_NODE_TYPES.CallExpression && !isSchemaConstruction(inner), true);
|
|
13153
|
+
}
|
|
12900
13154
|
function isSharedEnumDomain(node, factory) {
|
|
12901
13155
|
if (factory !== "enum") return false;
|
|
12902
13156
|
const [argument] = node.arguments;
|
|
@@ -12965,7 +13219,7 @@ var prefer_module_level_refined_schema_default = createRule({
|
|
|
12965
13219
|
const enclosing = outermostEnclosingFunction2(node);
|
|
12966
13220
|
if (enclosing === void 0) return;
|
|
12967
13221
|
const expression = schemaExpression2(node);
|
|
12968
|
-
if (readsReceiver2(expression) || buildsLocalizedText2(expression) || !closesOverNothing(expression, enclosing))
|
|
13222
|
+
if (hasEagerComputation(expression) || readsReceiver2(expression) || buildsLocalizedText2(expression) || !closesOverNothing(expression, enclosing))
|
|
12969
13223
|
return;
|
|
12970
13224
|
context.report({ node, messageId: "hoistRefinedSchema" });
|
|
12971
13225
|
}
|
|
@@ -12993,7 +13247,7 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
|
|
|
12993
13247
|
outcome: "no-match",
|
|
12994
13248
|
files: [{
|
|
12995
13249
|
path: "src/schema.ts",
|
|
12996
|
-
source: "import { z } from 'zod'; export const Version = z.literal([1, 2, 3]);"
|
|
13250
|
+
source: "import { z } from 'zod/v4'; export const Version = z.literal([1, 2, 3]);"
|
|
12997
13251
|
}],
|
|
12998
13252
|
focusPath: "src/schema.ts",
|
|
12999
13253
|
expectedCount: 0,
|
|
@@ -13005,7 +13259,7 @@ var PREFER_MULTI_VALUE_ZOD_LITERAL_DOCUMENTATION = {
|
|
|
13005
13259
|
outcome: "match",
|
|
13006
13260
|
files: [{
|
|
13007
13261
|
path: "src/schema.ts",
|
|
13008
|
-
source: "import { z } from 'zod'; export const Version = z.union([z.literal(1), z.literal(2), z.literal(3)]);"
|
|
13262
|
+
source: "import { z } from 'zod/v4'; export const Version = z.union([z.literal(1), z.literal(2), z.literal(3)]);"
|
|
13009
13263
|
}],
|
|
13010
13264
|
focusPath: "src/schema.ts",
|
|
13011
13265
|
expectedCount: 1,
|
|
@@ -13128,7 +13382,7 @@ function isLiteralUnion(node) {
|
|
|
13128
13382
|
function exportedContract(node) {
|
|
13129
13383
|
let current = node;
|
|
13130
13384
|
while (current !== void 0) {
|
|
13131
|
-
if (current.type === import_utils70.AST_NODE_TYPES.
|
|
13385
|
+
if (current.type === import_utils70.AST_NODE_TYPES.TSTypeAliasDeclaration || current.type === import_utils70.AST_NODE_TYPES.TSInterfaceDeclaration) return current.parent.type === import_utils70.AST_NODE_TYPES.ExportNamedDeclaration;
|
|
13132
13386
|
if (current.type === import_utils70.AST_NODE_TYPES.Program) return false;
|
|
13133
13387
|
current = current.parent ?? void 0;
|
|
13134
13388
|
}
|
|
@@ -13164,11 +13418,11 @@ var import_utils71 = require("@typescript-eslint/utils");
|
|
|
13164
13418
|
var PREFER_NAMED_COMPLEX_RETURN_TYPE_DOCUMENTATION = {
|
|
13165
13419
|
summary: "Prefer a named contract for structurally complex function return types.",
|
|
13166
13420
|
rationale: "A large inline return annotation hides a reusable domain concept and makes signatures difficult to scan.",
|
|
13167
|
-
remediation: "
|
|
13421
|
+
remediation: "Name the complex nested shape while preserving its generic wrappers and type parameters; reference the named contract from the return annotation.",
|
|
13168
13422
|
category: "maintainability",
|
|
13169
13423
|
limitations: [
|
|
13170
13424
|
"Only explicit object types with at least three members and unions with at least three object variants are reported.",
|
|
13171
|
-
"
|
|
13425
|
+
"Any single-argument generic wrapper is traversed to find nested shapes, not assumed semantically transparent. Inferred return types are outside this rule; extraction is manual and must preserve locally bound type parameters."
|
|
13172
13426
|
],
|
|
13173
13427
|
examples: [
|
|
13174
13428
|
{ id: "named-result", title: "Name a multi-state result", outcome: "no-match", files: [{ path: "src/queue.ts", source: "type ClaimResult = { state: 'idle' } | { state: 'waiting'; retryAt: number } | { state: 'claimed'; id: string }; export function claim(): ClaimResult { return { state: 'idle' }; }" }], focusPath: "src/queue.ts", expectedCount: 0, public: true },
|
|
@@ -13226,7 +13480,7 @@ var PREFER_NATIVE_RANDOM_UUID_DOCUMENTATION = {
|
|
|
13226
13480
|
remediation: "Call `globalThis.crypto.randomUUID()` and remove the unused `uuid` v4 import when possible.",
|
|
13227
13481
|
category: "maintainability",
|
|
13228
13482
|
autofix: "suggestion",
|
|
13229
|
-
limitations: ["Only resolved zero-argument UUID v4 calls are reported; customized and other UUID versions are excluded."],
|
|
13483
|
+
limitations: ["Only resolved zero-argument UUID v4 calls are reported; customized and other UUID versions are excluded. Suggestions require unshadowed globalThis and comment-free calls; verify native randomUUID availability in the deployment runtime."],
|
|
13230
13484
|
examples: [
|
|
13231
13485
|
{ id: "native-random-uuid", title: "Use the platform UUID generator", outcome: "no-match", files: [{ path: "src/id.ts", source: "const id = globalThis.crypto.randomUUID();" }], focusPath: "src/id.ts", expectedCount: 0, public: true },
|
|
13232
13486
|
{ id: "uuid-v4-package", title: "Do not call uuid v4 without options", outcome: "match", files: [{ path: "src/id.ts", source: "import { v4 } from 'uuid'; const id = v4();" }], focusPath: "src/id.ts", expectedCount: 1, public: true }
|
|
@@ -13246,7 +13500,7 @@ var prefer_native_random_uuid_default = createRule({
|
|
|
13246
13500
|
hasSuggestions: true,
|
|
13247
13501
|
schema: [],
|
|
13248
13502
|
messages: {
|
|
13249
|
-
preferNative: "
|
|
13503
|
+
preferNative: "Where supported by the deployment runtime, prefer native `globalThis.crypto.randomUUID()` over the `uuid` package for UUID v4.",
|
|
13250
13504
|
replaceWithNative: "Replace this UUID v4 call with the native implementation."
|
|
13251
13505
|
}
|
|
13252
13506
|
},
|
|
@@ -13262,15 +13516,17 @@ var prefer_native_random_uuid_default = createRule({
|
|
|
13262
13516
|
if (variable !== null) destination.add(variable);
|
|
13263
13517
|
}
|
|
13264
13518
|
function report2(node) {
|
|
13519
|
+
const globalBinding = import_utils72.ASTUtils.findVariable(context.sourceCode.getScope(node), "globalThis");
|
|
13520
|
+
const canSuggest = (globalBinding?.defs.length ?? 0) === 0 && context.sourceCode.getCommentsInside(node).length === 0;
|
|
13265
13521
|
context.report({
|
|
13266
13522
|
node,
|
|
13267
13523
|
messageId: "preferNative",
|
|
13268
|
-
suggest: [
|
|
13524
|
+
suggest: canSuggest ? [
|
|
13269
13525
|
{
|
|
13270
13526
|
messageId: "replaceWithNative",
|
|
13271
13527
|
fix: (fixer) => fixer.replaceText(node, "globalThis.crypto.randomUUID()")
|
|
13272
13528
|
}
|
|
13273
|
-
]
|
|
13529
|
+
] : []
|
|
13274
13530
|
});
|
|
13275
13531
|
}
|
|
13276
13532
|
return {
|
|
@@ -13322,12 +13578,14 @@ var import_utils73 = require("@typescript-eslint/utils");
|
|
|
13322
13578
|
var PREFER_NODE_CRYPTO_HASH_DOCUMENTATION = {
|
|
13323
13579
|
summary: "Prefer the modern one-shot node:crypto hash API when streaming state is unnecessary.",
|
|
13324
13580
|
rationale: "A createHash-update-digest chain allocates mutable streaming state for a single in-memory value; Node's built-in hash function expresses the one-shot operation directly and can use its optimized fast path.",
|
|
13325
|
-
remediation: "
|
|
13581
|
+
remediation: "On a supported Node runtime, consider hash(algorithm, value, encoding). Preserve the output encoding explicitly: digest() returns a Buffer, while hash defaults to hex. Keep createHash for streams or multiple updates.",
|
|
13326
13582
|
category: "performance",
|
|
13327
13583
|
limitations: [
|
|
13328
13584
|
"Only bindings and inline calls with statically proven provenance from crypto or node:crypto are analyzed; arbitrary assignments and dynamic module specifiers are excluded.",
|
|
13329
|
-
"Only a literal algorithm with exactly one update call is reported; streaming and incremental hashes remain valid."
|
|
13585
|
+
"Only a literal algorithm with exactly one update call is reported; streaming and incremental hashes remain valid.",
|
|
13586
|
+
"Runtime support and output encoding require manual review; no autofix or guaranteed speedup is promised."
|
|
13330
13587
|
],
|
|
13588
|
+
references: ["https://nodejs.org/api/crypto.html#cryptohashalgorithm-data-options"],
|
|
13331
13589
|
examples: [
|
|
13332
13590
|
{ id: "one-shot-hash", title: "Use Node's one-shot hash API", outcome: "no-match", files: [{ path: "case.ts", source: "import { hash } from 'node:crypto'; export const digest = hash('sha256', 'value', 'hex');" }], focusPath: "case.ts", expectedCount: 0, public: true },
|
|
13333
13591
|
{ id: "mutable-one-shot-chain", title: "Avoid mutable state for one value", outcome: "match", files: [{ path: "case.ts", source: "import { createHash } from 'node:crypto'; export const digest = createHash('sha256').update('value').digest('hex');" }], focusPath: "case.ts", expectedCount: 1, public: true }
|
|
@@ -13456,6 +13714,7 @@ var PREFER_NODE_FS_PROMISES_DOCUMENTATION = {
|
|
|
13456
13714
|
category: "performance",
|
|
13457
13715
|
limitations: [
|
|
13458
13716
|
"Tests and generated files are excluded.",
|
|
13717
|
+
"This recommendation applies to Node-compatible runtimes. Changing a synchronous API to a promise changes its caller contract; review startup-only work and APIs that require synchronous execution rather than mechanically adding await.",
|
|
13459
13718
|
"ESLint rule implementations under src/rules are excluded because visitor creation and execution are synchronous by contract.",
|
|
13460
13719
|
"Only statically identifiable node:fs loads are inspected; filesystem objects passed through arbitrary functions or assignments require type-aware analysis."
|
|
13461
13720
|
],
|
|
@@ -13474,14 +13733,14 @@ function memberName5(node) {
|
|
|
13474
13733
|
function unwrapAwait2(node) {
|
|
13475
13734
|
return node.type === import_utils74.AST_NODE_TYPES.AwaitExpression ? node.argument : node;
|
|
13476
13735
|
}
|
|
13477
|
-
function isFsLoader(node) {
|
|
13736
|
+
function isFsLoader(node, isGlobal) {
|
|
13478
13737
|
const expression = unwrapAwait2(node);
|
|
13479
13738
|
if (expression.type === import_utils74.AST_NODE_TYPES.ImportExpression) return isFsSpecifier(expression.source);
|
|
13480
13739
|
if (expression.type !== import_utils74.AST_NODE_TYPES.CallExpression || expression.arguments.length !== 1) return false;
|
|
13481
13740
|
const [argument] = expression.arguments;
|
|
13482
13741
|
if (argument === void 0 || argument.type === import_utils74.AST_NODE_TYPES.SpreadElement || !isFsSpecifier(argument)) return false;
|
|
13483
|
-
if (expression.callee.type === import_utils74.AST_NODE_TYPES.Identifier) return expression.callee.name === "require";
|
|
13484
|
-
return expression.callee.type === import_utils74.AST_NODE_TYPES.MemberExpression && expression.callee.object.type === import_utils74.AST_NODE_TYPES.Identifier && expression.callee.object.name === "process" && memberName5(expression.callee) === "getBuiltinModule";
|
|
13742
|
+
if (expression.callee.type === import_utils74.AST_NODE_TYPES.Identifier) return expression.callee.name === "require" && isGlobal(expression.callee);
|
|
13743
|
+
return expression.callee.type === import_utils74.AST_NODE_TYPES.MemberExpression && expression.callee.object.type === import_utils74.AST_NODE_TYPES.Identifier && expression.callee.object.name === "process" && isGlobal(expression.callee.object) && memberName5(expression.callee) === "getBuiltinModule";
|
|
13485
13744
|
}
|
|
13486
13745
|
function isFsSpecifier(node) {
|
|
13487
13746
|
return node.type === import_utils74.AST_NODE_TYPES.Literal && (node.value === "node:fs" || node.value === "fs");
|
|
@@ -13508,13 +13767,26 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13508
13767
|
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text) || normalizedFilename.includes("src/rules/"))
|
|
13509
13768
|
return {};
|
|
13510
13769
|
const namespaces = /* @__PURE__ */ new Set();
|
|
13770
|
+
const bindingOf = (node) => import_utils74.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
|
|
13771
|
+
const isGlobal = (node) => {
|
|
13772
|
+
const binding = bindingOf(node);
|
|
13773
|
+
return binding === null || binding.defs.length === 0;
|
|
13774
|
+
};
|
|
13775
|
+
const isNamespace = (node) => {
|
|
13776
|
+
const binding = bindingOf(node);
|
|
13777
|
+
return binding !== null && namespaces.has(binding) && !binding.references.some((reference) => reference.isWrite() && reference.init !== true);
|
|
13778
|
+
};
|
|
13779
|
+
const recordNamespace = (node) => {
|
|
13780
|
+
const binding = bindingOf(node);
|
|
13781
|
+
if (binding !== null) namespaces.add(binding);
|
|
13782
|
+
};
|
|
13511
13783
|
return {
|
|
13512
13784
|
ImportDeclaration(node) {
|
|
13513
13785
|
if (node.source.value !== "node:fs" && node.source.value !== "fs") return;
|
|
13514
13786
|
const synchronousImports = [];
|
|
13515
13787
|
for (const specifier of node.specifiers) {
|
|
13516
13788
|
if (specifier.type === import_utils74.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils74.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
13517
|
-
|
|
13789
|
+
recordNamespace(specifier.local);
|
|
13518
13790
|
continue;
|
|
13519
13791
|
}
|
|
13520
13792
|
if (specifier.type === import_utils74.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils74.AST_NODE_TYPES.Identifier && specifier.imported.name.endsWith("Sync")) synchronousImports.push(specifier.imported.name);
|
|
@@ -13528,10 +13800,10 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13528
13800
|
}
|
|
13529
13801
|
},
|
|
13530
13802
|
VariableDeclarator(node) {
|
|
13531
|
-
if (node.init === null || !isFsLoader(node.init) && (node.init.type !== import_utils74.AST_NODE_TYPES.Identifier || !
|
|
13803
|
+
if (node.init === null || !isFsLoader(node.init, isGlobal) && (node.init.type !== import_utils74.AST_NODE_TYPES.Identifier || !isNamespace(node.init)))
|
|
13532
13804
|
return;
|
|
13533
13805
|
if (node.id.type === import_utils74.AST_NODE_TYPES.Identifier) {
|
|
13534
|
-
|
|
13806
|
+
recordNamespace(node.id);
|
|
13535
13807
|
return;
|
|
13536
13808
|
}
|
|
13537
13809
|
if (node.id.type !== import_utils74.AST_NODE_TYPES.ObjectPattern) return;
|
|
@@ -13552,7 +13824,7 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13552
13824
|
const name = memberName5(node);
|
|
13553
13825
|
if (name?.endsWith("Sync") !== true) return;
|
|
13554
13826
|
const object = unwrapAwait2(node.object);
|
|
13555
|
-
if (object.type === import_utils74.AST_NODE_TYPES.Identifier &&
|
|
13827
|
+
if (object.type === import_utils74.AST_NODE_TYPES.Identifier && isNamespace(object) || isFsLoader(object, isGlobal)) {
|
|
13556
13828
|
context.report({ node, messageId: "preferAsyncFs", data: { name } });
|
|
13557
13829
|
}
|
|
13558
13830
|
}
|
|
@@ -13563,11 +13835,11 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
13563
13835
|
// src/rules/prefer-non-nullable-collection.ts
|
|
13564
13836
|
var import_utils75 = require("@typescript-eslint/utils");
|
|
13565
13837
|
var PREFER_NON_NULLABLE_COLLECTION_DOCUMENTATION = {
|
|
13566
|
-
summary: "Suggest
|
|
13838
|
+
summary: "Suggest reviewing nullish arrays that use local empty-array defaults or a shared null-or-empty guard.",
|
|
13567
13839
|
rationale: "A redundant nullish collection state spreads defaults and guards through consumers without carrying information.",
|
|
13568
13840
|
remediation: "Use a non-null collection type and normalize omitted input to an empty collection at the boundary.",
|
|
13569
13841
|
category: "maintainability",
|
|
13570
|
-
limitations: ["
|
|
13842
|
+
limitations: ["Recognized defaults and guards are manual modeling prompts, not proof of equivalence for every caller or later use. Exported wire shapes and unknown object escapes are excluded; preserve meaningful null states and boundary compatibility."],
|
|
13571
13843
|
examples: [
|
|
13572
13844
|
{ id: "non-null-array", title: "Model an always-present collection", outcome: "no-match", files: [{ path: "src/search.ts", source: "interface Input { items: string[] } function search({ items }: Input) { return items.length; }" }], focusPath: "src/search.ts", expectedCount: 0, public: true },
|
|
13573
13845
|
{ id: "defaulted-nullish-array", title: "Do not retain a redundant nullish state", outcome: "match", files: [{ path: "src/search.ts", source: "interface Input { items: string[] | undefined } function search({ items = [] }: Input) { return items.length; }" }], focusPath: "src/search.ts", expectedCount: 1, public: true }
|
|
@@ -13636,22 +13908,6 @@ function sameAccess(node, access) {
|
|
|
13636
13908
|
}
|
|
13637
13909
|
return node.type === import_utils75.AST_NODE_TYPES.MemberExpression && !node.computed && node.object.type === import_utils75.AST_NODE_TYPES.Identifier && node.object.name === access.object && node.property.type === import_utils75.AST_NODE_TYPES.Identifier && node.property.name === access.property;
|
|
13638
13910
|
}
|
|
13639
|
-
function isNullGuard(node, access) {
|
|
13640
|
-
if (node.type === import_utils75.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && (sameAccess(node.argument, access) || optionalMemberLengthOf(node.argument, access))) return true;
|
|
13641
|
-
if (node.type !== import_utils75.AST_NODE_TYPES.BinaryExpression || !["==", "==="].includes(node.operator)) {
|
|
13642
|
-
return false;
|
|
13643
|
-
}
|
|
13644
|
-
const nullish = (value) => value.type === import_utils75.AST_NODE_TYPES.Literal && value.value === null || value.type === import_utils75.AST_NODE_TYPES.Identifier && value.name === "undefined";
|
|
13645
|
-
return sameAccess(node.left, access) && nullish(node.right) || sameAccess(node.right, access) && nullish(node.left);
|
|
13646
|
-
}
|
|
13647
|
-
function isEmptyGuard(node, access) {
|
|
13648
|
-
if (node.type === import_utils75.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && memberLengthOf(node.argument, access)) return true;
|
|
13649
|
-
if (node.type !== import_utils75.AST_NODE_TYPES.BinaryExpression || !["==", "===", "<="].includes(node.operator)) {
|
|
13650
|
-
return false;
|
|
13651
|
-
}
|
|
13652
|
-
const zero = (value) => value.type === import_utils75.AST_NODE_TYPES.Literal && value.value === 0;
|
|
13653
|
-
return memberLengthOf(node.left, access) && zero(node.right) || memberLengthOf(node.right, access) && zero(node.left);
|
|
13654
|
-
}
|
|
13655
13911
|
function memberLengthOf(node, access) {
|
|
13656
13912
|
const target = node.type === import_utils75.AST_NODE_TYPES.ChainExpression ? node.expression : node;
|
|
13657
13913
|
return target.type === import_utils75.AST_NODE_TYPES.MemberExpression && !target.computed && target.property.type === import_utils75.AST_NODE_TYPES.Identifier && target.property.name === "length" && sameAccess(target.object, access);
|
|
@@ -13666,7 +13922,24 @@ function hasEquivalentLeadingGuard(fn, access, visitorKeys) {
|
|
|
13666
13922
|
const terminating = first.consequent.type === import_utils75.AST_NODE_TYPES.ReturnStatement || first.consequent.type === import_utils75.AST_NODE_TYPES.ThrowStatement || first.consequent.type === import_utils75.AST_NODE_TYPES.BlockStatement && first.consequent.body.length === 1 && (first.consequent.body[0]?.type === import_utils75.AST_NODE_TYPES.ReturnStatement || first.consequent.body[0]?.type === import_utils75.AST_NODE_TYPES.ThrowStatement);
|
|
13667
13923
|
if (!terminating) return false;
|
|
13668
13924
|
if (contains(first.consequent, visitorKeys, (node) => sameAccess(node, access))) return false;
|
|
13669
|
-
|
|
13925
|
+
if (first.test.type === import_utils75.AST_NODE_TYPES.UnaryExpression && first.test.operator === "!" && optionalMemberLengthOf(first.test.argument, access)) return true;
|
|
13926
|
+
return first.test.type === import_utils75.AST_NODE_TYPES.LogicalExpression && first.test.operator === "||" && isNullGuard(first.test.left, access) && isEmptyGuard(first.test.right, access);
|
|
13927
|
+
}
|
|
13928
|
+
function isNullGuard(node, access) {
|
|
13929
|
+
if (node.type === import_utils75.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && (sameAccess(node.argument, access) || optionalMemberLengthOf(node.argument, access))) return true;
|
|
13930
|
+
if (node.type !== import_utils75.AST_NODE_TYPES.BinaryExpression || !["==", "==="].includes(node.operator)) {
|
|
13931
|
+
return false;
|
|
13932
|
+
}
|
|
13933
|
+
const nullish = (value) => value.type === import_utils75.AST_NODE_TYPES.Literal && value.value === null || value.type === import_utils75.AST_NODE_TYPES.Identifier && value.name === "undefined";
|
|
13934
|
+
return sameAccess(node.left, access) && nullish(node.right) || sameAccess(node.right, access) && nullish(node.left);
|
|
13935
|
+
}
|
|
13936
|
+
function isEmptyGuard(node, access) {
|
|
13937
|
+
if (node.type === import_utils75.AST_NODE_TYPES.UnaryExpression && node.operator === "!" && memberLengthOf(node.argument, access)) return true;
|
|
13938
|
+
if (node.type !== import_utils75.AST_NODE_TYPES.BinaryExpression || !["==", "===", "<="].includes(node.operator)) {
|
|
13939
|
+
return false;
|
|
13940
|
+
}
|
|
13941
|
+
const zero = (value) => value.type === import_utils75.AST_NODE_TYPES.Literal && value.value === 0;
|
|
13942
|
+
return memberLengthOf(node.left, access) && zero(node.right) || node.operator !== "<=" && memberLengthOf(node.right, access) && zero(node.left);
|
|
13670
13943
|
}
|
|
13671
13944
|
function contains(node, visitorKeys, predicate) {
|
|
13672
13945
|
if (predicate(node)) return true;
|
|
@@ -13704,7 +13977,7 @@ function memberIsOnlyCoalesced(context, object, property, fn) {
|
|
|
13704
13977
|
if (!belongsToFunction(reference.identifier, fn)) return [null];
|
|
13705
13978
|
const parent = reference.identifier.parent;
|
|
13706
13979
|
if (parent?.type === import_utils75.AST_NODE_TYPES.MemberExpression && !parent.computed && parent.object === reference.identifier && parent.property.type === import_utils75.AST_NODE_TYPES.Identifier && parent.property.name === property) return [parent];
|
|
13707
|
-
return [];
|
|
13980
|
+
return parent?.type === import_utils75.AST_NODE_TYPES.MemberExpression && parent.object === reference.identifier ? [] : [null];
|
|
13708
13981
|
});
|
|
13709
13982
|
return accesses.length > 0 && accesses.every((access) => access !== null && directlyCoalesced(access));
|
|
13710
13983
|
}
|
|
@@ -13714,11 +13987,11 @@ var prefer_non_nullable_collection_default = createRule({
|
|
|
13714
13987
|
meta: {
|
|
13715
13988
|
type: "suggestion",
|
|
13716
13989
|
docs: {
|
|
13717
|
-
description: "Suggest
|
|
13990
|
+
description: "Suggest reviewing nullish arrays that use local empty-array defaults or a shared null-or-empty guard."
|
|
13718
13991
|
},
|
|
13719
13992
|
schema: [],
|
|
13720
13993
|
messages: {
|
|
13721
|
-
preferNonNullableCollection: "`{{name}}`
|
|
13994
|
+
preferNonNullableCollection: "`{{name}}` uses an empty-array default or shared null-or-empty guard; consider a non-null array after checking that the nullish state carries no separate meaning."
|
|
13722
13995
|
}
|
|
13723
13996
|
},
|
|
13724
13997
|
defaultOptions: [],
|
|
@@ -13967,12 +14240,12 @@ var ts4 = __toESM(require("typescript"), 1);
|
|
|
13967
14240
|
var PREFER_AWAIT_IN_ASYNC_RETURN_DOCUMENTATION = {
|
|
13968
14241
|
summary: "Prefer explicit `await` when an async function directly returns one typed Promise `.then` transform.",
|
|
13969
14242
|
rationale: "Mixing a directly returned Promise callback into otherwise async control flow makes sequencing and failures harder to read.",
|
|
13970
|
-
remediation: "
|
|
14243
|
+
remediation: "Consider awaiting the Promise and returning the transformed value with ordinary async statements. Preserve catch boundaries, callback behavior, and observable scheduling when rewriting manually.",
|
|
13971
14244
|
category: "maintainability",
|
|
13972
14245
|
since: "15.6.3",
|
|
13973
14246
|
limitations: [
|
|
13974
14247
|
"Only a single directly returned `.then` call with an inline callback is checked.",
|
|
13975
|
-
"The receiver must be proven Promise-like by TypeScript;
|
|
14248
|
+
"The receiver must be proven Promise-like by TypeScript; an earlier chain can still produce that receiver. Untyped receivers, rejection handlers and named callbacks are excluded. This is not the upstream return-await policy and no scheduling equivalence is promised.",
|
|
13976
14249
|
"Direct loader callbacks passed to resolved `React.lazy` and `next/dynamic` imports are excluded because returning the module Promise is their framework contract."
|
|
13977
14250
|
],
|
|
13978
14251
|
examples: [
|
|
@@ -14113,12 +14386,17 @@ var import_utils78 = require("@typescript-eslint/utils");
|
|
|
14113
14386
|
var PREFER_SCHEMA_FOR_API_PAYLOAD_DOCUMENTATION = {
|
|
14114
14387
|
summary: "Require Zod (or similar) schema validation on `response.json()` / `JSON.parse()` results before property access.",
|
|
14115
14388
|
rationale: "External JSON is untrusted at runtime even when its expected TypeScript shape is known statically.",
|
|
14116
|
-
remediation: "
|
|
14389
|
+
remediation: "For external payloads, parse through a schema or establish runtime validation before reading fields. Review validator implementations separately rather than recursively requiring another schema.",
|
|
14117
14390
|
category: "correctness",
|
|
14118
|
-
limitations: [
|
|
14391
|
+
limitations: [
|
|
14392
|
+
"A JSON.parse call alone does not prove external input; exact native JSON stringify/parse round trips are excluded. Validator implementations and other non-network parsing can still require manual review rather than a schema rewrite.",
|
|
14393
|
+
"JSON.parse must resolve to the global JSON object. json() receivers require an unshadowed global Request/Response annotation or construction, or stable local aliases of global fetch results; imported, inferred and unknown response types are deliberately not inferred.",
|
|
14394
|
+
"Named validators remain conventions, not proof of their implementation. Their exemptions are confined to a valid branch or a preceding same-block validation statement; ignored predicate results and deferred callbacks do not validate later reads.",
|
|
14395
|
+
"Test fixtures, generated clients and recognized local-file reads are excluded. This is bounded local analysis, not a general control-flow or mutation proof."
|
|
14396
|
+
],
|
|
14119
14397
|
examples: [
|
|
14120
|
-
{ 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 },
|
|
14121
|
-
{ id: "unvalidated-payload", title: "Do not trust response JSON directly", outcome: "match", files: [{ path: "src/client.ts", source: "async function load(response) { const body = await response.json(); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
|
|
14398
|
+
{ id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "async function load(response: Response) { const body = UserSchema.parse(await response.json()); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
|
|
14399
|
+
{ id: "unvalidated-payload", title: "Do not trust response JSON directly", outcome: "match", files: [{ path: "src/client.ts", source: "async function load(response: Response) { const body = await response.json(); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
|
|
14122
14400
|
]
|
|
14123
14401
|
};
|
|
14124
14402
|
var unwrap6 = (node) => {
|
|
@@ -14143,7 +14421,7 @@ var isSchemaParseReference = (node) => {
|
|
|
14143
14421
|
const inner = unwrap6(node);
|
|
14144
14422
|
return inner !== null && inner.type === import_utils78.AST_NODE_TYPES.MemberExpression && !inner.computed && inner.property.type === import_utils78.AST_NODE_TYPES.Identifier && (inner.property.name === "parse" || inner.property.name === "safeParse");
|
|
14145
14423
|
};
|
|
14146
|
-
var isRawPayloadSource = (node, isKnownLocalText) => {
|
|
14424
|
+
var isRawPayloadSource = (node, context, isKnownLocalText) => {
|
|
14147
14425
|
let current = unwrap6(node);
|
|
14148
14426
|
if (current === null) return false;
|
|
14149
14427
|
if (current.type === import_utils78.AST_NODE_TYPES.AwaitExpression) {
|
|
@@ -14161,13 +14439,31 @@ var isRawPayloadSource = (node, isKnownLocalText) => {
|
|
|
14161
14439
|
return false;
|
|
14162
14440
|
}
|
|
14163
14441
|
if (property.name === "json") {
|
|
14164
|
-
return
|
|
14442
|
+
return !callee.computed && current.arguments.length === 0 && isResponseSource(callee.object, context);
|
|
14165
14443
|
}
|
|
14166
14444
|
if (PROMISE_CHAIN_METHODS.has(property.name)) {
|
|
14167
|
-
return !current.arguments.some(isSchemaParseReference) && isRawPayloadSource(callee.object);
|
|
14445
|
+
return !current.arguments.some(isSchemaParseReference) && isRawPayloadSource(callee.object, context, isKnownLocalText);
|
|
14168
14446
|
}
|
|
14169
14447
|
const object = unwrap6(callee.object);
|
|
14170
|
-
|
|
14448
|
+
const input = unwrap6(current.arguments[0]);
|
|
14449
|
+
if (input?.type === import_utils78.AST_NODE_TYPES.CallExpression && input.arguments.length === 1 && input.callee.type === import_utils78.AST_NODE_TYPES.MemberExpression && !input.callee.computed && input.callee.object.type === import_utils78.AST_NODE_TYPES.Identifier && input.callee.object.name === "JSON" && input.callee.property.type === import_utils78.AST_NODE_TYPES.Identifier && input.callee.property.name === "stringify" && (import_utils78.ASTUtils.findVariable(context.sourceCode.getScope(input.callee.object), "JSON")?.defs.length ?? 0) === 0) return false;
|
|
14450
|
+
return property.name === "parse" && object !== null && object.type === import_utils78.AST_NODE_TYPES.Identifier && object.name === "JSON" && (import_utils78.ASTUtils.findVariable(context.sourceCode.getScope(object), "JSON")?.defs.length ?? 0) === 0 && !isLocalFileRead(current.arguments[0]) && isKnownLocalText?.(current.arguments[0]) !== true;
|
|
14451
|
+
};
|
|
14452
|
+
var isResponseSource = (node, context, seen = /* @__PURE__ */ new Set()) => {
|
|
14453
|
+
let current = unwrap6(node);
|
|
14454
|
+
if (current?.type === import_utils78.AST_NODE_TYPES.AwaitExpression) current = unwrap6(current.argument);
|
|
14455
|
+
if (current === null || seen.has(current)) return false;
|
|
14456
|
+
seen.add(current);
|
|
14457
|
+
const isGlobal = (identifier) => (import_utils78.ASTUtils.findVariable(context.sourceCode.getScope(identifier), identifier.name)?.defs.length ?? 0) === 0;
|
|
14458
|
+
if (current.type === import_utils78.AST_NODE_TYPES.CallExpression) return current.callee.type === import_utils78.AST_NODE_TYPES.Identifier && current.callee.name === "fetch" && isGlobal(current.callee);
|
|
14459
|
+
if (current.type === import_utils78.AST_NODE_TYPES.NewExpression) return current.callee.type === import_utils78.AST_NODE_TYPES.Identifier && ["Request", "Response"].includes(current.callee.name) && isGlobal(current.callee);
|
|
14460
|
+
if (current.type !== import_utils78.AST_NODE_TYPES.Identifier) return false;
|
|
14461
|
+
const binding = import_utils78.ASTUtils.findVariable(context.sourceCode.getScope(current), current.name);
|
|
14462
|
+
if (binding?.defs.length !== 1 || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
14463
|
+
const definition = binding.defs[0];
|
|
14464
|
+
const annotation = definition?.name.type === import_utils78.AST_NODE_TYPES.Identifier ? definition.name.typeAnnotation?.typeAnnotation : void 0;
|
|
14465
|
+
if (annotation?.type === import_utils78.AST_NODE_TYPES.TSTypeReference && annotation.typeName.type === import_utils78.AST_NODE_TYPES.Identifier && ["Request", "Response"].includes(annotation.typeName.name) && isGlobal(annotation.typeName)) return true;
|
|
14466
|
+
return definition?.type === "Variable" && definition.parent.kind === "const" && definition.node.init !== null && isResponseSource(definition.node.init, context, seen);
|
|
14171
14467
|
};
|
|
14172
14468
|
var FILE_READ_RE = /^(readFile|readFileSync|readJson|readJsonSync|readJSON)$/;
|
|
14173
14469
|
var isDirectLocalFileRead = (node) => {
|
|
@@ -14433,7 +14729,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14433
14729
|
},
|
|
14434
14730
|
schema: [],
|
|
14435
14731
|
messages: {
|
|
14436
|
-
unparsedJsonAccess: "
|
|
14732
|
+
unparsedJsonAccess: "Review property access on parsed JSON without a recognized validation boundary. For external payloads, validate before reading fields; validator implementations need manual review, not a recursive schema rewrite."
|
|
14437
14733
|
}
|
|
14438
14734
|
},
|
|
14439
14735
|
defaultOptions: [],
|
|
@@ -14444,6 +14740,40 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14444
14740
|
const unvalidatedVariables = /* @__PURE__ */ new Set();
|
|
14445
14741
|
const aliasGroups = /* @__PURE__ */ new Map();
|
|
14446
14742
|
const localFileTextVariables = /* @__PURE__ */ new Set();
|
|
14743
|
+
const namedGuards = /* @__PURE__ */ new Map();
|
|
14744
|
+
const guardDominates = (use, call) => {
|
|
14745
|
+
if (context.sourceCode.getScope(use).variableScope !== context.sourceCode.getScope(call).variableScope) return false;
|
|
14746
|
+
let guard = call;
|
|
14747
|
+
let positive = true;
|
|
14748
|
+
while (guard.parent.type === import_utils78.AST_NODE_TYPES.UnaryExpression && guard.parent.operator === "!") {
|
|
14749
|
+
positive = !positive;
|
|
14750
|
+
guard = guard.parent;
|
|
14751
|
+
}
|
|
14752
|
+
while (positive && guard.parent.type === import_utils78.AST_NODE_TYPES.LogicalExpression && guard.parent.operator === "&&") guard = guard.parent;
|
|
14753
|
+
const branch = guard.parent;
|
|
14754
|
+
if (branch.type === import_utils78.AST_NODE_TYPES.IfStatement && branch.test === guard) {
|
|
14755
|
+
if (positive && nodeWithin2(use, branch.consequent)) return true;
|
|
14756
|
+
if (!positive && branch.alternate !== null && nodeWithin2(use, branch.alternate)) return true;
|
|
14757
|
+
const terminal = branch.consequent.type === import_utils78.AST_NODE_TYPES.BlockStatement ? branch.consequent.body.at(-1) : branch.consequent;
|
|
14758
|
+
if (!positive && (terminal?.type === import_utils78.AST_NODE_TYPES.ThrowStatement || terminal?.type === import_utils78.AST_NODE_TYPES.ReturnStatement)) {
|
|
14759
|
+
let statement2 = use;
|
|
14760
|
+
while (statement2.parent !== void 0 && statement2.parent !== branch.parent && statement2.parent.type !== import_utils78.AST_NODE_TYPES.Program) {
|
|
14761
|
+
if (statement2.type === import_utils78.AST_NODE_TYPES.FunctionDeclaration || statement2.type === import_utils78.AST_NODE_TYPES.FunctionExpression || statement2.type === import_utils78.AST_NODE_TYPES.ArrowFunctionExpression) return false;
|
|
14762
|
+
statement2 = statement2.parent;
|
|
14763
|
+
}
|
|
14764
|
+
return statement2.parent === branch.parent && statement2.range[0] > branch.range[1];
|
|
14765
|
+
}
|
|
14766
|
+
}
|
|
14767
|
+
if (branch.type === import_utils78.AST_NODE_TYPES.ConditionalExpression && branch.test === guard) return nodeWithin2(use, positive ? branch.consequent : branch.alternate);
|
|
14768
|
+
if (positive && branch.type === import_utils78.AST_NODE_TYPES.WhileStatement && branch.test === guard) return nodeWithin2(use, branch.body);
|
|
14769
|
+
if (call.parent.type !== import_utils78.AST_NODE_TYPES.ExpressionStatement || call.callee.type !== import_utils78.AST_NODE_TYPES.Identifier || /^(?:is|has)[A-Z]/u.test(call.callee.name)) return false;
|
|
14770
|
+
let statement = use;
|
|
14771
|
+
while (statement.parent !== void 0 && statement.parent !== call.parent.parent && statement.parent.type !== import_utils78.AST_NODE_TYPES.Program) {
|
|
14772
|
+
if (statement.type === import_utils78.AST_NODE_TYPES.FunctionDeclaration || statement.type === import_utils78.AST_NODE_TYPES.FunctionExpression || statement.type === import_utils78.AST_NODE_TYPES.ArrowFunctionExpression) return false;
|
|
14773
|
+
statement = statement.parent;
|
|
14774
|
+
}
|
|
14775
|
+
return statement.parent === call.parent.parent && statement.range[0] > call.parent.range[1];
|
|
14776
|
+
};
|
|
14447
14777
|
const localFileTextRef = (node, scope) => {
|
|
14448
14778
|
const unwrapped = unwrap6(node);
|
|
14449
14779
|
if (unwrapped?.type !== import_utils78.AST_NODE_TYPES.Identifier) return null;
|
|
@@ -14460,6 +14790,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14460
14790
|
};
|
|
14461
14791
|
const clearBinding = (variable) => {
|
|
14462
14792
|
unvalidatedVariables.delete(variable);
|
|
14793
|
+
namedGuards.delete(variable);
|
|
14463
14794
|
const group = aliasGroups.get(variable);
|
|
14464
14795
|
aliasGroups.delete(variable);
|
|
14465
14796
|
group?.delete(variable);
|
|
@@ -14494,10 +14825,24 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14494
14825
|
};
|
|
14495
14826
|
const isFullyNarrowedPattern = (declarator) => {
|
|
14496
14827
|
const declared = context.sourceCode.getDeclaredVariables(declarator);
|
|
14828
|
+
const statement = declarator.parent;
|
|
14829
|
+
const block = statement.parent;
|
|
14830
|
+
const followsRejectingGuard = (identifier) => {
|
|
14831
|
+
if (block.type !== import_utils78.AST_NODE_TYPES.BlockStatement && block.type !== import_utils78.AST_NODE_TYPES.Program) return false;
|
|
14832
|
+
if (context.sourceCode.getScope(identifier).variableScope !== context.sourceCode.getScope(declarator).variableScope) return false;
|
|
14833
|
+
return block.body.some((candidate2) => {
|
|
14834
|
+
if (candidate2.type !== import_utils78.AST_NODE_TYPES.IfStatement || candidate2.range[1] >= identifier.range[0] || bindingValidationPolarity(candidate2.test, identifier.name) !== "valid-when-false") return false;
|
|
14835
|
+
const terminal = candidate2.consequent.type === import_utils78.AST_NODE_TYPES.BlockStatement ? candidate2.consequent.body.at(-1) : candidate2.consequent;
|
|
14836
|
+
return terminal?.type === import_utils78.AST_NODE_TYPES.ThrowStatement || terminal?.type === import_utils78.AST_NODE_TYPES.ReturnStatement;
|
|
14837
|
+
});
|
|
14838
|
+
};
|
|
14497
14839
|
return declared.length > 0 && declared.every(
|
|
14498
|
-
(variable) => variable.references.some(
|
|
14499
|
-
|
|
14500
|
-
|
|
14840
|
+
(variable) => variable.references.some((reference) => isValidationRead(reference.identifier)) && variable.references.every((reference) => {
|
|
14841
|
+
const identifier = reference.identifier;
|
|
14842
|
+
if (reference.init === true) return true;
|
|
14843
|
+
if (reference.isWrite() || identifier.type !== import_utils78.AST_NODE_TYPES.Identifier) return false;
|
|
14844
|
+
return isValidationRead(identifier) || isUseWithinValidatedBranch(identifier, identifier.name) || followsRejectingGuard(identifier);
|
|
14845
|
+
})
|
|
14501
14846
|
);
|
|
14502
14847
|
};
|
|
14503
14848
|
const trackInitializer = (declarator, scope) => {
|
|
@@ -14505,7 +14850,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14505
14850
|
const variable = declaredVars[0];
|
|
14506
14851
|
if (variable === void 0) return;
|
|
14507
14852
|
const localText = (candidate2) => localFileTextRef(candidate2, scope) !== null;
|
|
14508
|
-
if (isRawPayloadSource(declarator.init, localText)) {
|
|
14853
|
+
if (isRawPayloadSource(declarator.init, context, localText)) {
|
|
14509
14854
|
trackRawBinding(variable);
|
|
14510
14855
|
return;
|
|
14511
14856
|
}
|
|
@@ -14526,6 +14871,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14526
14871
|
if (node.id.type === import_utils78.AST_NODE_TYPES.ObjectPattern || node.id.type === import_utils78.AST_NODE_TYPES.ArrayPattern) {
|
|
14527
14872
|
if (isRawPayloadSource(
|
|
14528
14873
|
node.init,
|
|
14874
|
+
context,
|
|
14529
14875
|
(candidate2) => localFileTextRef(candidate2, scope) !== null
|
|
14530
14876
|
)) {
|
|
14531
14877
|
if (!isFullyNarrowedPattern(node)) {
|
|
@@ -14545,7 +14891,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14545
14891
|
if (variable === null) return;
|
|
14546
14892
|
const isLocalText = (candidate2) => localFileTextRef(candidate2, scope) !== null;
|
|
14547
14893
|
updateLocalFileText(variable, node.right, scope);
|
|
14548
|
-
if (isRawPayloadSource(node.right, isLocalText)) {
|
|
14894
|
+
if (isRawPayloadSource(node.right, context, isLocalText)) {
|
|
14549
14895
|
trackRawBinding(variable);
|
|
14550
14896
|
} else {
|
|
14551
14897
|
const source = unvalidatedVariableRef(node.right, scope, unvalidatedVariables);
|
|
@@ -14557,6 +14903,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14557
14903
|
if (node.left.type === import_utils78.AST_NODE_TYPES.ObjectPattern || node.left.type === import_utils78.AST_NODE_TYPES.ArrayPattern) {
|
|
14558
14904
|
if (isRawPayloadSource(
|
|
14559
14905
|
node.right,
|
|
14906
|
+
context,
|
|
14560
14907
|
(candidate2) => localFileTextRef(candidate2, scope) !== null
|
|
14561
14908
|
)) {
|
|
14562
14909
|
context.report({
|
|
@@ -14586,7 +14933,13 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14586
14933
|
continue;
|
|
14587
14934
|
}
|
|
14588
14935
|
const variable = findVariable2(scope, unwrapped.name);
|
|
14589
|
-
if (variable !== null)
|
|
14936
|
+
if (variable !== null) {
|
|
14937
|
+
for (const alias of aliasGroups.get(variable) ?? [variable]) {
|
|
14938
|
+
const guards = namedGuards.get(alias) ?? [];
|
|
14939
|
+
guards.push(node);
|
|
14940
|
+
namedGuards.set(alias, guards);
|
|
14941
|
+
}
|
|
14942
|
+
}
|
|
14590
14943
|
}
|
|
14591
14944
|
},
|
|
14592
14945
|
MemberExpression(node) {
|
|
@@ -14596,6 +14949,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14596
14949
|
const obj = unwrap6(node.object);
|
|
14597
14950
|
if (isRawPayloadSource(
|
|
14598
14951
|
obj,
|
|
14952
|
+
context,
|
|
14599
14953
|
(candidate2) => localFileTextRef(candidate2, scope) !== null
|
|
14600
14954
|
)) {
|
|
14601
14955
|
const parent = node.parent;
|
|
@@ -14607,6 +14961,7 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
14607
14961
|
}
|
|
14608
14962
|
const variable = obj?.type === import_utils78.AST_NODE_TYPES.Identifier ? unvalidatedVariableRef(obj, scope, unvalidatedVariables) : null;
|
|
14609
14963
|
if (variable !== null && obj?.type === import_utils78.AST_NODE_TYPES.Identifier) {
|
|
14964
|
+
if (namedGuards.get(variable)?.some((call) => guardDominates(node, call))) return;
|
|
14610
14965
|
if (isUseWithinValidatedBranch(node, obj.name)) {
|
|
14611
14966
|
return;
|
|
14612
14967
|
}
|
|
@@ -14632,7 +14987,7 @@ var PREFER_SHARED_ZOD_ENUM_DOCUMENTATION = {
|
|
|
14632
14987
|
rationale: "Inline or repeated literal domains hide a reusable contract and allow equivalent fields to drift independently.",
|
|
14633
14988
|
remediation: "Declare a module-level named Zod enum schema and reuse it at each field or contract site.",
|
|
14634
14989
|
category: "maintainability",
|
|
14635
|
-
limitations: ["Only direct z.enum calls with string-literal arrays are inspected; computed domains require review."],
|
|
14990
|
+
limitations: ["Only direct z.enum calls with string-literal arrays are inspected; computed domains require review. Equal values do not prove a shared business domain: retain local schemas when ownership, error customization, or future evolution differs, and review initialization order before extraction."],
|
|
14636
14991
|
examples: [
|
|
14637
14992
|
{ id: "shared-provider", title: "Reuse a named enum schema", outcome: "no-match", files: [{ path: "src/provider.ts", source: "import { z } from 'zod'; const ProviderSchema = z.enum(['agy', 'claude', 'sol']); const JobSchema = z.object({ provider: ProviderSchema }); const StatusSchema = z.object({ provider: ProviderSchema.optional() });" }], focusPath: "src/provider.ts", expectedCount: 0, public: true },
|
|
14638
14993
|
{ id: "inline-provider", title: "Do not inline enum domains in object fields", outcome: "match", files: [{ path: "src/provider.ts", source: "import { z } from 'zod'; const JobSchema = z.object({ provider: z.enum(['agy', 'claude', 'sol']) });" }], focusPath: "src/provider.ts", expectedCount: 1, public: true }
|
|
@@ -14674,16 +15029,22 @@ var prefer_shared_zod_enum_default = createRule({
|
|
|
14674
15029
|
create(context) {
|
|
14675
15030
|
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
14676
15031
|
const zodBindings = /* @__PURE__ */ new Set();
|
|
15032
|
+
const bindingOf = (node) => import_utils79.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
|
|
14677
15033
|
const seen = /* @__PURE__ */ new Set();
|
|
14678
15034
|
return {
|
|
14679
15035
|
ImportDeclaration(node) {
|
|
14680
15036
|
if (!isZodModule(node.source.value)) return;
|
|
14681
15037
|
for (const specifier of node.specifiers) {
|
|
14682
|
-
if (specifier.type === import_utils79.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils79.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils79.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils79.AST_NODE_TYPES.Identifier && specifier.imported.name === "z")
|
|
15038
|
+
if (specifier.type === import_utils79.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils79.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils79.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils79.AST_NODE_TYPES.Identifier && specifier.imported.name === "z") {
|
|
15039
|
+
const binding = bindingOf(specifier.local);
|
|
15040
|
+
if (binding !== null) zodBindings.add(binding);
|
|
15041
|
+
}
|
|
14683
15042
|
}
|
|
14684
15043
|
},
|
|
14685
15044
|
CallExpression(node) {
|
|
14686
|
-
if (node.callee.type !== import_utils79.AST_NODE_TYPES.MemberExpression || node.callee.computed || node.callee.object.type !== import_utils79.AST_NODE_TYPES.Identifier ||
|
|
15045
|
+
if (node.callee.type !== import_utils79.AST_NODE_TYPES.MemberExpression || node.callee.computed || node.callee.object.type !== import_utils79.AST_NODE_TYPES.Identifier || node.callee.property.type !== import_utils79.AST_NODE_TYPES.Identifier || node.callee.property.name !== "enum") return;
|
|
15046
|
+
const binding = bindingOf(node.callee.object);
|
|
15047
|
+
if (binding === null || !zodBindings.has(binding)) return;
|
|
14687
15048
|
const domain = literalDomain(node);
|
|
14688
15049
|
if (domain === null) return;
|
|
14689
15050
|
const key = JSON.stringify(domain);
|
|
@@ -14705,6 +15066,7 @@ var PREFER_SWITCH_FOR_REPEATED_EQUALITY_DOCUMENTATION = {
|
|
|
14705
15066
|
category: "maintainability",
|
|
14706
15067
|
limitations: [
|
|
14707
15068
|
"Only direct if/else-if chains with at least three strict-equality tests are reported.",
|
|
15069
|
+
"The discriminant must be a bare identifier; calls, getters, indexed reads and other repeatedly evaluated expressions are excluded. Review case-value effects, selector mutation, branch scoping, and break/continue targets when converting manually; this is not an equivalence proof.",
|
|
14708
15070
|
"Case values may be literals, enum-like member references, or upper-case named constants; dynamic expressions are excluded.",
|
|
14709
15071
|
"The rule deliberately ignores compound predicates, loose equality, ranges, and chains that compare different discriminants."
|
|
14710
15072
|
],
|
|
@@ -14718,7 +15080,8 @@ function discriminantText(sourceCode, test) {
|
|
|
14718
15080
|
const leftIsCase = isCaseValue(test.left);
|
|
14719
15081
|
const rightIsCase = isCaseValue(test.right);
|
|
14720
15082
|
if (leftIsCase === rightIsCase) return null;
|
|
14721
|
-
|
|
15083
|
+
const discriminant = leftIsCase ? test.right : test.left;
|
|
15084
|
+
return discriminant.type === import_utils80.AST_NODE_TYPES.Identifier ? sourceCode.getText(discriminant) : null;
|
|
14722
15085
|
}
|
|
14723
15086
|
function isCaseValue(node) {
|
|
14724
15087
|
if (node.type === import_utils80.AST_NODE_TYPES.Literal) return true;
|
|
@@ -15659,6 +16022,13 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
15659
16022
|
return null;
|
|
15660
16023
|
}
|
|
15661
16024
|
const actual = expectCall.arguments[0];
|
|
16025
|
+
const variable = import_utils83.ASTUtils.findVariable(sourceCode.getScope(expectCall.callee), expectCall.callee.name);
|
|
16026
|
+
if (variable !== null && variable.defs.some((definition) => {
|
|
16027
|
+
if (definition.node.type !== import_utils83.AST_NODE_TYPES.ImportSpecifier) return true;
|
|
16028
|
+
const declaration = definition.node.parent;
|
|
16029
|
+
const imported = definition.node.imported;
|
|
16030
|
+
return declaration.type !== import_utils83.AST_NODE_TYPES.ImportDeclaration || !["vitest", "@jest/globals", "@playwright/test", "bun:test"].includes(String(declaration.source.value)) || (imported.type === import_utils83.AST_NODE_TYPES.Identifier ? imported.name : imported.value) !== "expect";
|
|
16031
|
+
})) return null;
|
|
15662
16032
|
if (actual === void 0 || actual.type !== import_utils83.AST_NODE_TYPES.MemberExpression || actual.optional) {
|
|
15663
16033
|
return null;
|
|
15664
16034
|
}
|
|
@@ -15820,12 +16190,12 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
15820
16190
|
// src/rules/repeated-static-call-cases.ts
|
|
15821
16191
|
var import_utils84 = require("@typescript-eslint/utils");
|
|
15822
16192
|
var REPEATED_STATIC_CALL_CASES_DOCUMENTATION = {
|
|
15823
|
-
summary: "
|
|
15824
|
-
rationale: "Copy-pasted cases obscure the input table and stop later cases from being reported
|
|
15825
|
-
remediation: "
|
|
16193
|
+
summary: "Review three or more consecutive static-input call assertions as potential named cases.",
|
|
16194
|
+
rationale: "Copy-pasted cases obscure the input table, and a thrown assertion can stop later cases from being reported.",
|
|
16195
|
+
remediation: "If the calls are independent, use the runner's named parameterized cases or subtests. Preserve ordered state-transition scenarios as one test.",
|
|
15826
16196
|
category: "testing",
|
|
15827
16197
|
filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
15828
|
-
limitations: ["Only consecutive top-level assertions with direct calls and
|
|
16198
|
+
limitations: ["Only consecutive top-level assertions with direct calls and static inputs and expected values are checked. Test-local callees and fixtures are excluded; imported or outer functions can still be stateful, so manual independence review is required. Not every runner supports test.each."],
|
|
15829
16199
|
examples: [
|
|
15830
16200
|
{ id: "parameterized", title: "Name each case", outcome: "no-match", files: [{ path: "src/parser.test.ts", source: "test.each([['a', true], ['b', false], ['c', true]])('parses %s', (input, expected) => { expect(parse(input)).toBe(expected); });" }], focusPath: "src/parser.test.ts", expectedCount: 0, public: true },
|
|
15831
16201
|
{ id: "repeated", title: "Do not repeat literal cases", outcome: "match", files: [{ path: "src/parser.test.ts", source: "test('parses', () => { expect(parse('a')).toBe(true); expect(parse('b')).toBe(false); expect(parse('c')).toBe(true); });" }], focusPath: "src/parser.test.ts", expectedCount: 1, public: true }
|
|
@@ -15906,7 +16276,7 @@ function staticShape(node) {
|
|
|
15906
16276
|
return "dynamic";
|
|
15907
16277
|
}
|
|
15908
16278
|
}
|
|
15909
|
-
function assertionShape(statement, context) {
|
|
16279
|
+
function assertionShape(statement, context, callback) {
|
|
15910
16280
|
if (statement.type !== import_utils84.AST_NODE_TYPES.ExpressionStatement || statement.expression.type !== import_utils84.AST_NODE_TYPES.CallExpression) return null;
|
|
15911
16281
|
const matcherCall = statement.expression;
|
|
15912
16282
|
if (matcherCall.callee.type !== import_utils84.AST_NODE_TYPES.MemberExpression || matcherCall.callee.computed || matcherCall.callee.property.type !== import_utils84.AST_NODE_TYPES.Identifier || matcherCall.arguments.length !== 1) return null;
|
|
@@ -15918,6 +16288,10 @@ function assertionShape(statement, context) {
|
|
|
15918
16288
|
const expected = matcherCall.arguments[0];
|
|
15919
16289
|
if (observed?.type !== import_utils84.AST_NODE_TYPES.CallExpression || observed.callee.type !== import_utils84.AST_NODE_TYPES.Identifier || observed.arguments.length === 0 || observed.arguments.some((arg) => arg.type === import_utils84.AST_NODE_TYPES.SpreadElement || !isStatic(arg)) || expected?.type === import_utils84.AST_NODE_TYPES.SpreadElement || expected === void 0 || !isStatic(expected)) return null;
|
|
15920
16290
|
const skeleton = `${observed.callee.name}/${observed.arguments.map((item) => staticShape(item)).join(",")}/${chain.modifiers.join(".")}/${matcher}/${staticShape(expected)}`;
|
|
16291
|
+
const binding = import_utils84.ASTUtils.findVariable(context.sourceCode.getScope(observed.callee), observed.callee.name);
|
|
16292
|
+
if (binding?.defs.some(
|
|
16293
|
+
(definition) => definition.node.range[0] >= callback.range[0] && definition.node.range[1] <= callback.range[1]
|
|
16294
|
+
)) return null;
|
|
15921
16295
|
const values = [...observed.arguments, expected].map((item) => context.sourceCode.getText(item)).join("\0");
|
|
15922
16296
|
return { statement, skeleton, values };
|
|
15923
16297
|
}
|
|
@@ -15937,9 +16311,9 @@ var repeated_static_call_cases_default = createRule({
|
|
|
15937
16311
|
documentation: REPEATED_STATIC_CALL_CASES_DOCUMENTATION,
|
|
15938
16312
|
meta: {
|
|
15939
16313
|
type: "suggestion",
|
|
15940
|
-
docs: { description:
|
|
16314
|
+
docs: { description: REPEATED_STATIC_CALL_CASES_DOCUMENTATION.summary },
|
|
15941
16315
|
schema: [],
|
|
15942
|
-
messages: { repeatedStaticCallCases: "These {{count}} consecutive assertions repeat
|
|
16316
|
+
messages: { repeatedStaticCallCases: "These {{count}} consecutive assertions repeat a call with static inputs. If independent, use named parameterized cases or subtests; preserve ordered scenarios as one test." }
|
|
15943
16317
|
},
|
|
15944
16318
|
defaultOptions: [],
|
|
15945
16319
|
create(context) {
|
|
@@ -15974,7 +16348,7 @@ var repeated_static_call_cases_default = createRule({
|
|
|
15974
16348
|
run = [];
|
|
15975
16349
|
};
|
|
15976
16350
|
for (const statement of node.body.body) {
|
|
15977
|
-
const shape = assertionShape(statement, context);
|
|
16351
|
+
const shape = assertionShape(statement, context, node);
|
|
15978
16352
|
if (shape === null || run.length > 0 && run[0]?.skeleton !== shape.skeleton) flush();
|
|
15979
16353
|
if (shape !== null) run.push(shape);
|
|
15980
16354
|
}
|
|
@@ -16007,6 +16381,11 @@ var PREFER_ZOD_INFER_DOCUMENTATION = {
|
|
|
16007
16381
|
rationale: "A derived type stays synchronized when the runtime schema changes.",
|
|
16008
16382
|
remediation: "Replace the hand-written twin with `z.infer<typeof Schema>`.",
|
|
16009
16383
|
category: "correctness",
|
|
16384
|
+
limitations: [
|
|
16385
|
+
"Only module-level const schemas and module-level type declarations are paired; local declarations are excluded rather than matched by spelling across scopes.",
|
|
16386
|
+
"By default every field must positively agree; collection, nested-object and referenced-schema equivalence is not inferred from outer syntax alone.",
|
|
16387
|
+
"This is a bounded syntactic comparison, not general type equivalence. Review schema input versus output, interface augmentation, and separately evolving domain contracts before replacing a declaration."
|
|
16388
|
+
],
|
|
16010
16389
|
examples: [
|
|
16011
16390
|
{ id: "inferred-type", title: "Infer the schema type", outcome: "no-match", files: [{ path: "src/user.ts", source: 'import { z } from "zod"; const UserSchema = z.object({ id: z.string() }); type User = z.infer<typeof UserSchema>;' }], focusPath: "src/user.ts", expectedCount: 0, public: true },
|
|
16012
16391
|
{ id: "handwritten-twin", title: "Do not duplicate the schema shape", outcome: "match", files: [{ path: "src/user.ts", source: 'import { z } from "zod"; const UserSchema = z.object({ id: z.string() }); interface User { id: string }' }], focusPath: "src/user.ts", expectedCount: 1, public: true }
|
|
@@ -16186,6 +16565,10 @@ function sameDomain(left, right) {
|
|
|
16186
16565
|
function isExportedDeclaration(node) {
|
|
16187
16566
|
return node.parent?.type === import_utils85.AST_NODE_TYPES.ExportNamedDeclaration;
|
|
16188
16567
|
}
|
|
16568
|
+
function isModuleLevelDeclaration(node) {
|
|
16569
|
+
const parent = node.parent;
|
|
16570
|
+
return parent?.type === import_utils85.AST_NODE_TYPES.Program || parent?.type === import_utils85.AST_NODE_TYPES.ExportNamedDeclaration && parent.parent.type === import_utils85.AST_NODE_TYPES.Program;
|
|
16571
|
+
}
|
|
16189
16572
|
function isModuleLevelConst(node) {
|
|
16190
16573
|
const declaration = node.parent;
|
|
16191
16574
|
if (declaration.type !== import_utils85.AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const") {
|
|
@@ -16236,6 +16619,9 @@ function leafAgrees(field, annotation) {
|
|
|
16236
16619
|
return annotationDomain !== null && sameDomain(field.domain, annotationDomain);
|
|
16237
16620
|
}
|
|
16238
16621
|
const { leaf } = field;
|
|
16622
|
+
if (leaf !== null && ["array", "tuple", "object", "strictObject", "looseObject", "record", "map", "set", "promise", "intersection"].includes(leaf)) {
|
|
16623
|
+
return false;
|
|
16624
|
+
}
|
|
16239
16625
|
if (leaf === null || annotation === null) {
|
|
16240
16626
|
return null;
|
|
16241
16627
|
}
|
|
@@ -16528,7 +16914,6 @@ var prefer_zod_infer_default = createRule({
|
|
|
16528
16914
|
if (fields.size !== members.size) {
|
|
16529
16915
|
return false;
|
|
16530
16916
|
}
|
|
16531
|
-
let agreements = 0;
|
|
16532
16917
|
for (const [name, field] of fields) {
|
|
16533
16918
|
const member = members.get(name);
|
|
16534
16919
|
if (member === void 0) {
|
|
@@ -16547,14 +16932,11 @@ var prefer_zod_infer_default = createRule({
|
|
|
16547
16932
|
return false;
|
|
16548
16933
|
}
|
|
16549
16934
|
const agrees = leafAgrees(field, member.annotation);
|
|
16550
|
-
if (agrees
|
|
16935
|
+
if (agrees !== true) {
|
|
16551
16936
|
return false;
|
|
16552
16937
|
}
|
|
16553
|
-
if (agrees === true) {
|
|
16554
|
-
agreements += 1;
|
|
16555
|
-
}
|
|
16556
16938
|
}
|
|
16557
|
-
return
|
|
16939
|
+
return true;
|
|
16558
16940
|
}
|
|
16559
16941
|
return {
|
|
16560
16942
|
Program(node) {
|
|
@@ -16568,7 +16950,7 @@ var prefer_zod_infer_default = createRule({
|
|
|
16568
16950
|
recordZodImport(node);
|
|
16569
16951
|
},
|
|
16570
16952
|
VariableDeclarator(node) {
|
|
16571
|
-
if (node.id.type !== import_utils85.AST_NODE_TYPES.Identifier || node.init == null) {
|
|
16953
|
+
if (node.id.type !== import_utils85.AST_NODE_TYPES.Identifier || node.init == null || !isModuleLevelConst(node)) {
|
|
16572
16954
|
return;
|
|
16573
16955
|
}
|
|
16574
16956
|
const fields = schemaFields(node.init);
|
|
@@ -16601,6 +16983,7 @@ var prefer_zod_infer_default = createRule({
|
|
|
16601
16983
|
}
|
|
16602
16984
|
},
|
|
16603
16985
|
TSInterfaceDeclaration(node) {
|
|
16986
|
+
if (!isModuleLevelDeclaration(node)) return;
|
|
16604
16987
|
if (node.typeParameters !== void 0 || (node.extends?.length ?? 0) > 0) {
|
|
16605
16988
|
return;
|
|
16606
16989
|
}
|
|
@@ -16616,6 +16999,7 @@ var prefer_zod_infer_default = createRule({
|
|
|
16616
16999
|
);
|
|
16617
17000
|
},
|
|
16618
17001
|
TSTypeAliasDeclaration(node) {
|
|
17002
|
+
if (!isModuleLevelDeclaration(node)) return;
|
|
16619
17003
|
const schemaName = inferredSchemaName(node.typeAnnotation);
|
|
16620
17004
|
if (schemaName !== null) {
|
|
16621
17005
|
inferredAliases.push({
|
|
@@ -16730,6 +17114,7 @@ var REQUIRE_ASSERT_NEVER_DOCUMENTATION = {
|
|
|
16730
17114
|
rationale: "An empty default silently accepts new union members instead of making the compiler identify the missing case.",
|
|
16731
17115
|
remediation: "Call `assertNever` with the discriminant in the exhaustive switch default.",
|
|
16732
17116
|
category: "correctness",
|
|
17117
|
+
limitations: ["Requires type information and singleton case values covering the current union. The helper must accept never to provide a compile-time check; its spelling alone is not a proof of that contract. Review the desired runtime behavior for unexpected external values."],
|
|
16733
17118
|
examples: [
|
|
16734
17119
|
{ id: "assert-never-default", title: "Make the default exhaustive", outcome: "no-match", files: [{ path: "src/render.ts", source: "declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: assertNever(kind); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
|
|
16735
17120
|
{ id: "empty-default", title: "Do not leave an exhaustive default empty", outcome: "match", files: [{ path: "src/render.ts", source: "declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: }" }], focusPath: "src/render.ts", expectedCount: 1, public: true }
|
|
@@ -16786,11 +17171,9 @@ function isExhaustiveFiniteSwitch(node, services) {
|
|
|
16786
17171
|
if (caseNode.test === null) continue;
|
|
16787
17172
|
const test = services.esTreeNodeToTSNodeMap.get(caseNode.test);
|
|
16788
17173
|
const testType = checker.getTypeAtLocation(test);
|
|
16789
|
-
|
|
16790
|
-
|
|
16791
|
-
|
|
16792
|
-
if (key !== null) handled.add(key);
|
|
16793
|
-
}
|
|
17174
|
+
if (testType.isUnion()) continue;
|
|
17175
|
+
const key = finiteTypeKey(testType, checker);
|
|
17176
|
+
if (key !== null) handled.add(key);
|
|
16794
17177
|
}
|
|
16795
17178
|
return [...expected].every((key) => handled.has(key));
|
|
16796
17179
|
}
|
|
@@ -16984,10 +17367,12 @@ var require_fetch_timeout_default = createRule({
|
|
|
16984
17367
|
var import_utils88 = require("@typescript-eslint/utils");
|
|
16985
17368
|
var REQUIRE_INTERFACE_FOR_EXPORTED_CLASS_DOCUMENTATION = {
|
|
16986
17369
|
summary: "Require exported concrete classes with public behavior to declare a contract.",
|
|
16987
|
-
rationale: "
|
|
17370
|
+
rationale: "An explicit contract names the intended public capability separately from implementation details. TypeScript already supports structural compatibility; this is an architecture policy, not a prerequisite for substitution.",
|
|
16988
17371
|
remediation: "Declare a focused interface and add an implements clause, or inherit from an intentional base contract.",
|
|
16989
17372
|
category: "architecture",
|
|
16990
17373
|
limitations: [
|
|
17374
|
+
"JavaScript files are excluded because implements is TypeScript syntax; imported framework base contracts still require manual policy review.",
|
|
17375
|
+
"An exported injected service may also receive require-port-for-service: its service-boundary error and this general exported-contract warning intentionally enforce distinct policy scopes.",
|
|
16991
17376
|
"The warning-stage rule checks module-level class declarations and direct class-expression values exported directly, through local export specifiers, or through a default identifier; re-exports and expressions wrapped in other calls require review.",
|
|
16992
17377
|
"An extends clause satisfies the contract only when its target is a locally declared abstract class; imported base-class contracts require an explicit implements clause.",
|
|
16993
17378
|
"Static factories and data-only classes without public instance methods are outside the contract requirement."
|
|
@@ -17059,7 +17444,7 @@ var require_interface_for_exported_class_default = createRule({
|
|
|
17059
17444
|
},
|
|
17060
17445
|
defaultOptions: [],
|
|
17061
17446
|
create(context) {
|
|
17062
|
-
if (isTestFile(context.filename) || isStoryFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
17447
|
+
if (/\.(?:js|jsx|mjs|cjs)$/iu.test(context.filename) || isTestFile(context.filename) || isStoryFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
17063
17448
|
return {
|
|
17064
17449
|
"Program:exit"(program) {
|
|
17065
17450
|
const classes = /* @__PURE__ */ new Map();
|
|
@@ -17445,15 +17830,14 @@ var publicMethodNames = (body2, functionAliases) => {
|
|
|
17445
17830
|
if (member.static || member.accessibility === "private" || member.accessibility === "protected") continue;
|
|
17446
17831
|
if (member.key.type === import_utils89.AST_NODE_TYPES.PrivateIdentifier) continue;
|
|
17447
17832
|
if (member.value?.type !== import_utils89.AST_NODE_TYPES.ArrowFunctionExpression && member.value?.type !== import_utils89.AST_NODE_TYPES.FunctionExpression && member.typeAnnotation?.typeAnnotation.type !== import_utils89.AST_NODE_TYPES.TSFunctionType && !(member.typeAnnotation?.typeAnnotation.type === import_utils89.AST_NODE_TYPES.TSTypeReference && member.typeAnnotation.typeAnnotation.typeName.type === import_utils89.AST_NODE_TYPES.Identifier && functionAliases.has(member.typeAnnotation.typeAnnotation.typeName.name))) continue;
|
|
17448
|
-
names.push(member
|
|
17833
|
+
names.push(declaredMemberName(member) ?? "\u2026");
|
|
17449
17834
|
continue;
|
|
17450
17835
|
}
|
|
17451
17836
|
if (member.type !== import_utils89.AST_NODE_TYPES.MethodDefinition) continue;
|
|
17452
17837
|
if (member.kind !== "method" || member.static) continue;
|
|
17453
17838
|
if (member.accessibility === "private" || member.accessibility === "protected") continue;
|
|
17454
17839
|
if (member.key.type === import_utils89.AST_NODE_TYPES.PrivateIdentifier) continue;
|
|
17455
|
-
|
|
17456
|
-
else names.push("\u2026");
|
|
17840
|
+
names.push(declaredMemberName(member) ?? "\u2026");
|
|
17457
17841
|
}
|
|
17458
17842
|
return names;
|
|
17459
17843
|
};
|
|
@@ -17494,6 +17878,11 @@ function localClassAbstractness(program) {
|
|
|
17494
17878
|
}
|
|
17495
17879
|
return classes;
|
|
17496
17880
|
}
|
|
17881
|
+
function declaredMemberName(member) {
|
|
17882
|
+
if (!member.computed && member.key.type === import_utils89.AST_NODE_TYPES.Identifier) return member.key.name;
|
|
17883
|
+
if (member.key.type === import_utils89.AST_NODE_TYPES.Literal && typeof member.key.value === "string") return member.key.value;
|
|
17884
|
+
return null;
|
|
17885
|
+
}
|
|
17497
17886
|
function localInterfaceSurfaces(program) {
|
|
17498
17887
|
const interfaces = /* @__PURE__ */ new Map();
|
|
17499
17888
|
const parents = /* @__PURE__ */ new Map();
|
|
@@ -17516,14 +17905,15 @@ function localInterfaceSurfaces(program) {
|
|
|
17516
17905
|
if (part.type !== import_utils89.AST_NODE_TYPES.TSTypeLiteral) continue;
|
|
17517
17906
|
for (const member of part.members) {
|
|
17518
17907
|
if (member.type !== import_utils89.AST_NODE_TYPES.TSMethodSignature && member.type !== import_utils89.AST_NODE_TYPES.TSPropertySignature) continue;
|
|
17519
|
-
|
|
17908
|
+
const name = declaredMemberName(member);
|
|
17909
|
+
if (name === null) continue;
|
|
17520
17910
|
if (member.type === import_utils89.AST_NODE_TYPES.TSMethodSignature) {
|
|
17521
|
-
callables2.add(
|
|
17911
|
+
callables2.add(name);
|
|
17522
17912
|
continue;
|
|
17523
17913
|
}
|
|
17524
17914
|
if (member.type !== import_utils89.AST_NODE_TYPES.TSPropertySignature) continue;
|
|
17525
17915
|
const annotation = member.typeAnnotation?.typeAnnotation;
|
|
17526
|
-
if (annotation?.type === import_utils89.AST_NODE_TYPES.TSFunctionType || annotation?.type === import_utils89.AST_NODE_TYPES.TSTypeReference && annotation.typeName.type === import_utils89.AST_NODE_TYPES.Identifier && functionAliases.has(annotation.typeName.name)) callables2.add(
|
|
17916
|
+
if (annotation?.type === import_utils89.AST_NODE_TYPES.TSFunctionType || annotation?.type === import_utils89.AST_NODE_TYPES.TSTypeReference && annotation.typeName.type === import_utils89.AST_NODE_TYPES.Identifier && functionAliases.has(annotation.typeName.name)) callables2.add(name);
|
|
17527
17917
|
}
|
|
17528
17918
|
}
|
|
17529
17919
|
interfaces.set(declaration.id.name, callables2);
|
|
@@ -17534,8 +17924,9 @@ function localInterfaceSurfaces(program) {
|
|
|
17534
17924
|
const callables = interfaces.get(declaration.id.name) ?? /* @__PURE__ */ new Set();
|
|
17535
17925
|
for (const member of declaration.body.body) {
|
|
17536
17926
|
if (member.type !== import_utils89.AST_NODE_TYPES.TSMethodSignature && member.type !== import_utils89.AST_NODE_TYPES.TSPropertySignature) continue;
|
|
17537
|
-
|
|
17538
|
-
if (
|
|
17927
|
+
const name = declaredMemberName(member);
|
|
17928
|
+
if (name === null) continue;
|
|
17929
|
+
if (member.type === import_utils89.AST_NODE_TYPES.TSMethodSignature || member.typeAnnotation?.typeAnnotation.type === import_utils89.AST_NODE_TYPES.TSFunctionType || member.typeAnnotation?.typeAnnotation.type === import_utils89.AST_NODE_TYPES.TSTypeReference && member.typeAnnotation.typeAnnotation.typeName.type === import_utils89.AST_NODE_TYPES.Identifier && functionAliases.has(member.typeAnnotation.typeAnnotation.typeName.name)) callables.add(name);
|
|
17539
17930
|
}
|
|
17540
17931
|
interfaces.set(declaration.id.name, callables);
|
|
17541
17932
|
parents.set(
|
|
@@ -17714,12 +18105,13 @@ var MODEL_EXECUTION_METHODS = /* @__PURE__ */ new Set([
|
|
|
17714
18105
|
var DATABASE_NAMES = /^(?:db|database|connection|pool|prisma|query|transaction|tx)$/iu;
|
|
17715
18106
|
var REQUIRE_SQL_ACCESS_CLASS_DOCUMENTATION = {
|
|
17716
18107
|
summary: "Keep SQL reads and writes inside a class that receives its database dependency.",
|
|
17717
|
-
rationale: "
|
|
18108
|
+
rationale: "An injected repository class is the preferred ownership boundary for database access under this architectural policy; free functions can also express explicit dependencies.",
|
|
17718
18109
|
remediation: "Move the query into a repository or store class and inject the pool, connection, transaction, or typed database binding through its constructor.",
|
|
17719
18110
|
category: "architecture",
|
|
17720
18111
|
limitations: [
|
|
17721
18112
|
"The rule recognizes conventional database receiver names, Cloudflare DB bindings, direct pool.query calls, explicit query-builder terminals, and Prisma-style model delegates; unusually named or heavily aliased clients require architectural review.",
|
|
17722
18113
|
"Query construction without a recognized execution terminal is intentionally not reported.",
|
|
18114
|
+
"Stable local Map, WeakMap, and URLSearchParams instances are excluded. Other conventional receiver names are heuristics, not proof of a database API.",
|
|
17723
18115
|
"Constructor injection inherited from a base class or transformed through a wrapper is not inferred by this syntax-only rule."
|
|
17724
18116
|
],
|
|
17725
18117
|
examples: [
|
|
@@ -17925,11 +18317,23 @@ var require_sql_access_class_default = createRule({
|
|
|
17925
18317
|
create(context) {
|
|
17926
18318
|
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text))
|
|
17927
18319
|
return {};
|
|
18320
|
+
function knownNonDatabase(node, seen = /* @__PURE__ */ new Set()) {
|
|
18321
|
+
if (seen.has(node)) return false;
|
|
18322
|
+
seen.add(node);
|
|
18323
|
+
if (node.type === import_utils90.AST_NODE_TYPES.Identifier) {
|
|
18324
|
+
const binding = import_utils90.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
|
|
18325
|
+
if (binding?.defs.length !== 1 || binding.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
18326
|
+
const definition = binding.defs[0];
|
|
18327
|
+
return definition?.type === "Variable" && definition.node.init !== null && knownNonDatabase(definition.node.init, seen);
|
|
18328
|
+
}
|
|
18329
|
+
return node.type === import_utils90.AST_NODE_TYPES.NewExpression && node.callee.type === import_utils90.AST_NODE_TYPES.Identifier && ["Map", "WeakMap", "URLSearchParams"].includes(node.callee.name) && (import_utils90.ASTUtils.findVariable(context.sourceCode.getScope(node.callee), node.callee.name)?.defs.length ?? 0) === 0;
|
|
18330
|
+
}
|
|
17928
18331
|
return {
|
|
17929
18332
|
CallExpression(node) {
|
|
17930
18333
|
if (node.callee.type !== import_utils90.AST_NODE_TYPES.MemberExpression)
|
|
17931
18334
|
return;
|
|
17932
18335
|
const method = memberName6(node.callee);
|
|
18336
|
+
if (knownNonDatabase(node.callee.object)) return;
|
|
17933
18337
|
if (method === null || !isDatabaseOperation(method, node.callee.object))
|
|
17934
18338
|
return;
|
|
17935
18339
|
const owner = owningClass2(node);
|
|
@@ -17950,6 +18354,8 @@ var REQUIRE_STATIC_NEXT_MATCHER_DOCUMENTATION = {
|
|
|
17950
18354
|
rationale: "Next.js must statically analyze matcher values at build time; computed values are ignored.",
|
|
17951
18355
|
remediation: "Write matcher strings, arrays, and object fields as literals in the exported config.",
|
|
17952
18356
|
category: "correctness",
|
|
18357
|
+
limitations: ["Only directly exported matcher configuration in middleware/proxy entry files is inspected. Literal matcher validity is left to Next.js; this rule checks static syntax, not the complete framework schema."],
|
|
18358
|
+
references: ["https://nextjs.org/docs/app/api-reference/file-conventions/proxy"],
|
|
17953
18359
|
examples: [
|
|
17954
18360
|
{ id: "literal-matcher", title: "Use a literal matcher", outcome: "no-match", files: [{ path: "src/middleware.ts", source: 'export const config = { matcher: "/api/:path*" };' }], focusPath: "src/middleware.ts", expectedCount: 0, public: true },
|
|
17955
18361
|
{ id: "computed-matcher", title: "Do not compute the matcher", outcome: "match", files: [{ path: "src/middleware.ts", source: 'const matcher = "/api/:path*"; export const config = { matcher };' }], focusPath: "src/middleware.ts", expectedCount: 1, public: true }
|
|
@@ -17997,7 +18403,7 @@ var require_static_next_matcher_default = createRule({
|
|
|
17997
18403
|
},
|
|
17998
18404
|
schema: [],
|
|
17999
18405
|
messages: {
|
|
18000
|
-
dynamicMatcher: "Next.js matcher
|
|
18406
|
+
dynamicMatcher: "Keep Next.js matcher strings, arrays and object fields literal in the exported config. Dynamic values such as variables are not supported by its build-time static analysis and can be ignored."
|
|
18001
18407
|
}
|
|
18002
18408
|
},
|
|
18003
18409
|
defaultOptions: [],
|
|
@@ -18178,7 +18584,8 @@ var REQUIRE_ZOD_FORM_VALIDATION_DOCUMENTATION = {
|
|
|
18178
18584
|
category: "security",
|
|
18179
18585
|
limitations: [
|
|
18180
18586
|
"Tests are excluded; imported schema-shaped names are trusted when their implementation is outside the linted file.",
|
|
18181
|
-
"Delayed raw-value use is accepted only after an unconditional successful parse in the same block; safeParse remains valid when the raw binding has no unvalidated consumer."
|
|
18587
|
+
"Delayed raw-value use is accepted only after an unconditional successful parse in the same block; safeParse remains valid when the raw binding has no unvalidated consumer.",
|
|
18588
|
+
"An enclosing parse does not validate an earlier call or deferred callback that consumes its input. Direct object/array construction, unshadowed Object.fromEntries and native Number/String/Boolean coercion remain supported; arbitrary preprocessing needs an explicitly reviewed boundary."
|
|
18182
18589
|
],
|
|
18183
18590
|
examples: [
|
|
18184
18591
|
{ id: "validated-form-value", title: "Validate the form value", outcome: "no-match", files: [{ path: "src/action.ts", source: "const input = UserSchema.parse({ name: formData.get('name') });" }], focusPath: "src/action.ts", expectedCount: 0, public: true },
|
|
@@ -18293,6 +18700,15 @@ var require_zod_form_validation_default = createRule({
|
|
|
18293
18700
|
let parent = node.parent;
|
|
18294
18701
|
while (parent !== null && parent !== void 0) {
|
|
18295
18702
|
if (isZodParseCall(parent)) return parent;
|
|
18703
|
+
if (parent.type === import_utils94.AST_NODE_TYPES.CallExpression && parent.callee.type === import_utils94.AST_NODE_TYPES.Identifier && ["Number", "String", "Boolean"].includes(parent.callee.name) && parent.arguments.length === 1 && (resolvedBinding(parent.callee)?.defs.length ?? 0) === 0) {
|
|
18704
|
+
parent = parent.parent;
|
|
18705
|
+
continue;
|
|
18706
|
+
}
|
|
18707
|
+
if (parent.type === import_utils94.AST_NODE_TYPES.CallExpression && parent.callee.type === import_utils94.AST_NODE_TYPES.MemberExpression && !parent.callee.computed && parent.callee.object.type === import_utils94.AST_NODE_TYPES.Identifier && parent.callee.object.name === "Object" && parent.callee.property.type === import_utils94.AST_NODE_TYPES.Identifier && parent.callee.property.name === "fromEntries" && (resolvedBinding(parent.callee.object)?.defs.length ?? 0) === 0) {
|
|
18708
|
+
parent = parent.parent;
|
|
18709
|
+
continue;
|
|
18710
|
+
}
|
|
18711
|
+
if (parent.type === import_utils94.AST_NODE_TYPES.CallExpression || parent.type === import_utils94.AST_NODE_TYPES.NewExpression || parent.type === import_utils94.AST_NODE_TYPES.TaggedTemplateExpression || parent.type === import_utils94.AST_NODE_TYPES.ArrowFunctionExpression || parent.type === import_utils94.AST_NODE_TYPES.FunctionExpression || parent.type === import_utils94.AST_NODE_TYPES.FunctionDeclaration) return null;
|
|
18296
18712
|
parent = parent.parent;
|
|
18297
18713
|
}
|
|
18298
18714
|
return null;
|
|
@@ -18472,13 +18888,14 @@ var require_zod_form_validation_default = createRule({
|
|
|
18472
18888
|
// src/rules/store-insert-requires-on-conflict.ts
|
|
18473
18889
|
var import_utils95 = require("@typescript-eslint/utils");
|
|
18474
18890
|
var STORE_INSERT_REQUIRES_ON_CONFLICT_DOCUMENTATION = {
|
|
18475
|
-
summary: "
|
|
18476
|
-
rationale: "
|
|
18477
|
-
remediation: "
|
|
18891
|
+
summary: "Review conflict handling for embedded inserts in replay-named callables.",
|
|
18892
|
+
rationale: "Names such as seed, enqueue, or upsert suggest that repeated execution deserves a conflict-policy review, but do not prove a replay contract.",
|
|
18893
|
+
remediation: "Choose conflict handling appropriate to the schema and SQL dialect, or document why this insertion must fail on a duplicate.",
|
|
18478
18894
|
category: "correctness",
|
|
18895
|
+
limitations: ["Only the nearest statically named callable is considered; top-level inserts and anonymous callbacks are excluded. Recognized conflict syntax does not prove idempotence or concurrency safety: WHERE NOT EXISTS can race, and INSERT OR REPLACE can delete an existing row. Review unique constraints and dialect semantics manually."],
|
|
18479
18896
|
examples: [
|
|
18480
|
-
{ id: "conflict-safe-insert", title: "
|
|
18481
|
-
{ id: "bare-insert", title: "
|
|
18897
|
+
{ id: "conflict-safe-insert", title: "Review the conflict policy for a replayed insert", outcome: "no-match", files: [{ path: "src/store.ts", source: "function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?) ON CONFLICT(id) DO NOTHING`).run(); }" }], focusPath: "src/store.ts", expectedCount: 0, public: true },
|
|
18898
|
+
{ id: "bare-insert", title: "Review a bare insert in a replay-named callable", outcome: "match", files: [{ path: "src/store.ts", source: "function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?)`).run(); }" }], focusPath: "src/store.ts", expectedCount: 1, public: true }
|
|
18482
18899
|
]
|
|
18483
18900
|
};
|
|
18484
18901
|
var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
|
|
@@ -18490,14 +18907,18 @@ function owningCallableName(node) {
|
|
|
18490
18907
|
return current.id?.name ?? null;
|
|
18491
18908
|
}
|
|
18492
18909
|
if (current.type === "MethodDefinition") {
|
|
18493
|
-
return current.key.type === "Identifier" ? current.key.name : null;
|
|
18910
|
+
return !current.computed && current.key.type === "Identifier" ? current.key.name : null;
|
|
18494
18911
|
}
|
|
18495
18912
|
if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "VariableDeclarator" && current.parent.id.type === "Identifier") {
|
|
18496
18913
|
return current.parent.id.name;
|
|
18497
18914
|
}
|
|
18498
|
-
if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "Property" && current.parent.key.type === "Identifier") {
|
|
18915
|
+
if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "Property" && !current.parent.computed && current.parent.key.type === "Identifier") {
|
|
18499
18916
|
return current.parent.key.name;
|
|
18500
18917
|
}
|
|
18918
|
+
if (current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") {
|
|
18919
|
+
const parent = current.parent;
|
|
18920
|
+
return parent.type === "MethodDefinition" && !parent.computed && parent.key.type === "Identifier" ? parent.key.name : null;
|
|
18921
|
+
}
|
|
18501
18922
|
}
|
|
18502
18923
|
return null;
|
|
18503
18924
|
}
|
|
@@ -18508,11 +18929,11 @@ var store_insert_requires_on_conflict_default = createRule({
|
|
|
18508
18929
|
meta: {
|
|
18509
18930
|
type: "problem",
|
|
18510
18931
|
docs: {
|
|
18511
|
-
description: "
|
|
18932
|
+
description: "Review conflict handling for embedded inserts in replay-named callables."
|
|
18512
18933
|
},
|
|
18513
18934
|
schema: [],
|
|
18514
18935
|
messages: {
|
|
18515
|
-
storeInsertRequiresOnConflict: "This INSERT is
|
|
18936
|
+
storeInsertRequiresOnConflict: "This INSERT is inside a replay-named callable without recognized conflict handling. Review duplicate execution, unique constraints, and the appropriate conflict policy for your SQL dialect."
|
|
18516
18937
|
}
|
|
18517
18938
|
},
|
|
18518
18939
|
defaultOptions: [],
|
|
@@ -18525,7 +18946,7 @@ var store_insert_requires_on_conflict_default = createRule({
|
|
|
18525
18946
|
return;
|
|
18526
18947
|
}
|
|
18527
18948
|
const owner = owningCallableName(node);
|
|
18528
|
-
if (owner
|
|
18949
|
+
if (owner === null || !REPLAY_CONTRACT_NAME.test(owner)) {
|
|
18529
18950
|
return;
|
|
18530
18951
|
}
|
|
18531
18952
|
context.report({ node, messageId: "storeInsertRequiresOnConflict" });
|
|
@@ -18538,15 +18959,13 @@ var import_utils96 = require("@typescript-eslint/utils");
|
|
|
18538
18959
|
var STEPDOWN_DOCUMENTATION = {
|
|
18539
18960
|
summary: "Place a private helper below its sole direct same-scope caller.",
|
|
18540
18961
|
rationale: "Caller-first ordering lets a reader follow the main flow before descending into implementation details.",
|
|
18541
|
-
remediation: "
|
|
18962
|
+
remediation: "Consider moving the private helper below its sole caller after reviewing initialization and reflection dependencies.",
|
|
18542
18963
|
category: "maintainability",
|
|
18543
|
-
autofix: "safe",
|
|
18544
18964
|
limitations: [
|
|
18545
18965
|
"Generated and test files, cycles, dynamic references, overload targets, and helpers with multiple callers are excluded.",
|
|
18546
|
-
"Class helpers must be private; their sole caller may be public, protected, or private
|
|
18966
|
+
"Class helpers must be private; their sole caller may be public, protected, or private.",
|
|
18547
18967
|
"Runtime class-field, static-block, computed-member, and decorator barriers are never crossed.",
|
|
18548
|
-
"
|
|
18549
|
-
"Overlapping helper chains remain report-only so ESLint never leaves a partially reordered class after exhausting its fix-pass limit."
|
|
18968
|
+
"This rule is report-only: reordering methods can change reflective property order, and moving module declarations can change initialization behavior or introduce temporal-dead-zone failures. Review module cycles and eager callers manually."
|
|
18550
18969
|
],
|
|
18551
18970
|
examples: [
|
|
18552
18971
|
{ id: "caller-before-helper", title: "Place the caller first", outcome: "no-match", files: [{ path: "src/run.ts", source: "function run() { return load(); }\nfunction load() { return 1; }" }], focusPath: "src/run.ts", expectedCount: 0, public: true },
|
|
@@ -18556,7 +18975,7 @@ var STEPDOWN_DOCUMENTATION = {
|
|
|
18556
18975
|
function isFunction(node) {
|
|
18557
18976
|
return node.type === import_utils96.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils96.AST_NODE_TYPES.FunctionDeclaration || node.type === import_utils96.AST_NODE_TYPES.FunctionExpression;
|
|
18558
18977
|
}
|
|
18559
|
-
function reportMisordered(context, candidates, scopeDefinitions, calls, pinned, canMove = () => true
|
|
18978
|
+
function reportMisordered(context, candidates, scopeDefinitions, calls, pinned, canMove = () => true) {
|
|
18560
18979
|
const byName = new Map(scopeDefinitions.map((definition) => [definition.name, definition]));
|
|
18561
18980
|
const cycles = cycleComponents(calls);
|
|
18562
18981
|
const callers = /* @__PURE__ */ new Map();
|
|
@@ -18575,12 +18994,10 @@ function reportMisordered(context, candidates, scopeDefinitions, calls, pinned,
|
|
|
18575
18994
|
if (callerName2 === void 0 || cycles.has(helper.name) && cycles.get(helper.name) === cycles.get(callerName2)) continue;
|
|
18576
18995
|
const caller = byName.get(callerName2);
|
|
18577
18996
|
if (caller === void 0 || helper.node.range[0] >= caller.node.range[0] || !canMove(helper, caller)) continue;
|
|
18578
|
-
const fix = makeFix?.(helper, caller);
|
|
18579
18997
|
context.report({
|
|
18580
18998
|
node: helper.node,
|
|
18581
18999
|
messageId: "helperAboveOnlyCaller",
|
|
18582
|
-
data: { helper: helper.name, caller: callerName2 }
|
|
18583
|
-
...fix === void 0 ? {} : { fix }
|
|
19000
|
+
data: { helper: helper.name, caller: callerName2 }
|
|
18584
19001
|
});
|
|
18585
19002
|
}
|
|
18586
19003
|
}
|
|
@@ -18751,15 +19168,15 @@ function referencedPropertyName(node) {
|
|
|
18751
19168
|
if (!node.computed && node.property.type === import_utils96.AST_NODE_TYPES.Identifier) return node.property.name;
|
|
18752
19169
|
return node.computed && node.property.type === import_utils96.AST_NODE_TYPES.Literal && typeof node.property.value === "string" ? node.property.value : null;
|
|
18753
19170
|
}
|
|
18754
|
-
function
|
|
19171
|
+
function walk(node, visitorKeys, visit, nestedFunction = false) {
|
|
18755
19172
|
visit(node, nestedFunction);
|
|
18756
19173
|
const nested = nestedFunction || isFunction(node);
|
|
18757
19174
|
for (const key of visitorKeys[node.type] ?? []) {
|
|
18758
19175
|
const child = node[key];
|
|
18759
19176
|
if (Array.isArray(child)) {
|
|
18760
|
-
for (const item of child) if (typeof item === "object" && item !== null && "type" in item)
|
|
19177
|
+
for (const item of child) if (typeof item === "object" && item !== null && "type" in item) walk(item, visitorKeys, visit, nested);
|
|
18761
19178
|
} else if (typeof child === "object" && child !== null && "type" in child) {
|
|
18762
|
-
|
|
19179
|
+
walk(child, visitorKeys, visit, nested);
|
|
18763
19180
|
}
|
|
18764
19181
|
}
|
|
18765
19182
|
}
|
|
@@ -18807,7 +19224,7 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18807
19224
|
const parameterDecoratorNodes = /* @__PURE__ */ new Set();
|
|
18808
19225
|
for (const parameter of method.value.params) {
|
|
18809
19226
|
for (const decorator of parameter.decorators) {
|
|
18810
|
-
|
|
19227
|
+
walk(decorator, context.sourceCode.visitorKeys, (current) => parameterDecoratorNodes.add(current));
|
|
18811
19228
|
}
|
|
18812
19229
|
}
|
|
18813
19230
|
const thisValue = (value) => {
|
|
@@ -18839,10 +19256,10 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18839
19256
|
}
|
|
18840
19257
|
};
|
|
18841
19258
|
for (const parameter of method.value.params) {
|
|
18842
|
-
|
|
19259
|
+
walk(parameter, context.sourceCode.visitorKeys, collectAlias);
|
|
18843
19260
|
}
|
|
18844
19261
|
for (const statement of method.value.body.body) {
|
|
18845
|
-
|
|
19262
|
+
walk(statement, context.sourceCode.visitorKeys, collectAlias);
|
|
18846
19263
|
}
|
|
18847
19264
|
const visitCall = (current, nestedFunction) => {
|
|
18848
19265
|
if (current.type === import_utils96.AST_NODE_TYPES.VariableDeclarator && current.id.type === import_utils96.AST_NODE_TYPES.ObjectPattern && thisValue(current.init)) {
|
|
@@ -18876,19 +19293,19 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18876
19293
|
calls.set(caller, callees);
|
|
18877
19294
|
};
|
|
18878
19295
|
for (const decorator of method.decorators) {
|
|
18879
|
-
|
|
19296
|
+
walk(decorator, context.sourceCode.visitorKeys, visitCall, true);
|
|
18880
19297
|
}
|
|
18881
|
-
if (method.computed)
|
|
19298
|
+
if (method.computed) walk(method.key, context.sourceCode.visitorKeys, visitCall, true);
|
|
18882
19299
|
for (const parameter of method.value.params) {
|
|
18883
|
-
|
|
19300
|
+
walk(parameter, context.sourceCode.visitorKeys, visitCall);
|
|
18884
19301
|
}
|
|
18885
19302
|
for (const statement of method.value.body.body) {
|
|
18886
|
-
|
|
19303
|
+
walk(statement, context.sourceCode.visitorKeys, visitCall);
|
|
18887
19304
|
}
|
|
18888
19305
|
}
|
|
18889
19306
|
for (const member of node.body.body) {
|
|
18890
19307
|
if (member.type === import_utils96.AST_NODE_TYPES.MethodDefinition || member.type === import_utils96.AST_NODE_TYPES.TSAbstractMethodDefinition) continue;
|
|
18891
|
-
|
|
19308
|
+
walk(member, context.sourceCode.visitorKeys, (current) => {
|
|
18892
19309
|
if (current.type !== import_utils96.AST_NODE_TYPES.MemberExpression) return;
|
|
18893
19310
|
const target = referencedMethod(context, current, classVariables);
|
|
18894
19311
|
const possibleTarget = target ?? referencedPropertyName(current);
|
|
@@ -18907,37 +19324,7 @@ function classScope(context, node, computedReferenceNames) {
|
|
|
18907
19324
|
if (helperIndex === void 0 || callerIndex === void 0) return false;
|
|
18908
19325
|
return runtimeBarrierPrefix[callerIndex + 1] === runtimeBarrierPrefix[helperIndex + 1];
|
|
18909
19326
|
};
|
|
18910
|
-
|
|
18911
|
-
for (const [caller, callees] of calls) {
|
|
18912
|
-
for (const callee of callees) {
|
|
18913
|
-
if (callee === caller) continue;
|
|
18914
|
-
const callers = incoming.get(callee) ?? /* @__PURE__ */ new Set();
|
|
18915
|
-
callers.add(caller);
|
|
18916
|
-
incoming.set(callee, callers);
|
|
18917
|
-
}
|
|
18918
|
-
}
|
|
18919
|
-
reportMisordered(context, definitions, scopeDefinitions, calls, pinned, canMove, (helper, caller) => {
|
|
18920
|
-
if (!canMove(helper, caller)) return void 0;
|
|
18921
|
-
const helperCallsAnother = [...calls.get(helper.name) ?? []].some((callee) => callee !== helper.name);
|
|
18922
|
-
const callerIsAnotherHelper = [...incoming.get(caller.name) ?? []].some((name) => name !== helper.name);
|
|
18923
|
-
if (helperCallsAnother || callerIsAnotherHelper) return void 0;
|
|
18924
|
-
const helperMember = helper.node;
|
|
18925
|
-
const callerMember = caller.node;
|
|
18926
|
-
const helperIndex = memberIndexes.get(helperMember);
|
|
18927
|
-
if (helperIndex === void 0) return void 0;
|
|
18928
|
-
const next = node.body.body[helperIndex + 1];
|
|
18929
|
-
const suffixEnd = next?.range[0] ?? node.body.range[1] - 1;
|
|
18930
|
-
const suffix = context.sourceCode.text.slice(helperMember.range[1], suffixEnd);
|
|
18931
|
-
if (!/^\s*$/u.test(suffix)) return void 0;
|
|
18932
|
-
const previous = node.body.body[helperIndex - 1];
|
|
18933
|
-
const prefixStart = previous?.range[1] ?? node.body.range[0] + 1;
|
|
18934
|
-
if (!/^\s*$/u.test(context.sourceCode.text.slice(prefixStart, helperMember.range[0]))) return void 0;
|
|
18935
|
-
const helperText = context.sourceCode.getText(helperMember);
|
|
18936
|
-
return (fixer) => [
|
|
18937
|
-
fixer.removeRange([helperMember.range[0], suffixEnd]),
|
|
18938
|
-
fixer.insertTextAfter(callerMember, `${suffix}${helperText}`)
|
|
18939
|
-
];
|
|
18940
|
-
});
|
|
19327
|
+
reportMisordered(context, definitions, scopeDefinitions, calls, pinned, canMove);
|
|
18941
19328
|
}
|
|
18942
19329
|
function isClassRuntimeBarrier(member) {
|
|
18943
19330
|
switch (member.type) {
|
|
@@ -18959,7 +19346,6 @@ var stepdown_default = createRule({
|
|
|
18959
19346
|
type: "suggestion",
|
|
18960
19347
|
docs: { description: "Place a private helper below its sole direct same-scope caller." },
|
|
18961
19348
|
schema: [],
|
|
18962
|
-
fixable: "code",
|
|
18963
19349
|
messages: {
|
|
18964
19350
|
helperAboveOnlyCaller: "Private helper `{{helper}}` is defined above its only caller `{{caller}}`; move it below the caller."
|
|
18965
19351
|
}
|
|
@@ -18978,7 +19364,7 @@ var stepdown_default = createRule({
|
|
|
18978
19364
|
"Program:exit": (program) => {
|
|
18979
19365
|
moduleScope(context, program);
|
|
18980
19366
|
const computedReferenceNames = /* @__PURE__ */ new Set();
|
|
18981
|
-
|
|
19367
|
+
walk(program, context.sourceCode.visitorKeys, (node) => {
|
|
18982
19368
|
if (node.type === import_utils96.AST_NODE_TYPES.MemberExpression && node.computed && node.property.type === import_utils96.AST_NODE_TYPES.Literal && typeof node.property.value === "string") computedReferenceNames.add(node.property.value);
|
|
18983
19369
|
});
|
|
18984
19370
|
for (const node of classes) classScope(context, node, computedReferenceNames);
|
|
@@ -19032,7 +19418,7 @@ var SOURCE_COUPLED_TEST_DOCUMENTATION = {
|
|
|
19032
19418
|
remediation: "Parse the artifact, execute its validator, or assert on another runtime contract.",
|
|
19033
19419
|
category: "testing",
|
|
19034
19420
|
limitations: [
|
|
19035
|
-
"The rule follows lexical
|
|
19421
|
+
"The rule follows stable lexical bindings, static source paths, awaited reads, and common text operations. Reassigned bindings, dynamic paths, unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.",
|
|
19036
19422
|
"When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."
|
|
19037
19423
|
],
|
|
19038
19424
|
examples: [
|
|
@@ -19071,6 +19457,11 @@ function stringValue(node) {
|
|
|
19071
19457
|
const current = unwrap7(node);
|
|
19072
19458
|
if (current.type === import_utils97.AST_NODE_TYPES.Literal && typeof current.value === "string") return current.value;
|
|
19073
19459
|
if (current.type === import_utils97.AST_NODE_TYPES.TemplateLiteral && current.expressions.length === 0) return current.quasis[0]?.value.cooked ?? null;
|
|
19460
|
+
if (current.type === import_utils97.AST_NODE_TYPES.BinaryExpression && current.operator === "+") {
|
|
19461
|
+
const left = stringValue(current.left);
|
|
19462
|
+
const right = stringValue(current.right);
|
|
19463
|
+
return left === null || right === null ? null : left + right;
|
|
19464
|
+
}
|
|
19074
19465
|
return null;
|
|
19075
19466
|
}
|
|
19076
19467
|
function importSource(node) {
|
|
@@ -19100,14 +19491,19 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19100
19491
|
const scopes = [newScope()];
|
|
19101
19492
|
const reportedOrigins = /* @__PURE__ */ new Set();
|
|
19102
19493
|
const currentScope = () => scopes.at(-1) ?? scopes[0];
|
|
19103
|
-
const
|
|
19494
|
+
const bindingOf = (node) => import_utils97.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
|
|
19495
|
+
const visible = (kind, node) => {
|
|
19496
|
+
const name2 = bindingOf(node);
|
|
19497
|
+
if (name2 === null || name2.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
|
|
19104
19498
|
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
19105
19499
|
const scope = scopes[index];
|
|
19106
19500
|
if (scope.declared.has(name2)) return scope[kind].has(name2);
|
|
19107
19501
|
}
|
|
19108
19502
|
return false;
|
|
19109
19503
|
};
|
|
19110
|
-
const visibleRawOrigins = (
|
|
19504
|
+
const visibleRawOrigins = (node) => {
|
|
19505
|
+
const name2 = bindingOf(node);
|
|
19506
|
+
if (name2 === null || name2.references.some((reference) => reference.isWrite() && reference.init !== true)) return /* @__PURE__ */ new Set();
|
|
19111
19507
|
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
19112
19508
|
const scope = scopes[index];
|
|
19113
19509
|
if (scope.declared.has(name2)) return scope.rawOrigins.get(name2) ?? /* @__PURE__ */ new Set();
|
|
@@ -19118,15 +19514,14 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19118
19514
|
const current = unwrap7(node);
|
|
19119
19515
|
const value = stringValue(current);
|
|
19120
19516
|
if (value !== null) return sourceSuffixRe.test(value);
|
|
19121
|
-
if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return visible("paths", current
|
|
19122
|
-
if (current.type === import_utils97.AST_NODE_TYPES.BinaryExpression && current.operator === "+") {
|
|
19123
|
-
return sourcePath(current.left) || sourcePath(current.right);
|
|
19124
|
-
}
|
|
19125
|
-
if (current.type === import_utils97.AST_NODE_TYPES.TemplateLiteral) return current.expressions.some(sourcePath);
|
|
19517
|
+
if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return visible("paths", current);
|
|
19126
19518
|
if (current.type === import_utils97.AST_NODE_TYPES.CallExpression || current.type === import_utils97.AST_NODE_TYPES.NewExpression) {
|
|
19127
|
-
|
|
19519
|
+
const callee = current.callee;
|
|
19520
|
+
const first = current.arguments[0];
|
|
19521
|
+
if (first === void 0 || first.type === import_utils97.AST_NODE_TYPES.SpreadElement) return false;
|
|
19522
|
+
if (current.type === import_utils97.AST_NODE_TYPES.NewExpression && callee.type === import_utils97.AST_NODE_TYPES.Identifier && callee.name === "URL" && (bindingOf(callee)?.defs.length ?? 0) === 0) return sourcePath(first);
|
|
19523
|
+
if (callee.type === import_utils97.AST_NODE_TYPES.Identifier && bindingOf(callee)?.defs.some((definition) => definition.node.type === import_utils97.AST_NODE_TYPES.ImportSpecifier && definition.node.imported.type === import_utils97.AST_NODE_TYPES.Identifier && definition.node.imported.name === "fileURLToPath" && definition.node.parent.type === import_utils97.AST_NODE_TYPES.ImportDeclaration && ["node:url", "url"].includes(String(definition.node.parent.source.value)))) return sourcePath(first);
|
|
19128
19524
|
}
|
|
19129
|
-
if (current.type === import_utils97.AST_NODE_TYPES.MemberExpression) return sourcePath(current.object);
|
|
19130
19525
|
return false;
|
|
19131
19526
|
};
|
|
19132
19527
|
const rawRead = (node) => {
|
|
@@ -19134,16 +19529,16 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19134
19529
|
if (current.type !== import_utils97.AST_NODE_TYPES.CallExpression || current.arguments.length === 0) return false;
|
|
19135
19530
|
const callee = unwrap7(current.callee);
|
|
19136
19531
|
if (callee.type === import_utils97.AST_NODE_TYPES.Identifier) {
|
|
19137
|
-
return visible("fsReaders", callee
|
|
19532
|
+
return visible("fsReaders", callee) && sourcePath(current.arguments[0]);
|
|
19138
19533
|
}
|
|
19139
19534
|
if (callee.type !== import_utils97.AST_NODE_TYPES.MemberExpression) return false;
|
|
19140
19535
|
const name2 = staticMemberName7(callee);
|
|
19141
19536
|
const object = unwrap7(callee.object);
|
|
19142
|
-
return name2 !== null && FS_READERS.has(name2) && object.type === import_utils97.AST_NODE_TYPES.Identifier && visible("fsObjects", object
|
|
19537
|
+
return name2 !== null && FS_READERS.has(name2) && object.type === import_utils97.AST_NODE_TYPES.Identifier && visible("fsObjects", object) && sourcePath(current.arguments[0]);
|
|
19143
19538
|
};
|
|
19144
19539
|
const rawOrigins = (node) => {
|
|
19145
19540
|
const current = unwrap7(node);
|
|
19146
|
-
if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return visibleRawOrigins(current
|
|
19541
|
+
if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return visibleRawOrigins(current);
|
|
19147
19542
|
if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
|
|
19148
19543
|
if (current.type === import_utils97.AST_NODE_TYPES.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
|
|
19149
19544
|
if (current.type === import_utils97.AST_NODE_TYPES.MemberExpression && staticMemberName7(current) === "length") return rawOrigins(current.object);
|
|
@@ -19184,14 +19579,9 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19184
19579
|
if (receiver.type !== import_utils97.AST_NODE_TYPES.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
|
|
19185
19580
|
return new Set(node.arguments.flatMap((argument) => argument.type === import_utils97.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
19186
19581
|
};
|
|
19187
|
-
const
|
|
19188
|
-
const
|
|
19189
|
-
if (
|
|
19190
|
-
const argument = node.arguments[0];
|
|
19191
|
-
if (argument?.type !== import_utils97.AST_NODE_TYPES.Literal || !(argument.value instanceof RegExp)) return /* @__PURE__ */ new Set();
|
|
19192
|
-
return rawOrigins(callee.object);
|
|
19193
|
-
};
|
|
19194
|
-
const declare = (name2, state) => {
|
|
19582
|
+
const declare = (node, state) => {
|
|
19583
|
+
const name2 = bindingOf(node);
|
|
19584
|
+
if (name2 === null) return;
|
|
19195
19585
|
const scope = currentScope();
|
|
19196
19586
|
scope.declared.add(name2);
|
|
19197
19587
|
scope.collections.delete(name2);
|
|
@@ -19211,18 +19601,8 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19211
19601
|
const current = unwrap7(node);
|
|
19212
19602
|
return current.type === import_utils97.AST_NODE_TYPES.ArrayExpression && current.elements.length > 0 && current.elements.every((element) => element !== null && element.type !== import_utils97.AST_NODE_TYPES.SpreadElement && sourcePath(element));
|
|
19213
19603
|
};
|
|
19214
|
-
const
|
|
19215
|
-
const current = unwrap7(node);
|
|
19216
|
-
if (current.type === import_utils97.AST_NODE_TYPES.Identifier) return [current.name];
|
|
19217
|
-
if (current.type === import_utils97.AST_NODE_TYPES.AssignmentPattern) return declaredNames2(current.left);
|
|
19218
|
-
if (current.type === import_utils97.AST_NODE_TYPES.RestElement) return declaredNames2(current.argument);
|
|
19219
|
-
if (current.type === import_utils97.AST_NODE_TYPES.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
|
|
19220
|
-
if (current.type === import_utils97.AST_NODE_TYPES.ObjectPattern) return current.properties.flatMap((property) => property.type === import_utils97.AST_NODE_TYPES.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
|
|
19221
|
-
return [];
|
|
19222
|
-
};
|
|
19223
|
-
const enterFunction = (node) => {
|
|
19604
|
+
const enterFunction = () => {
|
|
19224
19605
|
scopes.push(newScope());
|
|
19225
|
-
for (const parameter of node.params) for (const name2 of declaredNames2(parameter)) declare(name2, {});
|
|
19226
19606
|
};
|
|
19227
19607
|
const exitFunction = () => {
|
|
19228
19608
|
scopes.pop();
|
|
@@ -19234,9 +19614,9 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19234
19614
|
for (const specifier of node.specifiers) {
|
|
19235
19615
|
if (specifier.type === import_utils97.AST_NODE_TYPES.ImportSpecifier) {
|
|
19236
19616
|
const imported = specifier.imported.type === import_utils97.AST_NODE_TYPES.Identifier ? specifier.imported.name : String(specifier.imported.value);
|
|
19237
|
-
if (FS_READERS.has(imported)) declare(specifier.local
|
|
19617
|
+
if (FS_READERS.has(imported)) declare(specifier.local, { fsReader: true });
|
|
19238
19618
|
} else {
|
|
19239
|
-
declare(specifier.local
|
|
19619
|
+
declare(specifier.local, { fsObject: true });
|
|
19240
19620
|
}
|
|
19241
19621
|
}
|
|
19242
19622
|
},
|
|
@@ -19245,35 +19625,34 @@ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
|
19245
19625
|
VariableDeclarator(node) {
|
|
19246
19626
|
if (node.init === null) return;
|
|
19247
19627
|
const required = requireSource(node.init);
|
|
19628
|
+
const initializer = unwrap7(node.init);
|
|
19629
|
+
if (required !== null && initializer.type === import_utils97.AST_NODE_TYPES.CallExpression && initializer.callee.type === import_utils97.AST_NODE_TYPES.Identifier && (bindingOf(initializer.callee)?.defs.length ?? 0) > 0) return;
|
|
19248
19630
|
if (required !== null && FS_MODULES.has(required) && node.id.type === import_utils97.AST_NODE_TYPES.Identifier) {
|
|
19249
|
-
declare(node.id
|
|
19631
|
+
declare(node.id, { fsObject: true });
|
|
19250
19632
|
return;
|
|
19251
19633
|
}
|
|
19252
19634
|
if (node.id.type === import_utils97.AST_NODE_TYPES.ObjectPattern && required !== null && FS_MODULES.has(required)) {
|
|
19253
19635
|
for (const property of node.id.properties) {
|
|
19254
19636
|
if (property.type !== import_utils97.AST_NODE_TYPES.Property || property.value.type !== import_utils97.AST_NODE_TYPES.Identifier) continue;
|
|
19255
19637
|
const key = property.key.type === import_utils97.AST_NODE_TYPES.Identifier ? property.key.name : property.key.type === import_utils97.AST_NODE_TYPES.Literal ? String(property.key.value) : "";
|
|
19256
|
-
if (FS_READERS.has(key)) declare(property.value
|
|
19638
|
+
if (FS_READERS.has(key)) declare(property.value, { fsReader: true });
|
|
19257
19639
|
}
|
|
19258
19640
|
return;
|
|
19259
19641
|
}
|
|
19260
19642
|
if (node.id.type !== import_utils97.AST_NODE_TYPES.Identifier) return;
|
|
19261
|
-
declare(node.id
|
|
19643
|
+
declare(node.id, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
|
|
19262
19644
|
},
|
|
19263
19645
|
AssignmentExpression(node) {
|
|
19264
|
-
if (node.left.type === import_utils97.AST_NODE_TYPES.Identifier) declare(node.left
|
|
19646
|
+
if (node.left.type === import_utils97.AST_NODE_TYPES.Identifier) declare(node.left, {});
|
|
19265
19647
|
},
|
|
19266
19648
|
ForOfStatement(node) {
|
|
19267
19649
|
const right = unwrap7(node.right);
|
|
19268
|
-
const collection = right.type === import_utils97.AST_NODE_TYPES.Identifier && visible("collections", right
|
|
19650
|
+
const collection = right.type === import_utils97.AST_NODE_TYPES.Identifier && visible("collections", right);
|
|
19269
19651
|
const left = node.left.type === import_utils97.AST_NODE_TYPES.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
|
|
19270
|
-
if (collection && left?.type === import_utils97.AST_NODE_TYPES.Identifier) declare(left
|
|
19652
|
+
if (collection && left?.type === import_utils97.AST_NODE_TYPES.Identifier) declare(left, { path: true });
|
|
19271
19653
|
},
|
|
19272
19654
|
CallExpression(node) {
|
|
19273
|
-
const origins =
|
|
19274
|
-
...rawAssertionOrigins(node),
|
|
19275
|
-
...rawRegexExtractionOrigins(node)
|
|
19276
|
-
]);
|
|
19655
|
+
const origins = rawAssertionOrigins(node);
|
|
19277
19656
|
if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
|
|
19278
19657
|
for (const origin of origins) reportedOrigins.add(origin);
|
|
19279
19658
|
context.report({ node, messageId: "rawSourceOracle" });
|
|
@@ -19416,8 +19795,8 @@ var IAC_SOURCE_COUPLED_TEST_DOCUMENTATION = {
|
|
|
19416
19795
|
remediation: "Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
|
|
19417
19796
|
category: "testing",
|
|
19418
19797
|
limitations: [
|
|
19419
|
-
"The rule follows lexical
|
|
19420
|
-
"
|
|
19798
|
+
"The rule follows stable lexical bindings and static source paths. Reassigned bindings, dynamic paths, unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.",
|
|
19799
|
+
"When raw representation is genuinely the contract, use an exact line suppression explaining that contract."
|
|
19421
19800
|
],
|
|
19422
19801
|
examples: [
|
|
19423
19802
|
{
|
|
@@ -19892,7 +20271,7 @@ var RULES = {
|
|
|
19892
20271
|
};
|
|
19893
20272
|
var meta = {
|
|
19894
20273
|
name: "@sarj/eslint-plugin",
|
|
19895
|
-
version: "15.17.
|
|
20274
|
+
version: "15.17.11"
|
|
19896
20275
|
};
|
|
19897
20276
|
var APPLICATION_ONLY_RULES = [];
|
|
19898
20277
|
var LIBRARY_IMPORT_POLICY = ["error", {
|