eslint-plugin-react-x 5.23.5 → 5.24.0-beta.1
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.js +754 -614
- package/package.json +8 -7
package/dist/index.js
CHANGED
|
@@ -145,7 +145,7 @@ const rules$6 = {
|
|
|
145
145
|
//#endregion
|
|
146
146
|
//#region package.json
|
|
147
147
|
var name$6 = "eslint-plugin-react-x";
|
|
148
|
-
var version = "5.
|
|
148
|
+
var version = "5.24.0-beta.1";
|
|
149
149
|
|
|
150
150
|
//#endregion
|
|
151
151
|
//#region src/utils/create-rule.ts
|
|
@@ -162,8 +162,8 @@ var error_boundaries_default = createRule({
|
|
|
162
162
|
type: "problem",
|
|
163
163
|
docs: { description: "Validates usage of Error Boundaries instead of try/catch for errors in child components." },
|
|
164
164
|
messages: {
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
"try-catch-with-jsx": "Use an Error Boundary to catch errors in child components. Try/catch can't catch errors during React's rendering process.",
|
|
166
|
+
"try-catch-with-use": "Use an Error Boundary instead of try/catch around the 'use' hook. The 'use' hook suspends the component, and its errors can only be caught by Error Boundaries."
|
|
167
167
|
},
|
|
168
168
|
schema: []
|
|
169
169
|
},
|
|
@@ -192,7 +192,7 @@ function create$50(context) {
|
|
|
192
192
|
const func = Traverse.findParent(stmt, (n) => funcs.some((f) => f.node === n));
|
|
193
193
|
if (stmt != null && func != null && !reported.has(stmt)) {
|
|
194
194
|
context.report({
|
|
195
|
-
messageId: "
|
|
195
|
+
messageId: "try-catch-with-use",
|
|
196
196
|
node: stmt
|
|
197
197
|
});
|
|
198
198
|
reported.add(stmt);
|
|
@@ -204,7 +204,7 @@ function create$50(context) {
|
|
|
204
204
|
const stmt = Traverse.findEnclosingTryBlock(ret);
|
|
205
205
|
if (stmt != null && !reported.has(stmt)) {
|
|
206
206
|
context.report({
|
|
207
|
-
messageId: "
|
|
207
|
+
messageId: "try-catch-with-jsx",
|
|
208
208
|
node: stmt
|
|
209
209
|
});
|
|
210
210
|
reported.add(stmt);
|
|
@@ -1155,7 +1155,7 @@ function resolveToFunction(context, node, seen = /* @__PURE__ */ new Set()) {
|
|
|
1155
1155
|
|
|
1156
1156
|
//#endregion
|
|
1157
1157
|
//#region src/rules/globals/collect.ts
|
|
1158
|
-
function createFactCollector$
|
|
1158
|
+
function createFactCollector$6() {
|
|
1159
1159
|
const facts = {
|
|
1160
1160
|
callEdges: [],
|
|
1161
1161
|
methodCalls: [],
|
|
@@ -1343,9 +1343,9 @@ var globals_default = createRule({
|
|
|
1343
1343
|
type: "problem",
|
|
1344
1344
|
docs: { description: "Validates against assignment/mutation of globals during render, part of ensuring that side effects must run outside of render." },
|
|
1345
1345
|
messages: {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1346
|
+
"mutating-global": "Do not mutate '{{name}}' during render. Global variables exist outside React's control and make rendering impure.",
|
|
1347
|
+
"mutating-global-array-method": "Do not call '{{method}}()' on '{{name}}' during render. Mutating global arrays during render makes rendering impure.",
|
|
1348
|
+
"mutating-global-property": "Do not mutate '{{name}}' during render. Modifying global objects during render makes rendering impure."
|
|
1349
1349
|
},
|
|
1350
1350
|
schema: []
|
|
1351
1351
|
},
|
|
@@ -1356,7 +1356,7 @@ var globals_default = createRule({
|
|
|
1356
1356
|
function create$49(context) {
|
|
1357
1357
|
const hooks = core.getHookCollector(context);
|
|
1358
1358
|
const comps = core.getFunctionComponentCollector(context);
|
|
1359
|
-
const facts = createFactCollector$
|
|
1359
|
+
const facts = createFactCollector$6();
|
|
1360
1360
|
return merge(hooks.visitor, comps.visitor, facts.visitor, { "Program:exit"(program) {
|
|
1361
1361
|
const renderFunctions = [...comps.api.getAllComponents(program), ...hooks.api.getAllHooks(program)].map(({ node }) => node);
|
|
1362
1362
|
const directEffects = inferGlobalMutations(context, facts.facts);
|
|
@@ -1368,7 +1368,7 @@ function create$49(context) {
|
|
|
1368
1368
|
};
|
|
1369
1369
|
context.report({
|
|
1370
1370
|
data,
|
|
1371
|
-
messageId: match(effect).with({ kind: "global" }, () => "
|
|
1371
|
+
messageId: match(effect).with({ kind: "global" }, () => "mutating-global").with({ kind: "method" }, () => "mutating-global-array-method").with({ kind: "property" }, () => "mutating-global-property").exhaustive(),
|
|
1372
1372
|
node: effect.node
|
|
1373
1373
|
});
|
|
1374
1374
|
}
|
|
@@ -1451,9 +1451,9 @@ function isRefLikeName$1(name) {
|
|
|
1451
1451
|
return name === "ref" || name.endsWith("Ref");
|
|
1452
1452
|
}
|
|
1453
1453
|
function hasRefLikeNameInChain(node) {
|
|
1454
|
-
const
|
|
1455
|
-
if (!Check.isExpression(
|
|
1456
|
-
return Extract.getMemberChain(
|
|
1454
|
+
const expr = Extract.unwrap(node);
|
|
1455
|
+
if (!Check.isExpression(expr)) return false;
|
|
1456
|
+
return Extract.getMemberChain(expr).some((member) => Check.isIdentifier(member) && isRefLikeName$1(member.name));
|
|
1457
1457
|
}
|
|
1458
1458
|
function isInitializedFromCall(context, node, isCall) {
|
|
1459
1459
|
const root = Extract.getMemberChain(node).at(0);
|
|
@@ -1482,7 +1482,7 @@ function isRefLikeChain(context, node) {
|
|
|
1482
1482
|
|
|
1483
1483
|
//#endregion
|
|
1484
1484
|
//#region src/rules/immutability/collect.ts
|
|
1485
|
-
function createFactCollector$
|
|
1485
|
+
function createFactCollector$5() {
|
|
1486
1486
|
const facts = {
|
|
1487
1487
|
mutations: [],
|
|
1488
1488
|
sinks: []
|
|
@@ -1746,9 +1746,9 @@ var immutability_default = createRule({
|
|
|
1746
1746
|
type: "problem",
|
|
1747
1747
|
docs: { description: "Validates against mutating props, state, and other immutable values, including through functions passed into frozen contexts such as JSX props, hook arguments, and hook return values." },
|
|
1748
1748
|
messages: {
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1749
|
+
"direct-mutation": "Do not mutate '{{name}}' directly. {{detail}}",
|
|
1750
|
+
"indirect-mutation": "This function may (indirectly) reassign or modify '{{name}}' after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.",
|
|
1751
|
+
"mutation-site": "This modifies '{{name}}'."
|
|
1752
1752
|
},
|
|
1753
1753
|
schema: []
|
|
1754
1754
|
},
|
|
@@ -1759,7 +1759,7 @@ var immutability_default = createRule({
|
|
|
1759
1759
|
function create$48(context) {
|
|
1760
1760
|
const hooks = core.getHookCollector(context);
|
|
1761
1761
|
const comps = core.getFunctionComponentCollector(context);
|
|
1762
|
-
const facts = createFactCollector$
|
|
1762
|
+
const facts = createFactCollector$5();
|
|
1763
1763
|
return merge(hooks.visitor, comps.visitor, facts.visitor, { "Program:exit"(program) {
|
|
1764
1764
|
for (const hook of hooks.api.getAllHooks(program)) for (const expression of hook.rets) if (expression != null) facts.facts.sinks.push({
|
|
1765
1765
|
kind: "hook-return",
|
|
@@ -1780,12 +1780,12 @@ function create$48(context) {
|
|
|
1780
1780
|
reportedMutations.add(mutation.node);
|
|
1781
1781
|
context.report({
|
|
1782
1782
|
data: { name: mutation.name },
|
|
1783
|
-
messageId: "
|
|
1783
|
+
messageId: "indirect-mutation",
|
|
1784
1784
|
node: expression
|
|
1785
1785
|
});
|
|
1786
1786
|
context.report({
|
|
1787
1787
|
data: { name: mutation.name },
|
|
1788
|
-
messageId: "
|
|
1788
|
+
messageId: "mutation-site",
|
|
1789
1789
|
node: mutation.node
|
|
1790
1790
|
});
|
|
1791
1791
|
}
|
|
@@ -1799,7 +1799,7 @@ function create$48(context) {
|
|
|
1799
1799
|
name: mutation.name,
|
|
1800
1800
|
detail: mutation.detail
|
|
1801
1801
|
},
|
|
1802
|
-
messageId: "direct",
|
|
1802
|
+
messageId: "direct-mutation",
|
|
1803
1803
|
node: mutation.node
|
|
1804
1804
|
});
|
|
1805
1805
|
}
|
|
@@ -3026,27 +3026,6 @@ const constNull = constant(null);
|
|
|
3026
3026
|
*/
|
|
3027
3027
|
const constUndefined = constant(void 0);
|
|
3028
3028
|
/**
|
|
3029
|
-
* Returns no meaningful value when called.
|
|
3030
|
-
*
|
|
3031
|
-
* **When to use**
|
|
3032
|
-
*
|
|
3033
|
-
* Use when you need a thunk that is called only for its effect and has no
|
|
3034
|
-
* meaningful return value.
|
|
3035
|
-
*
|
|
3036
|
-
* **Example** (Returning void from a thunk)
|
|
3037
|
-
*
|
|
3038
|
-
* ```ts
|
|
3039
|
-
* import { Function } from "effect"
|
|
3040
|
-
* import * as assert from "node:assert"
|
|
3041
|
-
*
|
|
3042
|
-
* assert.deepStrictEqual(Function.constVoid(), undefined)
|
|
3043
|
-
* ```
|
|
3044
|
-
*
|
|
3045
|
-
* @category constants
|
|
3046
|
-
* @since 2.0.0
|
|
3047
|
-
*/
|
|
3048
|
-
const constVoid = constUndefined;
|
|
3049
|
-
/**
|
|
3050
3029
|
* Composes two functions, `ab` and `bc` into a single function that takes in an argument `a` of type `A` and returns a result of type `C`.
|
|
3051
3030
|
* The result is obtained by first applying the `ab` function to `a` and then applying the `bc` function to the result of `ab`.
|
|
3052
3031
|
*
|
|
@@ -3412,10 +3391,10 @@ const INDEX_PARAM_POSITIONS = /* @__PURE__ */ new Map([
|
|
|
3412
3391
|
*/
|
|
3413
3392
|
function getNestedReturnStatements$1(node) {
|
|
3414
3393
|
const statements = [];
|
|
3415
|
-
const
|
|
3394
|
+
const boundary = Check.isFunction(node) ? node : Traverse.findParent(node, Check.isFunction);
|
|
3416
3395
|
simpleTraverse(node, { enter(node) {
|
|
3417
3396
|
if (node.type !== AST_NODE_TYPES.ReturnStatement) return;
|
|
3418
|
-
if (Traverse.findParent(node, Check.isFunction, (n) => n ===
|
|
3397
|
+
if (Traverse.findParent(node, Check.isFunction, (n) => n === boundary) != null) return;
|
|
3419
3398
|
statements.push(node);
|
|
3420
3399
|
} });
|
|
3421
3400
|
return statements;
|
|
@@ -3430,7 +3409,7 @@ var no_missing_key_default = createRule({
|
|
|
3430
3409
|
docs: { description: "Disallows missing 'key' on items in list rendering." },
|
|
3431
3410
|
messages: {
|
|
3432
3411
|
default: "Missing 'key' for element when rendering list.",
|
|
3433
|
-
|
|
3412
|
+
"unexpected-fragment-syntax": "Use fragment component instead of '<>' because it does not support `key`."
|
|
3434
3413
|
},
|
|
3435
3414
|
schema: []
|
|
3436
3415
|
},
|
|
@@ -3468,7 +3447,7 @@ function create$24(context) {
|
|
|
3468
3447
|
node
|
|
3469
3448
|
}];
|
|
3470
3449
|
case AST_NODE_TYPES.JSXFragment: return [{
|
|
3471
|
-
messageId: "
|
|
3450
|
+
messageId: "unexpected-fragment-syntax",
|
|
3472
3451
|
node
|
|
3473
3452
|
}];
|
|
3474
3453
|
case AST_NODE_TYPES.ConditionalExpression: return [...visitItemExpression(node.consequent), ...visitItemExpression(node.alternate)];
|
|
@@ -3554,8 +3533,8 @@ var no_misused_capture_owner_stack_default = createRule({
|
|
|
3554
3533
|
type: "problem",
|
|
3555
3534
|
docs: { description: "Prevents incorrect usage of 'captureOwnerStack'." },
|
|
3556
3535
|
messages: {
|
|
3557
|
-
|
|
3558
|
-
|
|
3536
|
+
"missing-development-only-check": `Don't call 'captureOwnerStack' directly. Use 'if (process.env.NODE_ENV !== "production") {...}' to conditionally access it.`,
|
|
3537
|
+
"use-namespace-import": "Don't use named imports of 'captureOwnerStack' in files that are bundled for development and production. Use a namespace import instead."
|
|
3559
3538
|
},
|
|
3560
3539
|
schema: []
|
|
3561
3540
|
},
|
|
@@ -3570,7 +3549,7 @@ function create$23(context) {
|
|
|
3570
3549
|
CallExpression(node) {
|
|
3571
3550
|
if (!core.isCaptureOwnerStackCall(context, node)) return;
|
|
3572
3551
|
if (Traverse.findParent(node, (n) => isDevelopmentOnlyCheck(context, n)) == null) context.report({
|
|
3573
|
-
messageId: "
|
|
3552
|
+
messageId: "missing-development-only-check",
|
|
3574
3553
|
node
|
|
3575
3554
|
});
|
|
3576
3555
|
},
|
|
@@ -3580,7 +3559,7 @@ function create$23(context) {
|
|
|
3580
3559
|
if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) continue;
|
|
3581
3560
|
if (!Check.isIdentifier(specifier.imported, "captureOwnerStack")) continue;
|
|
3582
3561
|
context.report({
|
|
3583
|
-
messageId: "
|
|
3562
|
+
messageId: "use-namespace-import",
|
|
3584
3563
|
node: specifier
|
|
3585
3564
|
});
|
|
3586
3565
|
}
|
|
@@ -4082,7 +4061,7 @@ var no_unstable_context_value_default = createRule({
|
|
|
4082
4061
|
meta: {
|
|
4083
4062
|
type: "problem",
|
|
4084
4063
|
docs: { description: "Prevents non-stable values (i.e., object literals) from being used as a value for 'Context.Provider'." },
|
|
4085
|
-
messages: {
|
|
4064
|
+
messages: { "unstable-context-value": "A/an '{{kind}}' passed as the value prop to the context provider should not be constructed. It will change on every render. {{suggestion}}" },
|
|
4086
4065
|
schema: []
|
|
4087
4066
|
},
|
|
4088
4067
|
name: RULE_NAME$13,
|
|
@@ -4125,7 +4104,7 @@ function create$13(context) {
|
|
|
4125
4104
|
kind: getHumanReadableKind$1(constructionNode),
|
|
4126
4105
|
suggestion
|
|
4127
4106
|
},
|
|
4128
|
-
messageId: "
|
|
4107
|
+
messageId: "unstable-context-value",
|
|
4129
4108
|
node: constructionNode
|
|
4130
4109
|
});
|
|
4131
4110
|
}
|
|
@@ -5289,7 +5268,7 @@ function isAfterTerminatingNonNullGuard(node, getNullBranch) {
|
|
|
5289
5268
|
* reachable during render and are not collected. All inference is deferred to
|
|
5290
5269
|
* `Program:exit`.
|
|
5291
5270
|
*/
|
|
5292
|
-
function createFactCollector$
|
|
5271
|
+
function createFactCollector$4() {
|
|
5293
5272
|
const facts = {
|
|
5294
5273
|
bindings: [],
|
|
5295
5274
|
callEdges: [],
|
|
@@ -5722,10 +5701,10 @@ var refs_default = createRule({
|
|
|
5722
5701
|
type: "problem",
|
|
5723
5702
|
docs: { description: "Validates correct usage of refs by checking that 'ref.current' is not read or written during render." },
|
|
5724
5703
|
messages: {
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5704
|
+
"duplicate-ref-init": "Ref is initialized more than once during render. Only a single 'if (ref.current == null)' initialization is allowed; move any additional initialization into an effect or event handler.",
|
|
5705
|
+
"read-during-render": "Cannot access refs during render",
|
|
5706
|
+
"ref-passed-to-function": "Passing a ref to a function may read its value during render",
|
|
5707
|
+
"write-during-render": "Cannot update ref during render"
|
|
5729
5708
|
},
|
|
5730
5709
|
schema: []
|
|
5731
5710
|
},
|
|
@@ -5736,7 +5715,7 @@ var refs_default = createRule({
|
|
|
5736
5715
|
function create$6(context) {
|
|
5737
5716
|
const hooks = core.getHookCollector(context);
|
|
5738
5717
|
const comps = core.getFunctionComponentCollector(context);
|
|
5739
|
-
const facts = createFactCollector$
|
|
5718
|
+
const facts = createFactCollector$4();
|
|
5740
5719
|
return merge(hooks.visitor, comps.visitor, facts.visitor, { "Program:exit"(program) {
|
|
5741
5720
|
const boundaries = /* @__PURE__ */ new Set([...comps.api.getAllComponents(program).map((component) => component.node), ...hooks.api.getAllHooks(program).map((hook) => hook.node)]);
|
|
5742
5721
|
const resolver = createBindingResolver(context, facts.facts);
|
|
@@ -5744,7 +5723,7 @@ function create$6(context) {
|
|
|
5744
5723
|
const reachability = collectReachableFunctions(boundaries, callGraph);
|
|
5745
5724
|
const violations = [...inferRefViolations(facts.facts.refAccesses, boundaries, reachability, resolver), ...inferRefPassViolations(facts.facts.passSites, boundaries, reachability, resolver)];
|
|
5746
5725
|
for (const violation of violations) context.report({
|
|
5747
|
-
messageId: match(violation).with({ kind: "duplicate-init" }, () => "
|
|
5726
|
+
messageId: match(violation).with({ kind: "duplicate-init" }, () => "duplicate-ref-init").with({ kind: "pass" }, () => "ref-passed-to-function").with({ kind: "read" }, () => "read-during-render").with({ kind: "write" }, () => "write-during-render").exhaustive(),
|
|
5748
5727
|
node: violation.node
|
|
5749
5728
|
});
|
|
5750
5729
|
} });
|
|
@@ -7885,7 +7864,43 @@ function last(array) {
|
|
|
7885
7864
|
}
|
|
7886
7865
|
|
|
7887
7866
|
//#endregion
|
|
7888
|
-
//#region src/rules/set-state-in-effect/
|
|
7867
|
+
//#region src/rules/set-state-in-effect/helpers.ts
|
|
7868
|
+
const IDENTIFIER_VISITOR_KEYS = {
|
|
7869
|
+
[AST_NODE_TYPES.ArrayExpression]: ["elements"],
|
|
7870
|
+
[AST_NODE_TYPES.ArrayPattern]: ["elements"],
|
|
7871
|
+
[AST_NODE_TYPES.AssignmentExpression]: ["left", "right"],
|
|
7872
|
+
[AST_NODE_TYPES.AssignmentPattern]: ["left", "right"],
|
|
7873
|
+
[AST_NODE_TYPES.AwaitExpression]: ["argument"],
|
|
7874
|
+
[AST_NODE_TYPES.BinaryExpression]: ["left", "right"],
|
|
7875
|
+
[AST_NODE_TYPES.CallExpression]: ["callee", "arguments"],
|
|
7876
|
+
[AST_NODE_TYPES.ChainExpression]: ["expression"],
|
|
7877
|
+
[AST_NODE_TYPES.ConditionalExpression]: [
|
|
7878
|
+
"test",
|
|
7879
|
+
"consequent",
|
|
7880
|
+
"alternate"
|
|
7881
|
+
],
|
|
7882
|
+
[AST_NODE_TYPES.ForInStatement]: ["left", "right"],
|
|
7883
|
+
[AST_NODE_TYPES.ForOfStatement]: ["left", "right"],
|
|
7884
|
+
[AST_NODE_TYPES.ImportExpression]: ["source"],
|
|
7885
|
+
[AST_NODE_TYPES.LogicalExpression]: ["left", "right"],
|
|
7886
|
+
[AST_NODE_TYPES.MemberExpression]: ["object", "property"],
|
|
7887
|
+
[AST_NODE_TYPES.NewExpression]: ["callee", "arguments"],
|
|
7888
|
+
[AST_NODE_TYPES.ObjectExpression]: ["properties"],
|
|
7889
|
+
[AST_NODE_TYPES.ObjectPattern]: ["properties"],
|
|
7890
|
+
[AST_NODE_TYPES.Property]: ["value"],
|
|
7891
|
+
[AST_NODE_TYPES.SequenceExpression]: ["expressions"],
|
|
7892
|
+
[AST_NODE_TYPES.SpreadElement]: ["argument"],
|
|
7893
|
+
[AST_NODE_TYPES.TaggedTemplateExpression]: ["tag", "quasi"],
|
|
7894
|
+
[AST_NODE_TYPES.TemplateLiteral]: ["expressions"],
|
|
7895
|
+
[AST_NODE_TYPES.TSAsExpression]: ["expression"],
|
|
7896
|
+
[AST_NODE_TYPES.TSInstantiationExpression]: ["expression"],
|
|
7897
|
+
[AST_NODE_TYPES.TSNonNullExpression]: ["expression"],
|
|
7898
|
+
[AST_NODE_TYPES.TSSatisfiesExpression]: ["expression"],
|
|
7899
|
+
[AST_NODE_TYPES.TSTypeAssertion]: ["expression"],
|
|
7900
|
+
[AST_NODE_TYPES.UnaryExpression]: ["argument"],
|
|
7901
|
+
[AST_NODE_TYPES.UpdateExpression]: ["argument"],
|
|
7902
|
+
[AST_NODE_TYPES.YieldExpression]: ["argument"]
|
|
7903
|
+
};
|
|
7889
7904
|
/**
|
|
7890
7905
|
* Get all nested identifiers in a expression like node
|
|
7891
7906
|
* @param node The node to get the nested identifiers from
|
|
@@ -7893,79 +7908,14 @@ function last(array) {
|
|
|
7893
7908
|
*/
|
|
7894
7909
|
function getNestedIdentifiers(node) {
|
|
7895
7910
|
const identifiers = [];
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
}
|
|
7905
|
-
if ("properties" in node) {
|
|
7906
|
-
const chunk = node.properties.flatMap(getNestedIdentifiers);
|
|
7907
|
-
identifiers.push(...chunk);
|
|
7908
|
-
}
|
|
7909
|
-
if ("expressions" in node) {
|
|
7910
|
-
const chunk = node.expressions.flatMap(getNestedIdentifiers);
|
|
7911
|
-
identifiers.push(...chunk);
|
|
7912
|
-
}
|
|
7913
|
-
if ("left" in node) {
|
|
7914
|
-
const chunk = getNestedIdentifiers(node.left);
|
|
7915
|
-
identifiers.push(...chunk);
|
|
7916
|
-
}
|
|
7917
|
-
if ("right" in node) {
|
|
7918
|
-
const chunk = getNestedIdentifiers(node.right);
|
|
7919
|
-
identifiers.push(...chunk);
|
|
7920
|
-
}
|
|
7921
|
-
if (node.type === AST_NODE_TYPES.Property) {
|
|
7922
|
-
const chunk = getNestedIdentifiers(node.value);
|
|
7923
|
-
identifiers.push(...chunk);
|
|
7924
|
-
}
|
|
7925
|
-
if (node.type === AST_NODE_TYPES.SpreadElement) {
|
|
7926
|
-
const chunk = getNestedIdentifiers(node.argument);
|
|
7927
|
-
identifiers.push(...chunk);
|
|
7928
|
-
}
|
|
7929
|
-
if (node.type === AST_NODE_TYPES.MemberExpression) {
|
|
7930
|
-
identifiers.push(...getNestedIdentifiers(node.object));
|
|
7931
|
-
if (node.computed) identifiers.push(...getNestedIdentifiers(node.property));
|
|
7932
|
-
}
|
|
7933
|
-
if (node.type === AST_NODE_TYPES.UnaryExpression) {
|
|
7934
|
-
const chunk = getNestedIdentifiers(node.argument);
|
|
7935
|
-
identifiers.push(...chunk);
|
|
7936
|
-
}
|
|
7937
|
-
if (node.type === AST_NODE_TYPES.ChainExpression) {
|
|
7938
|
-
const chunk = getNestedIdentifiers(node.expression);
|
|
7939
|
-
identifiers.push(...chunk);
|
|
7940
|
-
}
|
|
7941
|
-
if (node.type === AST_NODE_TYPES.TSNonNullExpression) {
|
|
7942
|
-
const chunk = getNestedIdentifiers(node.expression);
|
|
7943
|
-
identifiers.push(...chunk);
|
|
7944
|
-
}
|
|
7945
|
-
if (node.type === AST_NODE_TYPES.TSAsExpression) {
|
|
7946
|
-
const chunk = getNestedIdentifiers(node.expression);
|
|
7947
|
-
identifiers.push(...chunk);
|
|
7948
|
-
}
|
|
7949
|
-
if (node.type === AST_NODE_TYPES.TSSatisfiesExpression) {
|
|
7950
|
-
const chunk = getNestedIdentifiers(node.expression);
|
|
7951
|
-
identifiers.push(...chunk);
|
|
7952
|
-
}
|
|
7953
|
-
if (node.type === AST_NODE_TYPES.ConditionalExpression) {
|
|
7954
|
-
identifiers.push(...getNestedIdentifiers(node.test));
|
|
7955
|
-
identifiers.push(...getNestedIdentifiers(node.consequent));
|
|
7956
|
-
identifiers.push(...getNestedIdentifiers(node.alternate));
|
|
7957
|
-
}
|
|
7958
|
-
if (node.type === AST_NODE_TYPES.AwaitExpression) identifiers.push(...getNestedIdentifiers(node.argument));
|
|
7959
|
-
if (node.type === AST_NODE_TYPES.YieldExpression && node.argument != null) identifiers.push(...getNestedIdentifiers(node.argument));
|
|
7960
|
-
if (node.type === AST_NODE_TYPES.UpdateExpression) identifiers.push(...getNestedIdentifiers(node.argument));
|
|
7961
|
-
if (node.type === AST_NODE_TYPES.CallExpression || node.type === AST_NODE_TYPES.NewExpression) identifiers.push(...getNestedIdentifiers(node.callee));
|
|
7962
|
-
if (node.type === AST_NODE_TYPES.TaggedTemplateExpression) {
|
|
7963
|
-
identifiers.push(...getNestedIdentifiers(node.tag));
|
|
7964
|
-
identifiers.push(...getNestedIdentifiers(node.quasi));
|
|
7965
|
-
}
|
|
7966
|
-
if (node.type === AST_NODE_TYPES.ImportExpression) identifiers.push(...getNestedIdentifiers(node.source));
|
|
7967
|
-
if (node.type === AST_NODE_TYPES.TSTypeAssertion) identifiers.push(...getNestedIdentifiers(node.expression));
|
|
7968
|
-
if (node.type === AST_NODE_TYPES.TSInstantiationExpression) identifiers.push(...getNestedIdentifiers(node.expression));
|
|
7911
|
+
simpleTraverse(node, {
|
|
7912
|
+
enter(node, parent) {
|
|
7913
|
+
if (node.type !== AST_NODE_TYPES.Identifier) return;
|
|
7914
|
+
if (parent?.type === AST_NODE_TYPES.MemberExpression && !parent.computed && parent.property === node) return;
|
|
7915
|
+
identifiers.push(node);
|
|
7916
|
+
},
|
|
7917
|
+
visitorKeys: IDENTIFIER_VISITOR_KEYS
|
|
7918
|
+
});
|
|
7969
7919
|
return identifiers;
|
|
7970
7920
|
}
|
|
7971
7921
|
function isHookDecl(node) {
|
|
@@ -7976,6 +7926,168 @@ function isHookDecl(node) {
|
|
|
7976
7926
|
const name = Extract.getCalleeName(init);
|
|
7977
7927
|
return name != null && core.isHookName(name);
|
|
7978
7928
|
}
|
|
7929
|
+
function isThenCall(node) {
|
|
7930
|
+
return Extract.unwrap(node.callee).type === AST_NODE_TYPES.MemberExpression && Extract.getCalleeName(node) === "then";
|
|
7931
|
+
}
|
|
7932
|
+
function isTerminatingStatement(node) {
|
|
7933
|
+
if (node.type === AST_NODE_TYPES.ReturnStatement || node.type === AST_NODE_TYPES.ThrowStatement) return true;
|
|
7934
|
+
if (node.type === AST_NODE_TYPES.BlockStatement) return node.body.some(isTerminatingStatement);
|
|
7935
|
+
return false;
|
|
7936
|
+
}
|
|
7937
|
+
/**
|
|
7938
|
+
* Get the display name of a setState call reference for reporting.
|
|
7939
|
+
* @param node The setState call node (CallExpression or Identifier)
|
|
7940
|
+
* @param getText The source text getter
|
|
7941
|
+
* @returns The fully qualified name of the call
|
|
7942
|
+
*/
|
|
7943
|
+
function getCallName(node, getText) {
|
|
7944
|
+
if (node.type === AST_NODE_TYPES.CallExpression) return Extract.getFullyQualifiedName(node.callee, getText);
|
|
7945
|
+
return Extract.getFullyQualifiedName(node, getText);
|
|
7946
|
+
}
|
|
7947
|
+
/**
|
|
7948
|
+
* Get the actual CallExpression node from a setState call reference.
|
|
7949
|
+
* When the node is an Identifier that is the callee of a CallExpression,
|
|
7950
|
+
* returns the parent CallExpression; otherwise returns the node itself.
|
|
7951
|
+
* @param node The setState call node (CallExpression or Identifier)
|
|
7952
|
+
* @returns The actual CallExpression node
|
|
7953
|
+
*/
|
|
7954
|
+
function getSetStateCallExpression(node) {
|
|
7955
|
+
return Check.isIdentifier(node) && node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee === node ? node.parent : node;
|
|
7956
|
+
}
|
|
7957
|
+
|
|
7958
|
+
//#endregion
|
|
7959
|
+
//#region src/rules/set-state-in-effect/collect.ts
|
|
7960
|
+
function createFactCollector$3(additionalStateHooks, additionalEffectHooks) {
|
|
7961
|
+
const facts = {
|
|
7962
|
+
calls: [],
|
|
7963
|
+
setStateReferences: [],
|
|
7964
|
+
setupIdentifiers: []
|
|
7965
|
+
};
|
|
7966
|
+
const functionEntries = [];
|
|
7967
|
+
const setupFnRef = { current: null };
|
|
7968
|
+
function isUseStateCall(node) {
|
|
7969
|
+
return core.isUseStateLikeCall(node, additionalStateHooks);
|
|
7970
|
+
}
|
|
7971
|
+
function isUseEffectCall(node) {
|
|
7972
|
+
return core.isUseEffectLikeCall(node, additionalEffectHooks);
|
|
7973
|
+
}
|
|
7974
|
+
function isUseEffectSetupCallback(node) {
|
|
7975
|
+
return node.parent?.type === AST_NODE_TYPES.CallExpression && node.parent.callee !== node && isUseEffectCall(node.parent);
|
|
7976
|
+
}
|
|
7977
|
+
function getFunctionKind(node) {
|
|
7978
|
+
const parent = Traverse.findParent(node, not(Check.isTypeExpression)) ?? node.parent;
|
|
7979
|
+
switch (true) {
|
|
7980
|
+
case node.async:
|
|
7981
|
+
case parent.type === AST_NODE_TYPES.CallExpression && isThenCall(parent): return "deferred";
|
|
7982
|
+
case node.type !== AST_NODE_TYPES.FunctionDeclaration && parent.type === AST_NODE_TYPES.CallExpression && parent.callee === node: return "immediate";
|
|
7983
|
+
case isUseEffectSetupCallback(node): return "setup";
|
|
7984
|
+
default: return "other";
|
|
7985
|
+
}
|
|
7986
|
+
}
|
|
7987
|
+
return {
|
|
7988
|
+
facts,
|
|
7989
|
+
visitor: {
|
|
7990
|
+
":function"(node) {
|
|
7991
|
+
const kind = getFunctionKind(node);
|
|
7992
|
+
functionEntries.push({
|
|
7993
|
+
kind,
|
|
7994
|
+
node
|
|
7995
|
+
});
|
|
7996
|
+
if (kind === "setup") setupFnRef.current = node;
|
|
7997
|
+
},
|
|
7998
|
+
":function:exit"(node) {
|
|
7999
|
+
const { kind } = functionEntries.at(-1) ?? {};
|
|
8000
|
+
if (kind === "setup" && setupFnRef.current === node) setupFnRef.current = null;
|
|
8001
|
+
functionEntries.pop();
|
|
8002
|
+
},
|
|
8003
|
+
CallExpression(node) {
|
|
8004
|
+
const entry = functionEntries.at(-1);
|
|
8005
|
+
if (entry == null || entry.node.async) return;
|
|
8006
|
+
if (isUseStateCall(node)) return;
|
|
8007
|
+
if (isUseEffectCall(node)) {
|
|
8008
|
+
if (!Check.isFunction(node.arguments.at(0))) facts.setupIdentifiers.push(...getNestedIdentifiers(node));
|
|
8009
|
+
return;
|
|
8010
|
+
}
|
|
8011
|
+
facts.calls.push({
|
|
8012
|
+
enclosingFunction: entry.node,
|
|
8013
|
+
enclosingFunctionKind: entry.kind,
|
|
8014
|
+
node,
|
|
8015
|
+
setupFunction: setupFnRef.current
|
|
8016
|
+
});
|
|
8017
|
+
},
|
|
8018
|
+
Identifier(node) {
|
|
8019
|
+
if (node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee === node) return;
|
|
8020
|
+
switch (node.parent.type) {
|
|
8021
|
+
case AST_NODE_TYPES.ArrowFunctionExpression:
|
|
8022
|
+
facts.setStateReferences.push({
|
|
8023
|
+
kind: "callback-body",
|
|
8024
|
+
node
|
|
8025
|
+
});
|
|
8026
|
+
return;
|
|
8027
|
+
case AST_NODE_TYPES.CallExpression:
|
|
8028
|
+
if (node === node.parent.arguments.at(0)) facts.setStateReferences.push({
|
|
8029
|
+
kind: "call-argument",
|
|
8030
|
+
node
|
|
8031
|
+
});
|
|
8032
|
+
return;
|
|
8033
|
+
}
|
|
8034
|
+
}
|
|
8035
|
+
}
|
|
8036
|
+
};
|
|
8037
|
+
}
|
|
8038
|
+
|
|
8039
|
+
//#endregion
|
|
8040
|
+
//#region src/rules/set-state-in-effect/origins.ts
|
|
8041
|
+
/**
|
|
8042
|
+
* Create a resolver that answers whether an identifier or call originates from
|
|
8043
|
+
* a `useState`-like hook's setter function (the element at index 1 of the
|
|
8044
|
+
* returned tuple).
|
|
8045
|
+
* @param context The rule context
|
|
8046
|
+
* @returns The setState provenance resolver
|
|
8047
|
+
*/
|
|
8048
|
+
function createSetStateResolver$1(context) {
|
|
8049
|
+
const { additionalStateHooks } = getSettingsFromContext(context);
|
|
8050
|
+
function isUseStateCall(node) {
|
|
8051
|
+
return core.isUseStateLikeCall(node, additionalStateHooks);
|
|
8052
|
+
}
|
|
8053
|
+
function isSetStateId(id, at) {
|
|
8054
|
+
const initNode = resolveOrigin(context, id);
|
|
8055
|
+
if (initNode == null) return false;
|
|
8056
|
+
if (initNode.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
8057
|
+
if (!isUseStateCall(initNode)) return false;
|
|
8058
|
+
const variableNodeParent = initNode.parent;
|
|
8059
|
+
if (!("id" in variableNodeParent) || variableNodeParent.id?.type !== AST_NODE_TYPES.ArrayPattern) return true;
|
|
8060
|
+
return variableNodeParent.id.elements.findIndex((e) => e != null && Check.isIdentifier(e, id.name)) === at;
|
|
8061
|
+
}
|
|
8062
|
+
function isSetStateCall(node) {
|
|
8063
|
+
const callee = Extract.unwrap(node.callee);
|
|
8064
|
+
switch (callee.type) {
|
|
8065
|
+
case AST_NODE_TYPES.CallExpression: {
|
|
8066
|
+
const innerCallee = Extract.unwrap(callee.callee);
|
|
8067
|
+
if (innerCallee.type !== AST_NODE_TYPES.MemberExpression) return false;
|
|
8068
|
+
if (!("name" in innerCallee.object)) return false;
|
|
8069
|
+
const isAt = Extract.getCalleeName(callee) === "at";
|
|
8070
|
+
const [index] = callee.arguments;
|
|
8071
|
+
if (!isAt || index == null) return false;
|
|
8072
|
+
const indexScope = context.sourceCode.getScope(node);
|
|
8073
|
+
return getStaticValue(index, indexScope)?.value === 1 && isSetStateId(innerCallee.object);
|
|
8074
|
+
}
|
|
8075
|
+
case AST_NODE_TYPES.Identifier: return isSetStateId(callee, 1);
|
|
8076
|
+
case AST_NODE_TYPES.MemberExpression: {
|
|
8077
|
+
if (!("name" in callee.object)) return false;
|
|
8078
|
+
const property = callee.property;
|
|
8079
|
+
const propertyScope = context.sourceCode.getScope(node);
|
|
8080
|
+
return getStaticValue(property, propertyScope)?.value === 1 && isSetStateId(callee.object, 1);
|
|
8081
|
+
}
|
|
8082
|
+
default: return false;
|
|
8083
|
+
}
|
|
8084
|
+
}
|
|
8085
|
+
return {
|
|
8086
|
+
isSetStateCall,
|
|
8087
|
+
isSetStateId,
|
|
8088
|
+
isUseStateCall
|
|
8089
|
+
};
|
|
8090
|
+
}
|
|
7979
8091
|
function isInitializedFromRef(context, name, initialScope, seen = /* @__PURE__ */ new Set()) {
|
|
7980
8092
|
const { additionalRefHooks } = getSettingsFromContext(context);
|
|
7981
8093
|
if (seen.has(name)) return false;
|
|
@@ -8011,19 +8123,35 @@ function isInitializedFromRef(context, name, initialScope, seen = /* @__PURE__ *
|
|
|
8011
8123
|
return false;
|
|
8012
8124
|
}
|
|
8013
8125
|
/**
|
|
8014
|
-
*
|
|
8015
|
-
* @param
|
|
8016
|
-
* @
|
|
8126
|
+
* Check if the setState call is using a ref value, which is safe to use in an effect (ex: `setState(ref.current.scrollTop)`)
|
|
8127
|
+
* @param context The rule context
|
|
8128
|
+
* @param node The setState call argument to check
|
|
8129
|
+
* @returns `true` if the argument value is derived from a ref
|
|
8017
8130
|
*/
|
|
8018
|
-
function
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8131
|
+
function isArgumentUsingRefValue(context, node) {
|
|
8132
|
+
const isUsingRefValue = (n) => {
|
|
8133
|
+
switch (n.type) {
|
|
8134
|
+
case AST_NODE_TYPES.Identifier: return isInitializedFromRef(context, n.name, context.sourceCode.getScope(n));
|
|
8135
|
+
case AST_NODE_TYPES.MemberExpression: return isUsingRefValue(n.object);
|
|
8136
|
+
case AST_NODE_TYPES.CallExpression: return isUsingRefValue(n.callee) || getNestedIdentifiers(n).some(isUsingRefValue);
|
|
8137
|
+
case AST_NODE_TYPES.BinaryExpression:
|
|
8138
|
+
case AST_NODE_TYPES.LogicalExpression: return isUsingRefValue(n.left) || isUsingRefValue(n.right);
|
|
8139
|
+
case AST_NODE_TYPES.UnaryExpression:
|
|
8140
|
+
case AST_NODE_TYPES.UpdateExpression: return isUsingRefValue(n.argument);
|
|
8141
|
+
case AST_NODE_TYPES.ConditionalExpression: return isUsingRefValue(n.consequent) || isUsingRefValue(n.alternate);
|
|
8142
|
+
case AST_NODE_TYPES.SequenceExpression: return n.expressions.some(isUsingRefValue);
|
|
8143
|
+
case AST_NODE_TYPES.TemplateLiteral: return n.expressions.some(isUsingRefValue);
|
|
8144
|
+
case AST_NODE_TYPES.TaggedTemplateExpression: return isUsingRefValue(n.tag) || isUsingRefValue(n.quasi);
|
|
8145
|
+
case AST_NODE_TYPES.ObjectExpression: return n.properties.some(isUsingRefValue);
|
|
8146
|
+
case AST_NODE_TYPES.Property: return isUsingRefValue(n.value);
|
|
8147
|
+
case AST_NODE_TYPES.ArrayExpression: return n.elements.some((element) => element != null && isUsingRefValue(element));
|
|
8148
|
+
case AST_NODE_TYPES.SpreadElement: return isUsingRefValue(n.argument);
|
|
8149
|
+
case AST_NODE_TYPES.AssignmentExpression: return isUsingRefValue(n.right);
|
|
8150
|
+
default: return false;
|
|
8024
8151
|
}
|
|
8025
|
-
|
|
8026
|
-
|
|
8152
|
+
};
|
|
8153
|
+
if (isUsingRefValue(node)) return true;
|
|
8154
|
+
return Check.isFunction(node) && context.sourceCode.getScope(node.body).references.some((r) => isUsingRefValue(r.identifier));
|
|
8027
8155
|
}
|
|
8028
8156
|
/**
|
|
8029
8157
|
* Check if a setState call is inside a conditional block whose test expression
|
|
@@ -8056,23 +8184,133 @@ function isRefGatedContext(context, node) {
|
|
|
8056
8184
|
}
|
|
8057
8185
|
return false;
|
|
8058
8186
|
}
|
|
8059
|
-
function isTerminatingStatement(node) {
|
|
8060
|
-
if (node.type === AST_NODE_TYPES.ReturnStatement || node.type === AST_NODE_TYPES.ThrowStatement) return true;
|
|
8061
|
-
if (node.type === AST_NODE_TYPES.BlockStatement) return node.body.some(isTerminatingStatement);
|
|
8062
|
-
return false;
|
|
8063
|
-
}
|
|
8064
8187
|
function isRefInExpression(context, node) {
|
|
8065
8188
|
return getNestedIdentifiers(node).some((id) => isInitializedFromRef(context, id.name, context.sourceCode.getScope(id)));
|
|
8066
8189
|
}
|
|
8067
8190
|
/**
|
|
8068
|
-
* Get the
|
|
8069
|
-
*
|
|
8070
|
-
* returns the
|
|
8071
|
-
* @param node The setState call node (CallExpression or Identifier)
|
|
8072
|
-
* @returns The actual CallExpression node
|
|
8191
|
+
* Get the name that identifies the source of a `.current` member access.
|
|
8192
|
+
* @param node The object of the member expression
|
|
8193
|
+
* @returns The identifier name, the last property name of a member chain, or null
|
|
8073
8194
|
*/
|
|
8074
|
-
function
|
|
8075
|
-
|
|
8195
|
+
function getRefSourceName(node) {
|
|
8196
|
+
switch (node.type) {
|
|
8197
|
+
case AST_NODE_TYPES.Identifier: return node.name;
|
|
8198
|
+
case AST_NODE_TYPES.MemberExpression: {
|
|
8199
|
+
const property = Extract.unwrap(node.property);
|
|
8200
|
+
return !node.computed && property.type === AST_NODE_TYPES.Identifier ? property.name : null;
|
|
8201
|
+
}
|
|
8202
|
+
default: return null;
|
|
8203
|
+
}
|
|
8204
|
+
}
|
|
8205
|
+
|
|
8206
|
+
//#endregion
|
|
8207
|
+
//#region src/rules/set-state-in-effect/effects.ts
|
|
8208
|
+
/**
|
|
8209
|
+
* Infer the setState calls that run synchronously during an effect setup from
|
|
8210
|
+
* the collected facts: calls made directly in the setup body (or an IIFE within
|
|
8211
|
+
* it), and calls reached indirectly through local functions, hook callbacks, or
|
|
8212
|
+
* setup identifiers passed to the effect.
|
|
8213
|
+
* @param context The rule context
|
|
8214
|
+
* @param facts The collected facts
|
|
8215
|
+
* @returns The violations to report, in report order
|
|
8216
|
+
*/
|
|
8217
|
+
function inferViolations$2(context, facts) {
|
|
8218
|
+
const { additionalEffectHooks } = getSettingsFromContext(context);
|
|
8219
|
+
const resolver = createSetStateResolver$1(context);
|
|
8220
|
+
const getText = (n) => context.sourceCode.getText(n);
|
|
8221
|
+
const isUseEffectCall = (node) => core.isUseEffectLikeCall(node, additionalEffectHooks);
|
|
8222
|
+
const setStateCallsByFn = /* @__PURE__ */ new Map();
|
|
8223
|
+
const setStateInHookCallbacks = /* @__PURE__ */ new Map();
|
|
8224
|
+
const setStateInEffectArg = /* @__PURE__ */ new Map();
|
|
8225
|
+
const setStateInEffectSetup = /* @__PURE__ */ new Map();
|
|
8226
|
+
const trackedFnCalls = [];
|
|
8227
|
+
const violations = [];
|
|
8228
|
+
for (const { enclosingFunction, enclosingFunctionKind, node, setupFunction } of facts.calls) {
|
|
8229
|
+
if (resolver.isUseStateCall(node) || isThenCall(node)) continue;
|
|
8230
|
+
if (!resolver.isSetStateCall(node)) {
|
|
8231
|
+
if (setupFunction != null && enclosingFunction === setupFunction) trackedFnCalls.push(node);
|
|
8232
|
+
continue;
|
|
8233
|
+
}
|
|
8234
|
+
if (enclosingFunctionKind === "deferred") continue;
|
|
8235
|
+
if (setupFunction != null && (enclosingFunction === setupFunction || enclosingFunctionKind === "immediate" && Traverse.findParent(enclosingFunction, Check.isFunction) === setupFunction)) {
|
|
8236
|
+
const args0 = node.arguments.at(0);
|
|
8237
|
+
if (args0 == null) continue;
|
|
8238
|
+
if (isArgumentUsingRefValue(context, args0)) continue;
|
|
8239
|
+
if (isRefGatedContext(context, node)) continue;
|
|
8240
|
+
violations.push({
|
|
8241
|
+
name: getText(Extract.unwrap(node.callee)),
|
|
8242
|
+
node
|
|
8243
|
+
});
|
|
8244
|
+
continue;
|
|
8245
|
+
}
|
|
8246
|
+
const init = Traverse.findParent(node, isHookDecl)?.init;
|
|
8247
|
+
if (init == null) getOrInsertComputed(setStateCallsByFn, enclosingFunction, () => []).push(node);
|
|
8248
|
+
else getOrInsertComputed(setStateInHookCallbacks, init, () => []).push(node);
|
|
8249
|
+
}
|
|
8250
|
+
for (const { kind, node } of facts.setStateReferences) {
|
|
8251
|
+
if (!resolver.isSetStateId(node, 1)) continue;
|
|
8252
|
+
if (kind === "callback-body") {
|
|
8253
|
+
const parent = node.parent.parent;
|
|
8254
|
+
if (parent?.type !== AST_NODE_TYPES.CallExpression) continue;
|
|
8255
|
+
if (!core.isUseMemoCall(context, parent)) continue;
|
|
8256
|
+
const init = Traverse.findParent(parent, isHookDecl)?.init;
|
|
8257
|
+
if (init != null) getOrInsertComputed(setStateInEffectArg, init, () => []).push(node);
|
|
8258
|
+
continue;
|
|
8259
|
+
}
|
|
8260
|
+
const parent = node.parent;
|
|
8261
|
+
if (parent.type !== AST_NODE_TYPES.CallExpression) continue;
|
|
8262
|
+
if (core.isUseCallbackCall(context, parent)) {
|
|
8263
|
+
const init = Traverse.findParent(parent, isHookDecl)?.init;
|
|
8264
|
+
if (init != null) getOrInsertComputed(setStateInEffectArg, init, () => []).push(node);
|
|
8265
|
+
continue;
|
|
8266
|
+
}
|
|
8267
|
+
if (isUseEffectCall(parent)) getOrInsertComputed(setStateInEffectSetup, parent, () => []).push(node);
|
|
8268
|
+
}
|
|
8269
|
+
const isExemptSetStateCall = (setStateCall) => {
|
|
8270
|
+
const callNode = getSetStateCallExpression(setStateCall);
|
|
8271
|
+
if (isRefGatedContext(context, callNode)) return true;
|
|
8272
|
+
if (callNode.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
8273
|
+
const args0 = callNode.arguments.at(0);
|
|
8274
|
+
return args0 != null && isArgumentUsingRefValue(context, args0);
|
|
8275
|
+
};
|
|
8276
|
+
const getSetStateCalls = (id) => {
|
|
8277
|
+
const variable = findVariable(context.sourceCode.getScope(id), id);
|
|
8278
|
+
if (variable != null && variable.defs.some((def) => def.type === DefinitionType.Parameter)) return [];
|
|
8279
|
+
const node = resolveOrigin(context, id);
|
|
8280
|
+
switch (node?.type) {
|
|
8281
|
+
case AST_NODE_TYPES.ArrowFunctionExpression:
|
|
8282
|
+
case AST_NODE_TYPES.FunctionDeclaration:
|
|
8283
|
+
case AST_NODE_TYPES.FunctionExpression: return setStateCallsByFn.get(node) ?? [];
|
|
8284
|
+
case AST_NODE_TYPES.CallExpression: return setStateInHookCallbacks.get(node) ?? setStateInEffectArg.get(node) ?? [];
|
|
8285
|
+
}
|
|
8286
|
+
return [];
|
|
8287
|
+
};
|
|
8288
|
+
for (const [, calls] of setStateInEffectSetup) for (const call of calls) {
|
|
8289
|
+
if (isExemptSetStateCall(call)) continue;
|
|
8290
|
+
violations.push({
|
|
8291
|
+
name: call.name,
|
|
8292
|
+
node: call
|
|
8293
|
+
});
|
|
8294
|
+
}
|
|
8295
|
+
for (const { callee } of trackedFnCalls) {
|
|
8296
|
+
const unwrappedCallee = Extract.unwrap(callee);
|
|
8297
|
+
if (!Check.isIdentifier(unwrappedCallee)) continue;
|
|
8298
|
+
for (const setStateCall of getSetStateCalls(unwrappedCallee)) {
|
|
8299
|
+
if (isExemptSetStateCall(setStateCall)) continue;
|
|
8300
|
+
violations.push({
|
|
8301
|
+
name: getCallName(setStateCall, getText),
|
|
8302
|
+
node: setStateCall
|
|
8303
|
+
});
|
|
8304
|
+
}
|
|
8305
|
+
}
|
|
8306
|
+
for (const id of facts.setupIdentifiers) for (const setStateCall of getSetStateCalls(id)) {
|
|
8307
|
+
if (isExemptSetStateCall(setStateCall)) continue;
|
|
8308
|
+
violations.push({
|
|
8309
|
+
name: getCallName(setStateCall, getText),
|
|
8310
|
+
node: setStateCall
|
|
8311
|
+
});
|
|
8312
|
+
}
|
|
8313
|
+
return violations;
|
|
8076
8314
|
}
|
|
8077
8315
|
|
|
8078
8316
|
//#endregion
|
|
@@ -8092,51 +8330,121 @@ var set_state_in_effect_default = createRule({
|
|
|
8092
8330
|
function create$5(context) {
|
|
8093
8331
|
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
|
|
8094
8332
|
const { additionalEffectHooks, additionalStateHooks } = getSettingsFromContext(context);
|
|
8095
|
-
const
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8333
|
+
const facts = createFactCollector$3(additionalStateHooks, additionalEffectHooks);
|
|
8334
|
+
return merge(facts.visitor, { "Program:exit"() {
|
|
8335
|
+
for (const violation of inferViolations$2(context, facts.facts)) context.report({
|
|
8336
|
+
data: { name: violation.name },
|
|
8337
|
+
messageId: "default",
|
|
8338
|
+
node: violation.node
|
|
8339
|
+
});
|
|
8340
|
+
} });
|
|
8341
|
+
}
|
|
8342
|
+
|
|
8343
|
+
//#endregion
|
|
8344
|
+
//#region src/rules/set-state-in-render/helpers.ts
|
|
8345
|
+
function isComponentOrHookLikeFunction(node) {
|
|
8346
|
+
const id = core.getFunctionId(node);
|
|
8347
|
+
if (id == null) return false;
|
|
8348
|
+
if (Check.isIdentifier(id)) return core.isFunctionComponentName(id.name) || core.isHookName(id.name);
|
|
8349
|
+
if (id.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(id.property)) return core.isFunctionComponentName(id.property.name) || core.isHookName(id.property.name);
|
|
8350
|
+
return false;
|
|
8351
|
+
}
|
|
8352
|
+
function isInsideConditional(node, stopAt) {
|
|
8353
|
+
let current = node.parent;
|
|
8354
|
+
while (current != null && current !== stopAt) {
|
|
8355
|
+
switch (current.type) {
|
|
8356
|
+
case AST_NODE_TYPES.IfStatement:
|
|
8357
|
+
case AST_NODE_TYPES.ConditionalExpression:
|
|
8358
|
+
case AST_NODE_TYPES.LogicalExpression:
|
|
8359
|
+
case AST_NODE_TYPES.SwitchStatement:
|
|
8360
|
+
case AST_NODE_TYPES.SwitchCase: return true;
|
|
8361
|
+
}
|
|
8362
|
+
current = current.parent;
|
|
8112
8363
|
}
|
|
8364
|
+
return false;
|
|
8365
|
+
}
|
|
8366
|
+
|
|
8367
|
+
//#endregion
|
|
8368
|
+
//#region src/rules/set-state-in-render/collect.ts
|
|
8369
|
+
function createFactCollector$2(additionalStateHooks) {
|
|
8370
|
+
const facts = {
|
|
8371
|
+
calls: [],
|
|
8372
|
+
earlyReturns: []
|
|
8373
|
+
};
|
|
8374
|
+
const functionEntries = [];
|
|
8375
|
+
const componentFnRef = { current: null };
|
|
8113
8376
|
function isUseStateCall(node) {
|
|
8114
8377
|
return core.isUseStateLikeCall(node, additionalStateHooks);
|
|
8115
8378
|
}
|
|
8116
|
-
function isUseEffectCall(node) {
|
|
8117
|
-
return core.isUseEffectLikeCall(node, additionalEffectHooks);
|
|
8118
|
-
}
|
|
8119
|
-
function isUseEffectSetupCallback(node) {
|
|
8120
|
-
return node.parent?.type === AST_NODE_TYPES.CallExpression && node.parent.callee !== node && isUseEffectCall(node.parent);
|
|
8121
|
-
}
|
|
8122
|
-
function getCallName(node) {
|
|
8123
|
-
if (node.type === AST_NODE_TYPES.CallExpression) return Extract.getFullyQualifiedName(node.callee, getText);
|
|
8124
|
-
return Extract.getFullyQualifiedName(node, getText);
|
|
8125
|
-
}
|
|
8126
|
-
function getCallKind(node) {
|
|
8127
|
-
return match(node).when(isUseStateCall, () => "useState").when(isUseEffectCall, () => "useEffect").when(isSetStateCall, () => "setState").when(isThenCall, () => "then").otherwise(() => "other");
|
|
8128
|
-
}
|
|
8129
8379
|
function getFunctionKind(node) {
|
|
8380
|
+
if (isComponentOrHookLikeFunction(node)) return "component";
|
|
8130
8381
|
const parent = Traverse.findParent(node, not(Check.isTypeExpression)) ?? node.parent;
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8382
|
+
if (parent.type === AST_NODE_TYPES.CallExpression && parent.callee !== node) return "callback";
|
|
8383
|
+
return "other";
|
|
8384
|
+
}
|
|
8385
|
+
return {
|
|
8386
|
+
facts,
|
|
8387
|
+
visitor: {
|
|
8388
|
+
":function"(node) {
|
|
8389
|
+
const kind = getFunctionKind(node);
|
|
8390
|
+
functionEntries.push({
|
|
8391
|
+
kind,
|
|
8392
|
+
node
|
|
8393
|
+
});
|
|
8394
|
+
if (kind === "component") componentFnRef.current = node;
|
|
8395
|
+
},
|
|
8396
|
+
":function:exit"(node) {
|
|
8397
|
+
if (functionEntries.at(-1)?.kind === "component" && componentFnRef.current === node) componentFnRef.current = null;
|
|
8398
|
+
functionEntries.pop();
|
|
8399
|
+
},
|
|
8400
|
+
CallExpression(node) {
|
|
8401
|
+
const entry = functionEntries.at(-1);
|
|
8402
|
+
if (entry == null) return;
|
|
8403
|
+
if (isUseStateCall(node)) return;
|
|
8404
|
+
facts.calls.push({
|
|
8405
|
+
componentFunction: componentFnRef.current,
|
|
8406
|
+
enclosingFunction: entry.node,
|
|
8407
|
+
enclosingFunctionKind: entry.kind,
|
|
8408
|
+
node
|
|
8409
|
+
});
|
|
8410
|
+
},
|
|
8411
|
+
ReturnStatement(node) {
|
|
8412
|
+
const componentFn = componentFnRef.current;
|
|
8413
|
+
if (componentFn == null) return;
|
|
8414
|
+
const entry = functionEntries.at(-1);
|
|
8415
|
+
if (entry == null || entry.node !== componentFn) return;
|
|
8416
|
+
if (componentFn.body.type !== AST_NODE_TYPES.BlockStatement) return;
|
|
8417
|
+
const body = componentFn.body.body;
|
|
8418
|
+
let stmt = node;
|
|
8419
|
+
while (stmt.parent !== componentFn.body) {
|
|
8420
|
+
if (stmt.parent == null) return;
|
|
8421
|
+
stmt = stmt.parent;
|
|
8422
|
+
}
|
|
8423
|
+
const idx = body.indexOf(stmt);
|
|
8424
|
+
if (idx !== -1 && idx < body.length - 1) facts.earlyReturns.push({
|
|
8425
|
+
componentFunction: componentFn,
|
|
8426
|
+
node
|
|
8427
|
+
});
|
|
8428
|
+
}
|
|
8137
8429
|
}
|
|
8430
|
+
};
|
|
8431
|
+
}
|
|
8432
|
+
|
|
8433
|
+
//#endregion
|
|
8434
|
+
//#region src/rules/set-state-in-render/origins.ts
|
|
8435
|
+
/**
|
|
8436
|
+
* Create a resolver that answers whether an identifier or call originates from
|
|
8437
|
+
* a `useState`-like hook's setter function (the element at index 1 of the
|
|
8438
|
+
* returned tuple).
|
|
8439
|
+
* @param context The rule context
|
|
8440
|
+
* @returns The setState provenance resolver
|
|
8441
|
+
*/
|
|
8442
|
+
function createSetStateResolver(context) {
|
|
8443
|
+
const { additionalStateHooks } = getSettingsFromContext(context);
|
|
8444
|
+
function isUseStateCall(node) {
|
|
8445
|
+
return core.isUseStateLikeCall(node, additionalStateHooks);
|
|
8138
8446
|
}
|
|
8139
|
-
function
|
|
8447
|
+
function isSetStateId(id, at) {
|
|
8140
8448
|
const initNode = resolveOrigin(context, id);
|
|
8141
8449
|
if (initNode == null) return false;
|
|
8142
8450
|
if (initNode.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
@@ -8156,194 +8464,53 @@ function create$5(context) {
|
|
|
8156
8464
|
const [index] = callee.arguments;
|
|
8157
8465
|
if (!isAt || index == null) return false;
|
|
8158
8466
|
const indexScope = context.sourceCode.getScope(node);
|
|
8159
|
-
return getStaticValue(index, indexScope)?.value === 1 &&
|
|
8467
|
+
return getStaticValue(index, indexScope)?.value === 1 && isSetStateId(innerCallee.object);
|
|
8160
8468
|
}
|
|
8161
|
-
case AST_NODE_TYPES.Identifier: return
|
|
8469
|
+
case AST_NODE_TYPES.Identifier: return isSetStateId(callee, 1);
|
|
8162
8470
|
case AST_NODE_TYPES.MemberExpression: {
|
|
8163
8471
|
if (!("name" in callee.object)) return false;
|
|
8164
8472
|
const property = callee.property;
|
|
8165
8473
|
const propertyScope = context.sourceCode.getScope(node);
|
|
8166
|
-
return getStaticValue(property, propertyScope)?.value === 1 &&
|
|
8474
|
+
return getStaticValue(property, propertyScope)?.value === 1 && isSetStateId(callee.object, 1);
|
|
8167
8475
|
}
|
|
8168
8476
|
default: return false;
|
|
8169
8477
|
}
|
|
8170
8478
|
}
|
|
8171
|
-
function isArgumentUsingRefValue(context, node) {
|
|
8172
|
-
const isUsingRefValue = (n) => {
|
|
8173
|
-
switch (n.type) {
|
|
8174
|
-
case AST_NODE_TYPES.Identifier: return isInitializedFromRef(context, n.name, context.sourceCode.getScope(n));
|
|
8175
|
-
case AST_NODE_TYPES.MemberExpression: return isUsingRefValue(n.object);
|
|
8176
|
-
case AST_NODE_TYPES.CallExpression: return isUsingRefValue(n.callee) || getNestedIdentifiers(n).some(isUsingRefValue);
|
|
8177
|
-
case AST_NODE_TYPES.BinaryExpression:
|
|
8178
|
-
case AST_NODE_TYPES.LogicalExpression: return isUsingRefValue(n.left) || isUsingRefValue(n.right);
|
|
8179
|
-
case AST_NODE_TYPES.UnaryExpression:
|
|
8180
|
-
case AST_NODE_TYPES.UpdateExpression: return isUsingRefValue(n.argument);
|
|
8181
|
-
case AST_NODE_TYPES.ConditionalExpression: return isUsingRefValue(n.consequent) || isUsingRefValue(n.alternate);
|
|
8182
|
-
case AST_NODE_TYPES.SequenceExpression: return n.expressions.some(isUsingRefValue);
|
|
8183
|
-
case AST_NODE_TYPES.AssignmentExpression: return isUsingRefValue(n.right);
|
|
8184
|
-
default: return false;
|
|
8185
|
-
}
|
|
8186
|
-
};
|
|
8187
|
-
if (isUsingRefValue(node)) return true;
|
|
8188
|
-
return Check.isFunction(node) && context.sourceCode.getScope(node.body).references.some((r) => isUsingRefValue(r.identifier));
|
|
8189
|
-
}
|
|
8190
8479
|
return {
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
kind,
|
|
8195
|
-
node
|
|
8196
|
-
});
|
|
8197
|
-
if (kind === "setup") onSetupFunctionEnter(node);
|
|
8198
|
-
},
|
|
8199
|
-
":function:exit"(node) {
|
|
8200
|
-
const { kind } = functionEntries.at(-1) ?? {};
|
|
8201
|
-
if (kind === "setup") onSetupFunctionExit(node);
|
|
8202
|
-
functionEntries.pop();
|
|
8203
|
-
},
|
|
8204
|
-
CallExpression(node) {
|
|
8205
|
-
const setupFunction = setupFnRef.current;
|
|
8206
|
-
const entry = functionEntries.at(-1);
|
|
8207
|
-
if (entry == null || entry.node.async) return;
|
|
8208
|
-
match(getCallKind(node)).with("setState", () => {
|
|
8209
|
-
switch (true) {
|
|
8210
|
-
case entry.kind === "deferred":
|
|
8211
|
-
case entry.node.async: break;
|
|
8212
|
-
case entry.node === setupFunction:
|
|
8213
|
-
case entry.kind === "immediate" && Traverse.findParent(entry.node, Check.isFunction) === setupFunction: {
|
|
8214
|
-
const args0 = node.arguments.at(0);
|
|
8215
|
-
if (args0 == null) return;
|
|
8216
|
-
if (isArgumentUsingRefValue(context, args0)) return;
|
|
8217
|
-
if (isRefGatedContext(context, node)) return;
|
|
8218
|
-
context.report({
|
|
8219
|
-
data: { name: context.sourceCode.getText(Extract.unwrap(node.callee)) },
|
|
8220
|
-
messageId: "default",
|
|
8221
|
-
node
|
|
8222
|
-
});
|
|
8223
|
-
return;
|
|
8224
|
-
}
|
|
8225
|
-
default: {
|
|
8226
|
-
const init = Traverse.findParent(node, isHookDecl)?.init;
|
|
8227
|
-
if (init == null) getOrInsertComputed(setStateCallsByFn, entry.node, () => []).push(node);
|
|
8228
|
-
else getOrInsertComputed(setStateInHookCallbacks, init, () => []).push(node);
|
|
8229
|
-
}
|
|
8230
|
-
}
|
|
8231
|
-
}).with("useEffect", () => {
|
|
8232
|
-
if (Check.isFunction(node.arguments.at(0))) return;
|
|
8233
|
-
setupFnIds.push(...getNestedIdentifiers(node));
|
|
8234
|
-
}).with("other", () => {
|
|
8235
|
-
if (entry.node !== setupFunction) return;
|
|
8236
|
-
trackedFnCalls.push(node);
|
|
8237
|
-
}).otherwise(constVoid);
|
|
8238
|
-
},
|
|
8239
|
-
Identifier(node) {
|
|
8240
|
-
if (node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee === node) return;
|
|
8241
|
-
if (!isIdFromUseStateCall(node, 1)) return;
|
|
8242
|
-
switch (node.parent.type) {
|
|
8243
|
-
case AST_NODE_TYPES.ArrowFunctionExpression: {
|
|
8244
|
-
const parent = node.parent.parent;
|
|
8245
|
-
if (parent.type !== AST_NODE_TYPES.CallExpression) break;
|
|
8246
|
-
if (!core.isUseMemoCall(context, parent)) break;
|
|
8247
|
-
const init = Traverse.findParent(parent, isHookDecl)?.init;
|
|
8248
|
-
if (init != null) getOrInsertComputed(setStateInEffectArg, init, () => []).push(node);
|
|
8249
|
-
break;
|
|
8250
|
-
}
|
|
8251
|
-
case AST_NODE_TYPES.CallExpression:
|
|
8252
|
-
if (node !== node.parent.arguments.at(0)) break;
|
|
8253
|
-
if (core.isUseCallbackCall(context, node.parent)) {
|
|
8254
|
-
const init = Traverse.findParent(node.parent, isHookDecl)?.init;
|
|
8255
|
-
if (init != null) getOrInsertComputed(setStateInEffectArg, init, () => []).push(node);
|
|
8256
|
-
break;
|
|
8257
|
-
}
|
|
8258
|
-
if (isUseEffectCall(node.parent)) getOrInsertComputed(setStateInEffectSetup, node.parent, () => []).push(node);
|
|
8259
|
-
}
|
|
8260
|
-
},
|
|
8261
|
-
"Program:exit"() {
|
|
8262
|
-
const isExemptSetStateCall = (setStateCall) => {
|
|
8263
|
-
const callNode = getSetStateCallExpression(setStateCall);
|
|
8264
|
-
if (isRefGatedContext(context, callNode)) return true;
|
|
8265
|
-
if (callNode.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
8266
|
-
const args0 = callNode.arguments.at(0);
|
|
8267
|
-
return args0 != null && isArgumentUsingRefValue(context, args0);
|
|
8268
|
-
};
|
|
8269
|
-
const getSetStateCalls = (context, id) => {
|
|
8270
|
-
const variable = findVariable(context.sourceCode.getScope(id), id);
|
|
8271
|
-
if (variable != null && variable.defs.some((def) => def.type === DefinitionType.Parameter)) return [];
|
|
8272
|
-
const node = resolveOrigin(context, id);
|
|
8273
|
-
switch (node?.type) {
|
|
8274
|
-
case AST_NODE_TYPES.ArrowFunctionExpression:
|
|
8275
|
-
case AST_NODE_TYPES.FunctionDeclaration:
|
|
8276
|
-
case AST_NODE_TYPES.FunctionExpression: return setStateCallsByFn.get(node) ?? [];
|
|
8277
|
-
case AST_NODE_TYPES.CallExpression: return setStateInHookCallbacks.get(node) ?? setStateInEffectArg.get(node) ?? [];
|
|
8278
|
-
}
|
|
8279
|
-
return [];
|
|
8280
|
-
};
|
|
8281
|
-
for (const [, calls] of setStateInEffectSetup) for (const call of calls) {
|
|
8282
|
-
if (isExemptSetStateCall(call)) continue;
|
|
8283
|
-
context.report({
|
|
8284
|
-
data: { name: call.name },
|
|
8285
|
-
messageId: "default",
|
|
8286
|
-
node: call
|
|
8287
|
-
});
|
|
8288
|
-
}
|
|
8289
|
-
for (const { callee } of trackedFnCalls) {
|
|
8290
|
-
const unwrappedCallee = Extract.unwrap(callee);
|
|
8291
|
-
if (!Check.isIdentifier(unwrappedCallee)) continue;
|
|
8292
|
-
const setStateCalls = getSetStateCalls(context, unwrappedCallee);
|
|
8293
|
-
for (const setStateCall of setStateCalls) {
|
|
8294
|
-
if (isExemptSetStateCall(setStateCall)) continue;
|
|
8295
|
-
context.report({
|
|
8296
|
-
data: { name: getCallName(setStateCall) },
|
|
8297
|
-
messageId: "default",
|
|
8298
|
-
node: setStateCall
|
|
8299
|
-
});
|
|
8300
|
-
}
|
|
8301
|
-
}
|
|
8302
|
-
for (const id of setupFnIds) {
|
|
8303
|
-
const setStateCalls = getSetStateCalls(context, id);
|
|
8304
|
-
for (const setStateCall of setStateCalls) {
|
|
8305
|
-
if (isExemptSetStateCall(setStateCall)) continue;
|
|
8306
|
-
context.report({
|
|
8307
|
-
data: { name: getCallName(setStateCall) },
|
|
8308
|
-
messageId: "default",
|
|
8309
|
-
node: setStateCall
|
|
8310
|
-
});
|
|
8311
|
-
}
|
|
8312
|
-
}
|
|
8313
|
-
}
|
|
8480
|
+
isSetStateCall,
|
|
8481
|
+
isSetStateId,
|
|
8482
|
+
isUseStateCall
|
|
8314
8483
|
};
|
|
8315
8484
|
}
|
|
8316
8485
|
|
|
8317
8486
|
//#endregion
|
|
8318
|
-
//#region src/rules/set-state-in-render/
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
if (
|
|
8337
|
-
|
|
8487
|
+
//#region src/rules/set-state-in-render/effects.ts
|
|
8488
|
+
/**
|
|
8489
|
+
* Infer the setState calls that run unconditionally during render from the
|
|
8490
|
+
* collected facts: calls made directly in a component function body that are
|
|
8491
|
+
* neither guarded by a conditional nor positioned after an early return.
|
|
8492
|
+
* @param context The rule context
|
|
8493
|
+
* @param facts The collected facts
|
|
8494
|
+
* @returns The violations to report, in report order
|
|
8495
|
+
*/
|
|
8496
|
+
function inferViolations$1(context, facts) {
|
|
8497
|
+
const resolver = createSetStateResolver(context);
|
|
8498
|
+
const getText = (n) => context.sourceCode.getText(n);
|
|
8499
|
+
const earlyReturnPositions = /* @__PURE__ */ new Map();
|
|
8500
|
+
for (const { componentFunction, node } of facts.earlyReturns) getOrInsertComputed(earlyReturnPositions, componentFunction, () => []).push(node.range[0]);
|
|
8501
|
+
const violations = [];
|
|
8502
|
+
for (const { componentFunction, enclosingFunction, node } of facts.calls) {
|
|
8503
|
+
if (componentFunction == null) continue;
|
|
8504
|
+
if (enclosingFunction !== componentFunction) continue;
|
|
8505
|
+
if (!resolver.isSetStateCall(node)) continue;
|
|
8506
|
+
if (isInsideConditional(node, componentFunction)) continue;
|
|
8507
|
+
if (earlyReturnPositions.get(componentFunction)?.some((pos) => pos < node.range[0]) === true) continue;
|
|
8508
|
+
violations.push({
|
|
8509
|
+
name: getText(Extract.unwrap(node.callee)),
|
|
8510
|
+
node
|
|
8511
|
+
});
|
|
8338
8512
|
}
|
|
8339
|
-
return
|
|
8340
|
-
}
|
|
8341
|
-
function isComponentOrHookLikeFunction(node) {
|
|
8342
|
-
const id = core.getFunctionId(node);
|
|
8343
|
-
if (id == null) return false;
|
|
8344
|
-
if (Check.isIdentifier(id)) return core.isFunctionComponentName(id.name) || core.isHookName(id.name);
|
|
8345
|
-
if (id.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(id.property)) return core.isFunctionComponentName(id.property.name) || core.isHookName(id.property.name);
|
|
8346
|
-
return false;
|
|
8513
|
+
return violations;
|
|
8347
8514
|
}
|
|
8348
8515
|
|
|
8349
8516
|
//#endregion
|
|
@@ -8362,103 +8529,19 @@ var set_state_in_render_default = createRule({
|
|
|
8362
8529
|
});
|
|
8363
8530
|
function create$4(context) {
|
|
8364
8531
|
const { additionalStateHooks } = getSettingsFromContext(context);
|
|
8365
|
-
const
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
if (initNode == null) return false;
|
|
8374
|
-
if (initNode.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
8375
|
-
if (!isUseStateCall(initNode)) return false;
|
|
8376
|
-
const initNodeParent = initNode.parent;
|
|
8377
|
-
if (!("id" in initNodeParent) || initNodeParent.id?.type !== AST_NODE_TYPES.ArrayPattern) return true;
|
|
8378
|
-
return initNodeParent.id.elements.findIndex((e) => e != null && Check.isIdentifier(e, topLevelId.name)) === at;
|
|
8379
|
-
}
|
|
8380
|
-
function isSetStateCall(node) {
|
|
8381
|
-
const callee = Extract.unwrap(node.callee);
|
|
8382
|
-
switch (callee.type) {
|
|
8383
|
-
case AST_NODE_TYPES.CallExpression: {
|
|
8384
|
-
const innerCallee = Extract.unwrap(callee.callee);
|
|
8385
|
-
if (innerCallee.type !== AST_NODE_TYPES.MemberExpression) return false;
|
|
8386
|
-
if (!("name" in innerCallee.object)) return false;
|
|
8387
|
-
const isAt = Extract.getCalleeName(callee) === "at";
|
|
8388
|
-
const [index] = callee.arguments;
|
|
8389
|
-
if (!isAt || index == null) return false;
|
|
8390
|
-
const indexScope = context.sourceCode.getScope(node);
|
|
8391
|
-
return getStaticValue(index, indexScope)?.value === 1 && isIdFromUseStateCall(innerCallee.object);
|
|
8392
|
-
}
|
|
8393
|
-
case AST_NODE_TYPES.Identifier: return isIdFromUseStateCall(callee, 1);
|
|
8394
|
-
case AST_NODE_TYPES.MemberExpression: {
|
|
8395
|
-
if (!("name" in callee.object)) return false;
|
|
8396
|
-
const property = callee.property;
|
|
8397
|
-
const propertyScope = context.sourceCode.getScope(node);
|
|
8398
|
-
return getStaticValue(property, propertyScope)?.value === 1 && isIdFromUseStateCall(callee.object, 1);
|
|
8399
|
-
}
|
|
8400
|
-
default: return false;
|
|
8401
|
-
}
|
|
8402
|
-
}
|
|
8403
|
-
function getFunctionKind(node) {
|
|
8404
|
-
if (isComponentOrHookLikeFunction(node)) return "component";
|
|
8405
|
-
const parent = Traverse.findParent(node, not(Check.isTypeExpression)) ?? node.parent;
|
|
8406
|
-
if (parent.type === AST_NODE_TYPES.CallExpression && parent.callee !== node) return "callback";
|
|
8407
|
-
return "other";
|
|
8408
|
-
}
|
|
8409
|
-
return {
|
|
8410
|
-
":function"(node) {
|
|
8411
|
-
const kind = getFunctionKind(node);
|
|
8412
|
-
functionEntries.push({
|
|
8413
|
-
kind,
|
|
8414
|
-
node
|
|
8415
|
-
});
|
|
8416
|
-
if (kind === "component") {
|
|
8417
|
-
componentFnRef.current = node;
|
|
8418
|
-
componentHasEarlyReturn.current = false;
|
|
8419
|
-
}
|
|
8420
|
-
},
|
|
8421
|
-
":function:exit"(node) {
|
|
8422
|
-
if (functionEntries.at(-1)?.kind === "component" && componentFnRef.current === node) {
|
|
8423
|
-
componentFnRef.current = null;
|
|
8424
|
-
componentHasEarlyReturn.current = false;
|
|
8425
|
-
}
|
|
8426
|
-
functionEntries.pop();
|
|
8427
|
-
},
|
|
8428
|
-
CallExpression(node) {
|
|
8429
|
-
const componentFn = componentFnRef.current;
|
|
8430
|
-
if (componentFn == null) return;
|
|
8431
|
-
if (!isSetStateCall(node)) return;
|
|
8432
|
-
if (isInsideEventHandler(node, componentFn)) return;
|
|
8433
|
-
if (isInsideConditional(node, componentFn)) return;
|
|
8434
|
-
if (componentHasEarlyReturn.current) return;
|
|
8435
|
-
context.report({
|
|
8436
|
-
data: { name: context.sourceCode.getText(Extract.unwrap(node.callee)) },
|
|
8437
|
-
messageId: "default",
|
|
8438
|
-
node
|
|
8439
|
-
});
|
|
8440
|
-
},
|
|
8441
|
-
ReturnStatement(node) {
|
|
8442
|
-
const componentFn = componentFnRef.current;
|
|
8443
|
-
if (componentFn == null) return;
|
|
8444
|
-
const entry = functionEntries.at(-1);
|
|
8445
|
-
if (entry == null || entry.node !== componentFn) return;
|
|
8446
|
-
if (componentFn.body.type !== AST_NODE_TYPES.BlockStatement) return;
|
|
8447
|
-
const body = componentFn.body.body;
|
|
8448
|
-
let stmt = node;
|
|
8449
|
-
while (stmt.parent !== componentFn.body) {
|
|
8450
|
-
if (stmt.parent == null) return;
|
|
8451
|
-
stmt = stmt.parent;
|
|
8452
|
-
}
|
|
8453
|
-
const idx = body.indexOf(stmt);
|
|
8454
|
-
if (idx !== -1 && idx < body.length - 1) componentHasEarlyReturn.current = true;
|
|
8455
|
-
}
|
|
8456
|
-
};
|
|
8532
|
+
const facts = createFactCollector$2(additionalStateHooks);
|
|
8533
|
+
return merge(facts.visitor, { "Program:exit"() {
|
|
8534
|
+
for (const violation of inferViolations$1(context, facts.facts)) context.report({
|
|
8535
|
+
data: { name: violation.name },
|
|
8536
|
+
messageId: "default",
|
|
8537
|
+
node: violation.node
|
|
8538
|
+
});
|
|
8539
|
+
} });
|
|
8457
8540
|
}
|
|
8458
8541
|
|
|
8459
8542
|
//#endregion
|
|
8460
8543
|
//#region src/rules/static-components/collect.ts
|
|
8461
|
-
function createFactCollector() {
|
|
8544
|
+
function createFactCollector$1() {
|
|
8462
8545
|
const facts = { componentUsages: [] };
|
|
8463
8546
|
return {
|
|
8464
8547
|
facts,
|
|
@@ -8606,7 +8689,7 @@ var static_components_default = createRule({
|
|
|
8606
8689
|
docs: { description: "Validates that components are static, not recreated every render." },
|
|
8607
8690
|
messages: {
|
|
8608
8691
|
default: "Cannot create components during render. Components created during render will reset their state each time they are created. Declare components outside of render.",
|
|
8609
|
-
|
|
8692
|
+
"created-here": "The component is created during render here."
|
|
8610
8693
|
},
|
|
8611
8694
|
schema: []
|
|
8612
8695
|
},
|
|
@@ -8618,7 +8701,7 @@ function create$3(context) {
|
|
|
8618
8701
|
const hint = core.FunctionComponentDetectionHint.DoNotIncludeJsxWithNumberValue | core.FunctionComponentDetectionHint.DoNotIncludeJsxWithBooleanValue | core.FunctionComponentDetectionHint.DoNotIncludeJsxWithNullValue | core.FunctionComponentDetectionHint.DoNotIncludeJsxWithStringValue | core.FunctionComponentDetectionHint.DoNotIncludeJsxWithUndefinedValue | core.FunctionComponentDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx | core.FunctionComponentDetectionHint.RequireBothBranchesOfConditionalExpressionToBeJsx | core.FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayPatternElement | core.FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayExpressionElement | core.FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback;
|
|
8619
8702
|
const fc = core.getFunctionComponentCollector(context, { hint });
|
|
8620
8703
|
const cc = core.getClassComponentCollector(context);
|
|
8621
|
-
const facts = createFactCollector();
|
|
8704
|
+
const facts = createFactCollector$1();
|
|
8622
8705
|
return merge(fc.visitor, cc.visitor, facts.visitor, { "Program:exit"(program) {
|
|
8623
8706
|
const componentNodes = [...fc.api.getAllComponents(program), ...cc.api.getAllComponents(program)].map((component) => component.node);
|
|
8624
8707
|
const isInsideRender = createRenderBoundaryChecker(componentNodes);
|
|
@@ -8633,7 +8716,7 @@ function create$3(context) {
|
|
|
8633
8716
|
reportedCreations.add(effect.creationNode);
|
|
8634
8717
|
context.report({
|
|
8635
8718
|
data: { name: effect.name },
|
|
8636
|
-
messageId: "
|
|
8719
|
+
messageId: "created-here",
|
|
8637
8720
|
node: effect.creationNode
|
|
8638
8721
|
});
|
|
8639
8722
|
}
|
|
@@ -8717,14 +8800,17 @@ function create$2(context) {
|
|
|
8717
8800
|
}
|
|
8718
8801
|
|
|
8719
8802
|
//#endregion
|
|
8720
|
-
//#region src/rules/use-memo/
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8803
|
+
//#region src/rules/use-memo/helpers.ts
|
|
8804
|
+
/**
|
|
8805
|
+
* Check whether the result of a `useMemo` call is used (assigned, returned,
|
|
8806
|
+
* passed as an argument, etc.) rather than discarded as a bare statement.
|
|
8807
|
+
* @param node The `useMemo` call expression
|
|
8808
|
+
* @returns `true` when the call result flows into a surrounding expression
|
|
8809
|
+
*/
|
|
8810
|
+
function isResultUsed(node) {
|
|
8811
|
+
let parent = node.parent;
|
|
8812
|
+
while (Check.isTypeExpression(parent)) parent = parent.parent;
|
|
8813
|
+
return parent.type === AST_NODE_TYPES.VariableDeclarator || parent.type === AST_NODE_TYPES.AssignmentExpression || parent.type === AST_NODE_TYPES.AssignmentPattern || parent.type === AST_NODE_TYPES.Property || parent.type === AST_NODE_TYPES.ReturnStatement || parent.type === AST_NODE_TYPES.JSXExpressionContainer || parent.type === AST_NODE_TYPES.CallExpression || parent.type === AST_NODE_TYPES.NewExpression || parent.type === AST_NODE_TYPES.ArrayExpression || parent.type === AST_NODE_TYPES.ConditionalExpression || parent.type === AST_NODE_TYPES.LogicalExpression || parent.type === AST_NODE_TYPES.SequenceExpression || parent.type === AST_NODE_TYPES.SpreadElement || parent.type === AST_NODE_TYPES.TemplateLiteral || parent.type === AST_NODE_TYPES.BinaryExpression || parent.type === AST_NODE_TYPES.UnaryExpression || parent.type === AST_NODE_TYPES.MemberExpression || parent.type === AST_NODE_TYPES.TaggedTemplateExpression || parent.type === AST_NODE_TYPES.ChainExpression || parent.type === AST_NODE_TYPES.ArrowFunctionExpression || parent.type === AST_NODE_TYPES.ForOfStatement || parent.type === AST_NODE_TYPES.ForInStatement;
|
|
8728
8814
|
}
|
|
8729
8815
|
/**
|
|
8730
8816
|
* Gets the nested return statements in the node that are within the same function
|
|
@@ -8733,108 +8819,168 @@ function isDeclaredInsideCallback(variable, callback) {
|
|
|
8733
8819
|
*/
|
|
8734
8820
|
function getNestedReturnStatements(node) {
|
|
8735
8821
|
const statements = [];
|
|
8736
|
-
const
|
|
8822
|
+
const boundary = Check.isFunction(node) ? node : Traverse.findParent(node, Check.isFunction);
|
|
8737
8823
|
simpleTraverse(node, { enter(node) {
|
|
8738
8824
|
if (node.type !== AST_NODE_TYPES.ReturnStatement) return;
|
|
8739
|
-
if (Traverse.findParent(node, Check.isFunction, (n) => n ===
|
|
8825
|
+
if (Traverse.findParent(node, Check.isFunction, (n) => n === boundary) != null) return;
|
|
8740
8826
|
statements.push(node);
|
|
8741
8827
|
} });
|
|
8742
8828
|
return statements;
|
|
8743
8829
|
}
|
|
8744
8830
|
|
|
8745
8831
|
//#endregion
|
|
8746
|
-
//#region src/rules/use-memo/
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
messages: {
|
|
8753
|
-
mustReturnAValue: "useMemo() callbacks must return a value.\n\nThis useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects.",
|
|
8754
|
-
noAsyncOrGeneratorFunctions: "useMemo() callbacks may not be async or generator functions.\n\nuseMemo() callbacks are called once and must synchronously return a value.",
|
|
8755
|
-
noParameters: "useMemo() callbacks may not accept parameters.\n\nuseMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.",
|
|
8756
|
-
noReassigningOuterVariables: "useMemo() callbacks may not reassign variables declared outside of the callback.\n\nuseMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function.",
|
|
8757
|
-
resultMustBeUsed: "useMemo() result is unused.\n\nThis useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects."
|
|
8758
|
-
},
|
|
8759
|
-
schema: []
|
|
8760
|
-
},
|
|
8761
|
-
name: RULE_NAME$1,
|
|
8762
|
-
create: create$1,
|
|
8763
|
-
defaultOptions: []
|
|
8764
|
-
});
|
|
8765
|
-
function create$1(context) {
|
|
8766
|
-
if (!context.sourceCode.text.includes("useMemo")) return {};
|
|
8767
|
-
function validateNoOuterVariableReassignment(callback) {
|
|
8768
|
-
const violations = [];
|
|
8769
|
-
if (callback.body == null) return violations;
|
|
8770
|
-
function checkWriteTarget(node, target) {
|
|
8832
|
+
//#region src/rules/use-memo/collect.ts
|
|
8833
|
+
function createFactCollector(isUseMemoCall) {
|
|
8834
|
+
const facts = { calls: [] };
|
|
8835
|
+
function collectWrites(callback) {
|
|
8836
|
+
const writes = [];
|
|
8837
|
+
function pushWrite(node, target) {
|
|
8771
8838
|
if (!Check.isIdentifier(target)) return;
|
|
8772
8839
|
if (Traverse.findParent(node, Check.isFunction, (n) => n === callback) != null) return;
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
violations.push({
|
|
8777
|
-
messageId: "noReassigningOuterVariables",
|
|
8778
|
-
node: target
|
|
8840
|
+
writes.push({
|
|
8841
|
+
node,
|
|
8842
|
+
target
|
|
8779
8843
|
});
|
|
8780
8844
|
}
|
|
8781
8845
|
simpleTraverse(callback.body, { enter(node) {
|
|
8782
8846
|
switch (node.type) {
|
|
8783
8847
|
case AST_NODE_TYPES.AssignmentExpression:
|
|
8784
|
-
for (const target of Extract.getAssignmentTargets(node.left))
|
|
8848
|
+
for (const target of Extract.getAssignmentTargets(node.left)) pushWrite(node, target);
|
|
8785
8849
|
return;
|
|
8786
8850
|
case AST_NODE_TYPES.ForInStatement:
|
|
8787
8851
|
case AST_NODE_TYPES.ForOfStatement:
|
|
8788
8852
|
if (node.left.type === AST_NODE_TYPES.VariableDeclaration) return;
|
|
8789
|
-
for (const target of Extract.getAssignmentTargets(node.left))
|
|
8853
|
+
for (const target of Extract.getAssignmentTargets(node.left)) pushWrite(node, target);
|
|
8790
8854
|
return;
|
|
8791
8855
|
}
|
|
8792
8856
|
} });
|
|
8793
|
-
return
|
|
8857
|
+
return writes;
|
|
8794
8858
|
}
|
|
8795
|
-
return {
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8859
|
+
return {
|
|
8860
|
+
facts,
|
|
8861
|
+
visitor: { CallExpression(node) {
|
|
8862
|
+
if (!isUseMemoCall(node)) return;
|
|
8863
|
+
const callbackArg = node.arguments[0] ?? null;
|
|
8864
|
+
const callback = callbackArg != null ? Extract.unwrap(callbackArg) : null;
|
|
8865
|
+
const callbackFunction = callback != null && Check.isFunction(callback) ? callback : null;
|
|
8866
|
+
facts.calls.push({
|
|
8867
|
+
callback: callbackFunction,
|
|
8868
|
+
callbackArg,
|
|
8869
|
+
isResultUsed: isResultUsed(node),
|
|
8870
|
+
node,
|
|
8871
|
+
returnStatements: callbackFunction != null ? getNestedReturnStatements(callbackFunction) : [],
|
|
8872
|
+
writes: callbackFunction != null ? collectWrites(callbackFunction) : []
|
|
8803
8873
|
});
|
|
8804
|
-
|
|
8874
|
+
} }
|
|
8875
|
+
};
|
|
8876
|
+
}
|
|
8877
|
+
|
|
8878
|
+
//#endregion
|
|
8879
|
+
//#region src/rules/use-memo/origins.ts
|
|
8880
|
+
function isDeclaredInsideCallback(variable, callback) {
|
|
8881
|
+
let scope = variable.scope;
|
|
8882
|
+
while (scope != null) {
|
|
8883
|
+
if (scope.block === callback) return true;
|
|
8884
|
+
scope = scope.upper;
|
|
8885
|
+
}
|
|
8886
|
+
return false;
|
|
8887
|
+
}
|
|
8888
|
+
/**
|
|
8889
|
+
* Resolve whether an identifier written inside a `useMemo` callback refers to a
|
|
8890
|
+
* variable declared outside the callback. Unresolved identifiers and variables
|
|
8891
|
+
* without definitions are treated as outer variables.
|
|
8892
|
+
* @param context The rule context
|
|
8893
|
+
* @param target The written identifier
|
|
8894
|
+
* @param callback The `useMemo` callback the write occurs in
|
|
8895
|
+
* @returns `true` when the write reassigns an outer variable
|
|
8896
|
+
*/
|
|
8897
|
+
function isOuterVariable(context, target, callback) {
|
|
8898
|
+
const scope = context.sourceCode.getScope(target);
|
|
8899
|
+
const variable = findVariable(scope, target);
|
|
8900
|
+
if (variable == null || variable.defs.length === 0) return true;
|
|
8901
|
+
return !isDeclaredInsideCallback(variable, callback);
|
|
8902
|
+
}
|
|
8903
|
+
|
|
8904
|
+
//#endregion
|
|
8905
|
+
//#region src/rules/use-memo/effects.ts
|
|
8906
|
+
/**
|
|
8907
|
+
* Infer the `useMemo` usage violations from the collected facts: unused
|
|
8908
|
+
* results, callbacks with parameters, async or generator callbacks, outer
|
|
8909
|
+
* variable reassignments, and callbacks that never return a value.
|
|
8910
|
+
* @param context The rule context
|
|
8911
|
+
* @param facts The collected facts
|
|
8912
|
+
* @returns The violations to report, in report order
|
|
8913
|
+
*/
|
|
8914
|
+
function inferViolations(context, facts) {
|
|
8915
|
+
const violations = [];
|
|
8916
|
+
for (const fact of facts.calls) {
|
|
8917
|
+
if (!fact.isResultUsed) {
|
|
8918
|
+
violations.push({
|
|
8919
|
+
kind: "result-must-be-used",
|
|
8920
|
+
node: fact.node
|
|
8921
|
+
});
|
|
8922
|
+
continue;
|
|
8805
8923
|
}
|
|
8806
|
-
const
|
|
8807
|
-
if (callbackArg == null)
|
|
8808
|
-
const callback = Extract.unwrap(callbackArg);
|
|
8809
|
-
if (!Check.isFunction(callback)) return;
|
|
8924
|
+
const { callback, callbackArg } = fact;
|
|
8925
|
+
if (callbackArg == null || callback == null) continue;
|
|
8810
8926
|
if (callback.params.length > 0) {
|
|
8811
8927
|
const firstParam = callback.params[0];
|
|
8812
|
-
if (firstParam == null)
|
|
8813
|
-
|
|
8814
|
-
|
|
8928
|
+
if (firstParam == null) continue;
|
|
8929
|
+
violations.push({
|
|
8930
|
+
kind: "no-parameters",
|
|
8815
8931
|
node: Check.isIdentifier(firstParam) ? firstParam : callback
|
|
8816
8932
|
});
|
|
8817
8933
|
}
|
|
8818
|
-
if (callback.async || callback.generator)
|
|
8819
|
-
|
|
8934
|
+
if (callback.async || callback.generator) violations.push({
|
|
8935
|
+
kind: "no-async-or-generator-functions",
|
|
8820
8936
|
node: callback
|
|
8821
8937
|
});
|
|
8822
|
-
for (const
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
context.report({
|
|
8828
|
-
messageId: "mustReturnAValue",
|
|
8829
|
-
node: callbackArg
|
|
8938
|
+
for (const write of fact.writes) {
|
|
8939
|
+
if (!isOuterVariable(context, write.target, callback)) continue;
|
|
8940
|
+
violations.push({
|
|
8941
|
+
kind: "no-reassigning-outer-variables",
|
|
8942
|
+
node: write.target
|
|
8830
8943
|
});
|
|
8831
|
-
return;
|
|
8832
8944
|
}
|
|
8833
|
-
if (
|
|
8834
|
-
|
|
8945
|
+
if (callback.type === AST_NODE_TYPES.ArrowFunctionExpression && callback.body.type !== AST_NODE_TYPES.BlockStatement) continue;
|
|
8946
|
+
if (callback.body.type !== AST_NODE_TYPES.BlockStatement) continue;
|
|
8947
|
+
if (!fact.returnStatements.some((stmt) => stmt.argument != null)) violations.push({
|
|
8948
|
+
kind: "must-return-a-value",
|
|
8835
8949
|
node: callbackArg
|
|
8836
8950
|
});
|
|
8837
|
-
}
|
|
8951
|
+
}
|
|
8952
|
+
return violations;
|
|
8953
|
+
}
|
|
8954
|
+
|
|
8955
|
+
//#endregion
|
|
8956
|
+
//#region src/rules/use-memo/use-memo.ts
|
|
8957
|
+
const RULE_NAME$1 = "use-memo";
|
|
8958
|
+
var use_memo_default = createRule({
|
|
8959
|
+
meta: {
|
|
8960
|
+
type: "problem",
|
|
8961
|
+
docs: { description: "Validates that 'useMemo' is called with a callback that returns a value." },
|
|
8962
|
+
messages: {
|
|
8963
|
+
"must-return-a-value": "useMemo() callbacks must return a value.\n\nThis useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects.",
|
|
8964
|
+
"no-async-or-generator-functions": "useMemo() callbacks may not be async or generator functions.\n\nuseMemo() callbacks are called once and must synchronously return a value.",
|
|
8965
|
+
"no-parameters": "useMemo() callbacks may not accept parameters.\n\nuseMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.",
|
|
8966
|
+
"no-reassigning-outer-variables": "useMemo() callbacks may not reassign variables declared outside of the callback.\n\nuseMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function.",
|
|
8967
|
+
"result-must-be-used": "useMemo() result is unused.\n\nThis useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects."
|
|
8968
|
+
},
|
|
8969
|
+
schema: []
|
|
8970
|
+
},
|
|
8971
|
+
name: RULE_NAME$1,
|
|
8972
|
+
create: create$1,
|
|
8973
|
+
defaultOptions: []
|
|
8974
|
+
});
|
|
8975
|
+
function create$1(context) {
|
|
8976
|
+
if (!context.sourceCode.text.includes("useMemo")) return {};
|
|
8977
|
+
const facts = createFactCollector(core.isUseMemoCall(context));
|
|
8978
|
+
return merge(facts.visitor, { "Program:exit"() {
|
|
8979
|
+
for (const violation of inferViolations(context, facts.facts)) context.report({
|
|
8980
|
+
messageId: violation.kind,
|
|
8981
|
+
node: violation.node
|
|
8982
|
+
});
|
|
8983
|
+
} });
|
|
8838
8984
|
}
|
|
8839
8985
|
|
|
8840
8986
|
//#endregion
|
|
@@ -8844,6 +8990,59 @@ const LAZY_INIT_ALLOW_LIST = [
|
|
|
8844
8990
|
"String",
|
|
8845
8991
|
"Number"
|
|
8846
8992
|
];
|
|
8993
|
+
const EXPRESSION_VISITOR_KEYS = {
|
|
8994
|
+
[AST_NODE_TYPES.ArrayExpression]: ["elements"],
|
|
8995
|
+
[AST_NODE_TYPES.ArrayPattern]: ["elements"],
|
|
8996
|
+
[AST_NODE_TYPES.AssignmentExpression]: ["left", "right"],
|
|
8997
|
+
[AST_NODE_TYPES.AssignmentPattern]: ["left", "right"],
|
|
8998
|
+
[AST_NODE_TYPES.AwaitExpression]: ["argument"],
|
|
8999
|
+
[AST_NODE_TYPES.BinaryExpression]: ["left", "right"],
|
|
9000
|
+
[AST_NODE_TYPES.CallExpression]: ["callee", "arguments"],
|
|
9001
|
+
[AST_NODE_TYPES.ChainExpression]: ["expression"],
|
|
9002
|
+
[AST_NODE_TYPES.ConditionalExpression]: [
|
|
9003
|
+
"test",
|
|
9004
|
+
"consequent",
|
|
9005
|
+
"alternate"
|
|
9006
|
+
],
|
|
9007
|
+
[AST_NODE_TYPES.Decorator]: ["expression"],
|
|
9008
|
+
[AST_NODE_TYPES.DoWhileStatement]: ["test"],
|
|
9009
|
+
[AST_NODE_TYPES.ExpressionStatement]: ["expression"],
|
|
9010
|
+
[AST_NODE_TYPES.ForInStatement]: ["left", "right"],
|
|
9011
|
+
[AST_NODE_TYPES.ForOfStatement]: ["left", "right"],
|
|
9012
|
+
[AST_NODE_TYPES.ForStatement]: ["test"],
|
|
9013
|
+
[AST_NODE_TYPES.IfStatement]: [
|
|
9014
|
+
"test",
|
|
9015
|
+
"consequent",
|
|
9016
|
+
"alternate"
|
|
9017
|
+
],
|
|
9018
|
+
[AST_NODE_TYPES.ImportExpression]: ["source"],
|
|
9019
|
+
[AST_NODE_TYPES.JSXExpressionContainer]: ["expression"],
|
|
9020
|
+
[AST_NODE_TYPES.JSXSpreadChild]: ["expression"],
|
|
9021
|
+
[AST_NODE_TYPES.LogicalExpression]: ["left", "right"],
|
|
9022
|
+
[AST_NODE_TYPES.MemberExpression]: ["object", "property"],
|
|
9023
|
+
[AST_NODE_TYPES.NewExpression]: ["callee", "arguments"],
|
|
9024
|
+
[AST_NODE_TYPES.ObjectExpression]: ["properties"],
|
|
9025
|
+
[AST_NODE_TYPES.ObjectPattern]: ["properties"],
|
|
9026
|
+
[AST_NODE_TYPES.Property]: ["value"],
|
|
9027
|
+
[AST_NODE_TYPES.SequenceExpression]: ["expressions"],
|
|
9028
|
+
[AST_NODE_TYPES.SpreadElement]: ["argument"],
|
|
9029
|
+
[AST_NODE_TYPES.SwitchCase]: ["test", "consequent"],
|
|
9030
|
+
[AST_NODE_TYPES.TaggedTemplateExpression]: ["tag", "quasi"],
|
|
9031
|
+
[AST_NODE_TYPES.TemplateLiteral]: ["expressions"],
|
|
9032
|
+
[AST_NODE_TYPES.TSAsExpression]: ["expression"],
|
|
9033
|
+
[AST_NODE_TYPES.TSClassImplements]: ["expression"],
|
|
9034
|
+
[AST_NODE_TYPES.TSExportAssignment]: ["expression"],
|
|
9035
|
+
[AST_NODE_TYPES.TSExternalModuleReference]: ["expression"],
|
|
9036
|
+
[AST_NODE_TYPES.TSInstantiationExpression]: ["expression"],
|
|
9037
|
+
[AST_NODE_TYPES.TSInterfaceHeritage]: ["expression"],
|
|
9038
|
+
[AST_NODE_TYPES.TSNonNullExpression]: ["expression"],
|
|
9039
|
+
[AST_NODE_TYPES.TSSatisfiesExpression]: ["expression"],
|
|
9040
|
+
[AST_NODE_TYPES.TSTypeAssertion]: ["expression"],
|
|
9041
|
+
[AST_NODE_TYPES.UnaryExpression]: ["argument"],
|
|
9042
|
+
[AST_NODE_TYPES.UpdateExpression]: ["argument"],
|
|
9043
|
+
[AST_NODE_TYPES.WhileStatement]: ["test"],
|
|
9044
|
+
[AST_NODE_TYPES.YieldExpression]: ["argument"]
|
|
9045
|
+
};
|
|
8847
9046
|
/**
|
|
8848
9047
|
* Get all nested expressions of type T in an expression like node
|
|
8849
9048
|
* @param type The type of the expression to retrieve within the node
|
|
@@ -8854,72 +9053,13 @@ function getNestedExpressionsOfType(type) {
|
|
|
8854
9053
|
const isNodeOfType = Check.is(type);
|
|
8855
9054
|
const recurse = (node) => {
|
|
8856
9055
|
const expressions = [];
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
expressions.push(...chunk);
|
|
8865
|
-
}
|
|
8866
|
-
if ("left" in node) {
|
|
8867
|
-
const chunk = recurse(node.left);
|
|
8868
|
-
expressions.push(...chunk);
|
|
8869
|
-
}
|
|
8870
|
-
if ("right" in node) {
|
|
8871
|
-
const chunk = recurse(node.right);
|
|
8872
|
-
expressions.push(...chunk);
|
|
8873
|
-
}
|
|
8874
|
-
if ("test" in node && node.test != null) {
|
|
8875
|
-
const chunk = recurse(node.test);
|
|
8876
|
-
expressions.push(...chunk);
|
|
8877
|
-
}
|
|
8878
|
-
if ("consequent" in node) {
|
|
8879
|
-
const chunk = Array.isArray(node.consequent) ? node.consequent.flatMap(recurse) : recurse(node.consequent);
|
|
8880
|
-
expressions.push(...chunk);
|
|
8881
|
-
}
|
|
8882
|
-
if ("alternate" in node && node.alternate != null) {
|
|
8883
|
-
const chunk = Array.isArray(node.alternate) ? node.alternate.flatMap(recurse) : recurse(node.alternate);
|
|
8884
|
-
expressions.push(...chunk);
|
|
8885
|
-
}
|
|
8886
|
-
if ("elements" in node) {
|
|
8887
|
-
const chunk = node.elements.filter((x) => x != null).flatMap(recurse);
|
|
8888
|
-
expressions.push(...chunk);
|
|
8889
|
-
}
|
|
8890
|
-
if ("properties" in node) {
|
|
8891
|
-
const chunk = node.properties.flatMap(recurse);
|
|
8892
|
-
expressions.push(...chunk);
|
|
8893
|
-
}
|
|
8894
|
-
if ("expressions" in node) {
|
|
8895
|
-
const chunk = node.expressions.flatMap(recurse);
|
|
8896
|
-
expressions.push(...chunk);
|
|
8897
|
-
}
|
|
8898
|
-
if (node.type === AST_NODE_TYPES.Property) {
|
|
8899
|
-
const chunk = recurse(node.value);
|
|
8900
|
-
expressions.push(...chunk);
|
|
8901
|
-
}
|
|
8902
|
-
if (node.type === AST_NODE_TYPES.SpreadElement) {
|
|
8903
|
-
const chunk = recurse(node.argument);
|
|
8904
|
-
expressions.push(...chunk);
|
|
8905
|
-
}
|
|
8906
|
-
if (node.type === AST_NODE_TYPES.MemberExpression) {
|
|
8907
|
-
expressions.push(...recurse(node.object));
|
|
8908
|
-
if (node.computed) expressions.push(...recurse(node.property));
|
|
8909
|
-
}
|
|
8910
|
-
if (node.type === AST_NODE_TYPES.UnaryExpression) {
|
|
8911
|
-
const chunk = recurse(node.argument);
|
|
8912
|
-
expressions.push(...chunk);
|
|
8913
|
-
}
|
|
8914
|
-
if (node.type === AST_NODE_TYPES.AwaitExpression) expressions.push(...recurse(node.argument));
|
|
8915
|
-
if (node.type === AST_NODE_TYPES.YieldExpression && node.argument != null) expressions.push(...recurse(node.argument));
|
|
8916
|
-
if (node.type === AST_NODE_TYPES.UpdateExpression) expressions.push(...recurse(node.argument));
|
|
8917
|
-
if (node.type === AST_NODE_TYPES.CallExpression || node.type === AST_NODE_TYPES.NewExpression) expressions.push(...recurse(node.callee));
|
|
8918
|
-
if (node.type === AST_NODE_TYPES.TaggedTemplateExpression) {
|
|
8919
|
-
expressions.push(...recurse(node.tag));
|
|
8920
|
-
expressions.push(...recurse(node.quasi));
|
|
8921
|
-
}
|
|
8922
|
-
if (node.type === AST_NODE_TYPES.ImportExpression) expressions.push(...recurse(node.source));
|
|
9056
|
+
simpleTraverse(node, {
|
|
9057
|
+
enter(node, parent) {
|
|
9058
|
+
if (parent?.type === AST_NODE_TYPES.MemberExpression && !parent.computed && parent.property === node) return;
|
|
9059
|
+
if (isNodeOfType(node)) expressions.push(node);
|
|
9060
|
+
},
|
|
9061
|
+
visitorKeys: EXPRESSION_VISITOR_KEYS
|
|
9062
|
+
});
|
|
8923
9063
|
return expressions;
|
|
8924
9064
|
};
|
|
8925
9065
|
return recurse;
|
|
@@ -8968,9 +9108,9 @@ var use_state_default = createRule({
|
|
|
8968
9108
|
type: "suggestion",
|
|
8969
9109
|
docs: { description: "Enforces correct usage of 'useState', including destructuring, symmetric naming of the value and setter, and wrapping expensive initializers in a lazy initializer function." },
|
|
8970
9110
|
messages: {
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
9111
|
+
"invalid-assignment": "useState should be destructured into a value and setter pair, e.g., const [state, setState] = useState(...).",
|
|
9112
|
+
"invalid-initialization": "To prevent re-computation, consider using lazy initial state for useState calls that involve function calls. Ex: 'useState(() => getValue())'.",
|
|
9113
|
+
"invalid-setter-name": "The setter should be named 'set' followed by the capitalized state variable name, e.g., 'setState' for 'state'."
|
|
8974
9114
|
},
|
|
8975
9115
|
schema
|
|
8976
9116
|
},
|
|
@@ -8991,7 +9131,7 @@ function create(context) {
|
|
|
8991
9131
|
if (LAZY_INIT_ALLOW_LIST.includes(expr.callee.name)) continue;
|
|
8992
9132
|
if (Traverse.findParent(expr, (n) => core.isUseCall(context, n)) != null) continue;
|
|
8993
9133
|
context.report({
|
|
8994
|
-
messageId: "
|
|
9134
|
+
messageId: "invalid-initialization",
|
|
8995
9135
|
node: expr
|
|
8996
9136
|
});
|
|
8997
9137
|
}
|
|
@@ -9001,7 +9141,7 @@ function create(context) {
|
|
|
9001
9141
|
if (LAZY_INIT_ALLOW_LIST.includes(expr.callee.name)) continue;
|
|
9002
9142
|
if (Traverse.findParent(expr, (n) => core.isUseCall(context, n)) != null) continue;
|
|
9003
9143
|
context.report({
|
|
9004
|
-
messageId: "
|
|
9144
|
+
messageId: "invalid-initialization",
|
|
9005
9145
|
node: expr
|
|
9006
9146
|
});
|
|
9007
9147
|
}
|
|
@@ -9014,7 +9154,7 @@ function create(context) {
|
|
|
9014
9154
|
if (parent.type !== AST_NODE_TYPES.VariableDeclarator) {
|
|
9015
9155
|
if (!enforceAssignment) return;
|
|
9016
9156
|
context.report({
|
|
9017
|
-
messageId: "
|
|
9157
|
+
messageId: "invalid-assignment",
|
|
9018
9158
|
node
|
|
9019
9159
|
});
|
|
9020
9160
|
return;
|
|
@@ -9023,7 +9163,7 @@ function create(context) {
|
|
|
9023
9163
|
if (id?.type !== AST_NODE_TYPES.ArrayPattern) {
|
|
9024
9164
|
if (!enforceAssignment) return;
|
|
9025
9165
|
context.report({
|
|
9026
|
-
messageId: "
|
|
9166
|
+
messageId: "invalid-assignment",
|
|
9027
9167
|
node: id ?? node
|
|
9028
9168
|
});
|
|
9029
9169
|
return;
|
|
@@ -9032,7 +9172,7 @@ function create(context) {
|
|
|
9032
9172
|
if (value == null) {
|
|
9033
9173
|
if (!enforceAssignment) return;
|
|
9034
9174
|
context.report({
|
|
9035
|
-
messageId: "
|
|
9175
|
+
messageId: "invalid-assignment",
|
|
9036
9176
|
node: id
|
|
9037
9177
|
});
|
|
9038
9178
|
return;
|
|
@@ -9040,20 +9180,20 @@ function create(context) {
|
|
|
9040
9180
|
if (setter == null || !enforceSetterName) return;
|
|
9041
9181
|
if (value.type !== AST_NODE_TYPES.Identifier) {
|
|
9042
9182
|
context.report({
|
|
9043
|
-
messageId: "
|
|
9183
|
+
messageId: "invalid-assignment",
|
|
9044
9184
|
node: value
|
|
9045
9185
|
});
|
|
9046
9186
|
return;
|
|
9047
9187
|
}
|
|
9048
9188
|
if (setter.type !== AST_NODE_TYPES.Identifier || !setter.name.startsWith("set")) {
|
|
9049
9189
|
context.report({
|
|
9050
|
-
messageId: "
|
|
9190
|
+
messageId: "invalid-setter-name",
|
|
9051
9191
|
node: setter
|
|
9052
9192
|
});
|
|
9053
9193
|
return;
|
|
9054
9194
|
}
|
|
9055
9195
|
if (snakeCase(setter.name) !== `set_${snakeCase(value.name)}`) context.report({
|
|
9056
|
-
messageId: "
|
|
9196
|
+
messageId: "invalid-setter-name",
|
|
9057
9197
|
node: setter
|
|
9058
9198
|
});
|
|
9059
9199
|
} };
|