eslint-plugin-react-x 5.23.4 → 5.24.0-beta.0
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 +767 -597
- 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.0";
|
|
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
|
}
|
|
@@ -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]: ["arguments", "callee"],
|
|
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]: ["arguments", "callee"],
|
|
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,19 +7926,195 @@ 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;
|
|
7982
8094
|
seen.add(name);
|
|
7983
|
-
for (const
|
|
8095
|
+
for (const def of findVariable(initialScope, name)?.defs ?? []) {
|
|
8096
|
+
if (def.type === DefinitionType.Parameter) {
|
|
8097
|
+
if (def.name.type === AST_NODE_TYPES.Identifier && (def.name.name === "ref" || def.name.name.endsWith("Ref"))) return true;
|
|
8098
|
+
continue;
|
|
8099
|
+
}
|
|
8100
|
+
const { node } = def;
|
|
7984
8101
|
if (node.type !== AST_NODE_TYPES.VariableDeclarator) continue;
|
|
7985
8102
|
const init = node.init == null ? null : Extract.unwrap(node.init);
|
|
7986
8103
|
if (init == null) continue;
|
|
7987
8104
|
switch (true) {
|
|
7988
8105
|
case init.type === AST_NODE_TYPES.MemberExpression: {
|
|
7989
|
-
let
|
|
7990
|
-
while (
|
|
7991
|
-
|
|
8106
|
+
let current = init;
|
|
8107
|
+
while (current.type === AST_NODE_TYPES.MemberExpression) {
|
|
8108
|
+
const property = Extract.unwrap(current.property);
|
|
8109
|
+
const object = Extract.unwrap(current.object);
|
|
8110
|
+
if (!current.computed && Check.isIdentifier(property, "current")) {
|
|
8111
|
+
const sourceName = getRefSourceName(object);
|
|
8112
|
+
if (sourceName === "ref" || sourceName?.endsWith("Ref") === true) return true;
|
|
8113
|
+
}
|
|
8114
|
+
current = object;
|
|
8115
|
+
}
|
|
8116
|
+
if (Check.isIdentifier(current) && (current.name === "ref" || current.name.endsWith("Ref"))) return true;
|
|
8117
|
+
return getNestedIdentifiers(init).some((id) => isInitializedFromRef(context, id.name, context.sourceCode.getScope(id), seen));
|
|
7992
8118
|
}
|
|
7993
8119
|
case init.type === AST_NODE_TYPES.CallExpression && core.isUseRefLikeCall(init, additionalRefHooks): return true;
|
|
7994
8120
|
default: return getNestedIdentifiers(init).some((id) => isInitializedFromRef(context, id.name, context.sourceCode.getScope(id), seen));
|
|
@@ -7997,6 +8123,31 @@ function isInitializedFromRef(context, name, initialScope, seen = /* @__PURE__ *
|
|
|
7997
8123
|
return false;
|
|
7998
8124
|
}
|
|
7999
8125
|
/**
|
|
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
|
|
8130
|
+
*/
|
|
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.AssignmentExpression: return isUsingRefValue(n.right);
|
|
8144
|
+
default: return false;
|
|
8145
|
+
}
|
|
8146
|
+
};
|
|
8147
|
+
if (isUsingRefValue(node)) return true;
|
|
8148
|
+
return Check.isFunction(node) && context.sourceCode.getScope(node.body).references.some((r) => isUsingRefValue(r.identifier));
|
|
8149
|
+
}
|
|
8150
|
+
/**
|
|
8000
8151
|
* Check if a setState call is inside a conditional block whose test expression
|
|
8001
8152
|
* is derived from a ref value (e.g. `if (prevRef.current !== value) setState(...)`).
|
|
8002
8153
|
* @param context The ESLint rule context
|
|
@@ -8027,23 +8178,133 @@ function isRefGatedContext(context, node) {
|
|
|
8027
8178
|
}
|
|
8028
8179
|
return false;
|
|
8029
8180
|
}
|
|
8030
|
-
function isTerminatingStatement(node) {
|
|
8031
|
-
if (node.type === AST_NODE_TYPES.ReturnStatement || node.type === AST_NODE_TYPES.ThrowStatement) return true;
|
|
8032
|
-
if (node.type === AST_NODE_TYPES.BlockStatement) return node.body.some(isTerminatingStatement);
|
|
8033
|
-
return false;
|
|
8034
|
-
}
|
|
8035
8181
|
function isRefInExpression(context, node) {
|
|
8036
8182
|
return getNestedIdentifiers(node).some((id) => isInitializedFromRef(context, id.name, context.sourceCode.getScope(id)));
|
|
8037
8183
|
}
|
|
8038
8184
|
/**
|
|
8039
|
-
* Get the
|
|
8040
|
-
*
|
|
8041
|
-
* returns the
|
|
8042
|
-
* @param node The setState call node (CallExpression or Identifier)
|
|
8043
|
-
* @returns The actual CallExpression node
|
|
8185
|
+
* Get the name that identifies the source of a `.current` member access.
|
|
8186
|
+
* @param node The object of the member expression
|
|
8187
|
+
* @returns The identifier name, the last property name of a member chain, or null
|
|
8044
8188
|
*/
|
|
8045
|
-
function
|
|
8046
|
-
|
|
8189
|
+
function getRefSourceName(node) {
|
|
8190
|
+
switch (node.type) {
|
|
8191
|
+
case AST_NODE_TYPES.Identifier: return node.name;
|
|
8192
|
+
case AST_NODE_TYPES.MemberExpression: {
|
|
8193
|
+
const property = Extract.unwrap(node.property);
|
|
8194
|
+
return !node.computed && property.type === AST_NODE_TYPES.Identifier ? property.name : null;
|
|
8195
|
+
}
|
|
8196
|
+
default: return null;
|
|
8197
|
+
}
|
|
8198
|
+
}
|
|
8199
|
+
|
|
8200
|
+
//#endregion
|
|
8201
|
+
//#region src/rules/set-state-in-effect/effects.ts
|
|
8202
|
+
/**
|
|
8203
|
+
* Infer the setState calls that run synchronously during an effect setup from
|
|
8204
|
+
* the collected facts: calls made directly in the setup body (or an IIFE within
|
|
8205
|
+
* it), and calls reached indirectly through local functions, hook callbacks, or
|
|
8206
|
+
* setup identifiers passed to the effect.
|
|
8207
|
+
* @param context The rule context
|
|
8208
|
+
* @param facts The collected facts
|
|
8209
|
+
* @returns The violations to report, in report order
|
|
8210
|
+
*/
|
|
8211
|
+
function inferViolations$2(context, facts) {
|
|
8212
|
+
const { additionalEffectHooks } = getSettingsFromContext(context);
|
|
8213
|
+
const resolver = createSetStateResolver$1(context);
|
|
8214
|
+
const getText = (n) => context.sourceCode.getText(n);
|
|
8215
|
+
const isUseEffectCall = (node) => core.isUseEffectLikeCall(node, additionalEffectHooks);
|
|
8216
|
+
const setStateCallsByFn = /* @__PURE__ */ new Map();
|
|
8217
|
+
const setStateInHookCallbacks = /* @__PURE__ */ new Map();
|
|
8218
|
+
const setStateInEffectArg = /* @__PURE__ */ new Map();
|
|
8219
|
+
const setStateInEffectSetup = /* @__PURE__ */ new Map();
|
|
8220
|
+
const trackedFnCalls = [];
|
|
8221
|
+
const violations = [];
|
|
8222
|
+
for (const { enclosingFunction, enclosingFunctionKind, node, setupFunction } of facts.calls) {
|
|
8223
|
+
if (resolver.isUseStateCall(node) || isThenCall(node)) continue;
|
|
8224
|
+
if (!resolver.isSetStateCall(node)) {
|
|
8225
|
+
if (setupFunction != null && enclosingFunction === setupFunction) trackedFnCalls.push(node);
|
|
8226
|
+
continue;
|
|
8227
|
+
}
|
|
8228
|
+
if (enclosingFunctionKind === "deferred") continue;
|
|
8229
|
+
if (setupFunction != null && (enclosingFunction === setupFunction || enclosingFunctionKind === "immediate" && Traverse.findParent(enclosingFunction, Check.isFunction) === setupFunction)) {
|
|
8230
|
+
const args0 = node.arguments.at(0);
|
|
8231
|
+
if (args0 == null) continue;
|
|
8232
|
+
if (isArgumentUsingRefValue(context, args0)) continue;
|
|
8233
|
+
if (isRefGatedContext(context, node)) continue;
|
|
8234
|
+
violations.push({
|
|
8235
|
+
name: getText(Extract.unwrap(node.callee)),
|
|
8236
|
+
node
|
|
8237
|
+
});
|
|
8238
|
+
continue;
|
|
8239
|
+
}
|
|
8240
|
+
const init = Traverse.findParent(node, isHookDecl)?.init;
|
|
8241
|
+
if (init == null) getOrInsertComputed(setStateCallsByFn, enclosingFunction, () => []).push(node);
|
|
8242
|
+
else getOrInsertComputed(setStateInHookCallbacks, init, () => []).push(node);
|
|
8243
|
+
}
|
|
8244
|
+
for (const { kind, node } of facts.setStateReferences) {
|
|
8245
|
+
if (!resolver.isSetStateId(node, 1)) continue;
|
|
8246
|
+
if (kind === "callback-body") {
|
|
8247
|
+
const parent = node.parent.parent;
|
|
8248
|
+
if (parent?.type !== AST_NODE_TYPES.CallExpression) continue;
|
|
8249
|
+
if (!core.isUseMemoCall(context, parent)) continue;
|
|
8250
|
+
const init = Traverse.findParent(parent, isHookDecl)?.init;
|
|
8251
|
+
if (init != null) getOrInsertComputed(setStateInEffectArg, init, () => []).push(node);
|
|
8252
|
+
continue;
|
|
8253
|
+
}
|
|
8254
|
+
const parent = node.parent;
|
|
8255
|
+
if (parent.type !== AST_NODE_TYPES.CallExpression) continue;
|
|
8256
|
+
if (core.isUseCallbackCall(context, parent)) {
|
|
8257
|
+
const init = Traverse.findParent(parent, isHookDecl)?.init;
|
|
8258
|
+
if (init != null) getOrInsertComputed(setStateInEffectArg, init, () => []).push(node);
|
|
8259
|
+
continue;
|
|
8260
|
+
}
|
|
8261
|
+
if (isUseEffectCall(parent)) getOrInsertComputed(setStateInEffectSetup, parent, () => []).push(node);
|
|
8262
|
+
}
|
|
8263
|
+
const isExemptSetStateCall = (setStateCall) => {
|
|
8264
|
+
const callNode = getSetStateCallExpression(setStateCall);
|
|
8265
|
+
if (isRefGatedContext(context, callNode)) return true;
|
|
8266
|
+
if (callNode.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
8267
|
+
const args0 = callNode.arguments.at(0);
|
|
8268
|
+
return args0 != null && isArgumentUsingRefValue(context, args0);
|
|
8269
|
+
};
|
|
8270
|
+
const getSetStateCalls = (id) => {
|
|
8271
|
+
const variable = findVariable(context.sourceCode.getScope(id), id);
|
|
8272
|
+
if (variable != null && variable.defs.some((def) => def.type === DefinitionType.Parameter)) return [];
|
|
8273
|
+
const node = resolveOrigin(context, id);
|
|
8274
|
+
switch (node?.type) {
|
|
8275
|
+
case AST_NODE_TYPES.ArrowFunctionExpression:
|
|
8276
|
+
case AST_NODE_TYPES.FunctionDeclaration:
|
|
8277
|
+
case AST_NODE_TYPES.FunctionExpression: return setStateCallsByFn.get(node) ?? [];
|
|
8278
|
+
case AST_NODE_TYPES.CallExpression: return setStateInHookCallbacks.get(node) ?? setStateInEffectArg.get(node) ?? [];
|
|
8279
|
+
}
|
|
8280
|
+
return [];
|
|
8281
|
+
};
|
|
8282
|
+
for (const [, calls] of setStateInEffectSetup) for (const call of calls) {
|
|
8283
|
+
if (isExemptSetStateCall(call)) continue;
|
|
8284
|
+
violations.push({
|
|
8285
|
+
name: call.name,
|
|
8286
|
+
node: call
|
|
8287
|
+
});
|
|
8288
|
+
}
|
|
8289
|
+
for (const { callee } of trackedFnCalls) {
|
|
8290
|
+
const unwrappedCallee = Extract.unwrap(callee);
|
|
8291
|
+
if (!Check.isIdentifier(unwrappedCallee)) continue;
|
|
8292
|
+
for (const setStateCall of getSetStateCalls(unwrappedCallee)) {
|
|
8293
|
+
if (isExemptSetStateCall(setStateCall)) continue;
|
|
8294
|
+
violations.push({
|
|
8295
|
+
name: getCallName(setStateCall, getText),
|
|
8296
|
+
node: setStateCall
|
|
8297
|
+
});
|
|
8298
|
+
}
|
|
8299
|
+
}
|
|
8300
|
+
for (const id of facts.setupIdentifiers) for (const setStateCall of getSetStateCalls(id)) {
|
|
8301
|
+
if (isExemptSetStateCall(setStateCall)) continue;
|
|
8302
|
+
violations.push({
|
|
8303
|
+
name: getCallName(setStateCall, getText),
|
|
8304
|
+
node: setStateCall
|
|
8305
|
+
});
|
|
8306
|
+
}
|
|
8307
|
+
return violations;
|
|
8047
8308
|
}
|
|
8048
8309
|
|
|
8049
8310
|
//#endregion
|
|
@@ -8063,51 +8324,121 @@ var set_state_in_effect_default = createRule({
|
|
|
8063
8324
|
function create$5(context) {
|
|
8064
8325
|
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
|
|
8065
8326
|
const { additionalEffectHooks, additionalStateHooks } = getSettingsFromContext(context);
|
|
8066
|
-
const
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8327
|
+
const facts = createFactCollector$3(additionalStateHooks, additionalEffectHooks);
|
|
8328
|
+
return merge(facts.visitor, { "Program:exit"() {
|
|
8329
|
+
for (const violation of inferViolations$2(context, facts.facts)) context.report({
|
|
8330
|
+
data: { name: violation.name },
|
|
8331
|
+
messageId: "default",
|
|
8332
|
+
node: violation.node
|
|
8333
|
+
});
|
|
8334
|
+
} });
|
|
8335
|
+
}
|
|
8336
|
+
|
|
8337
|
+
//#endregion
|
|
8338
|
+
//#region src/rules/set-state-in-render/helpers.ts
|
|
8339
|
+
function isComponentOrHookLikeFunction(node) {
|
|
8340
|
+
const id = core.getFunctionId(node);
|
|
8341
|
+
if (id == null) return false;
|
|
8342
|
+
if (Check.isIdentifier(id)) return core.isFunctionComponentName(id.name) || core.isHookName(id.name);
|
|
8343
|
+
if (id.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(id.property)) return core.isFunctionComponentName(id.property.name) || core.isHookName(id.property.name);
|
|
8344
|
+
return false;
|
|
8345
|
+
}
|
|
8346
|
+
function isInsideConditional(node, stopAt) {
|
|
8347
|
+
let current = node.parent;
|
|
8348
|
+
while (current != null && current !== stopAt) {
|
|
8349
|
+
switch (current.type) {
|
|
8350
|
+
case AST_NODE_TYPES.IfStatement:
|
|
8351
|
+
case AST_NODE_TYPES.ConditionalExpression:
|
|
8352
|
+
case AST_NODE_TYPES.LogicalExpression:
|
|
8353
|
+
case AST_NODE_TYPES.SwitchStatement:
|
|
8354
|
+
case AST_NODE_TYPES.SwitchCase: return true;
|
|
8355
|
+
}
|
|
8356
|
+
current = current.parent;
|
|
8083
8357
|
}
|
|
8358
|
+
return false;
|
|
8359
|
+
}
|
|
8360
|
+
|
|
8361
|
+
//#endregion
|
|
8362
|
+
//#region src/rules/set-state-in-render/collect.ts
|
|
8363
|
+
function createFactCollector$2(additionalStateHooks) {
|
|
8364
|
+
const facts = {
|
|
8365
|
+
calls: [],
|
|
8366
|
+
earlyReturns: []
|
|
8367
|
+
};
|
|
8368
|
+
const functionEntries = [];
|
|
8369
|
+
const componentFnRef = { current: null };
|
|
8084
8370
|
function isUseStateCall(node) {
|
|
8085
8371
|
return core.isUseStateLikeCall(node, additionalStateHooks);
|
|
8086
8372
|
}
|
|
8087
|
-
function isUseEffectCall(node) {
|
|
8088
|
-
return core.isUseEffectLikeCall(node, additionalEffectHooks);
|
|
8089
|
-
}
|
|
8090
|
-
function isUseEffectSetupCallback(node) {
|
|
8091
|
-
return node.parent?.type === AST_NODE_TYPES.CallExpression && node.parent.callee !== node && isUseEffectCall(node.parent);
|
|
8092
|
-
}
|
|
8093
|
-
function getCallName(node) {
|
|
8094
|
-
if (node.type === AST_NODE_TYPES.CallExpression) return Extract.getFullyQualifiedName(node.callee, getText);
|
|
8095
|
-
return Extract.getFullyQualifiedName(node, getText);
|
|
8096
|
-
}
|
|
8097
|
-
function getCallKind(node) {
|
|
8098
|
-
return match(node).when(isUseStateCall, () => "useState").when(isUseEffectCall, () => "useEffect").when(isSetStateCall, () => "setState").when(isThenCall, () => "then").otherwise(() => "other");
|
|
8099
|
-
}
|
|
8100
8373
|
function getFunctionKind(node) {
|
|
8374
|
+
if (isComponentOrHookLikeFunction(node)) return "component";
|
|
8101
8375
|
const parent = Traverse.findParent(node, not(Check.isTypeExpression)) ?? node.parent;
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8376
|
+
if (parent.type === AST_NODE_TYPES.CallExpression && parent.callee !== node) return "callback";
|
|
8377
|
+
return "other";
|
|
8378
|
+
}
|
|
8379
|
+
return {
|
|
8380
|
+
facts,
|
|
8381
|
+
visitor: {
|
|
8382
|
+
":function"(node) {
|
|
8383
|
+
const kind = getFunctionKind(node);
|
|
8384
|
+
functionEntries.push({
|
|
8385
|
+
kind,
|
|
8386
|
+
node
|
|
8387
|
+
});
|
|
8388
|
+
if (kind === "component") componentFnRef.current = node;
|
|
8389
|
+
},
|
|
8390
|
+
":function:exit"(node) {
|
|
8391
|
+
if (functionEntries.at(-1)?.kind === "component" && componentFnRef.current === node) componentFnRef.current = null;
|
|
8392
|
+
functionEntries.pop();
|
|
8393
|
+
},
|
|
8394
|
+
CallExpression(node) {
|
|
8395
|
+
const entry = functionEntries.at(-1);
|
|
8396
|
+
if (entry == null) return;
|
|
8397
|
+
if (isUseStateCall(node)) return;
|
|
8398
|
+
facts.calls.push({
|
|
8399
|
+
componentFunction: componentFnRef.current,
|
|
8400
|
+
enclosingFunction: entry.node,
|
|
8401
|
+
enclosingFunctionKind: entry.kind,
|
|
8402
|
+
node
|
|
8403
|
+
});
|
|
8404
|
+
},
|
|
8405
|
+
ReturnStatement(node) {
|
|
8406
|
+
const componentFn = componentFnRef.current;
|
|
8407
|
+
if (componentFn == null) return;
|
|
8408
|
+
const entry = functionEntries.at(-1);
|
|
8409
|
+
if (entry == null || entry.node !== componentFn) return;
|
|
8410
|
+
if (componentFn.body.type !== AST_NODE_TYPES.BlockStatement) return;
|
|
8411
|
+
const body = componentFn.body.body;
|
|
8412
|
+
let stmt = node;
|
|
8413
|
+
while (stmt.parent !== componentFn.body) {
|
|
8414
|
+
if (stmt.parent == null) return;
|
|
8415
|
+
stmt = stmt.parent;
|
|
8416
|
+
}
|
|
8417
|
+
const idx = body.indexOf(stmt);
|
|
8418
|
+
if (idx !== -1 && idx < body.length - 1) facts.earlyReturns.push({
|
|
8419
|
+
componentFunction: componentFn,
|
|
8420
|
+
node
|
|
8421
|
+
});
|
|
8422
|
+
}
|
|
8108
8423
|
}
|
|
8424
|
+
};
|
|
8425
|
+
}
|
|
8426
|
+
|
|
8427
|
+
//#endregion
|
|
8428
|
+
//#region src/rules/set-state-in-render/origins.ts
|
|
8429
|
+
/**
|
|
8430
|
+
* Create a resolver that answers whether an identifier or call originates from
|
|
8431
|
+
* a `useState`-like hook's setter function (the element at index 1 of the
|
|
8432
|
+
* returned tuple).
|
|
8433
|
+
* @param context The rule context
|
|
8434
|
+
* @returns The setState provenance resolver
|
|
8435
|
+
*/
|
|
8436
|
+
function createSetStateResolver(context) {
|
|
8437
|
+
const { additionalStateHooks } = getSettingsFromContext(context);
|
|
8438
|
+
function isUseStateCall(node) {
|
|
8439
|
+
return core.isUseStateLikeCall(node, additionalStateHooks);
|
|
8109
8440
|
}
|
|
8110
|
-
function
|
|
8441
|
+
function isSetStateId(id, at) {
|
|
8111
8442
|
const initNode = resolveOrigin(context, id);
|
|
8112
8443
|
if (initNode == null) return false;
|
|
8113
8444
|
if (initNode.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
@@ -8127,187 +8458,53 @@ function create$5(context) {
|
|
|
8127
8458
|
const [index] = callee.arguments;
|
|
8128
8459
|
if (!isAt || index == null) return false;
|
|
8129
8460
|
const indexScope = context.sourceCode.getScope(node);
|
|
8130
|
-
return getStaticValue(index, indexScope)?.value === 1 &&
|
|
8461
|
+
return getStaticValue(index, indexScope)?.value === 1 && isSetStateId(innerCallee.object);
|
|
8131
8462
|
}
|
|
8132
|
-
case AST_NODE_TYPES.Identifier: return
|
|
8463
|
+
case AST_NODE_TYPES.Identifier: return isSetStateId(callee, 1);
|
|
8133
8464
|
case AST_NODE_TYPES.MemberExpression: {
|
|
8134
8465
|
if (!("name" in callee.object)) return false;
|
|
8135
8466
|
const property = callee.property;
|
|
8136
8467
|
const propertyScope = context.sourceCode.getScope(node);
|
|
8137
|
-
return getStaticValue(property, propertyScope)?.value === 1 &&
|
|
8468
|
+
return getStaticValue(property, propertyScope)?.value === 1 && isSetStateId(callee.object, 1);
|
|
8138
8469
|
}
|
|
8139
8470
|
default: return false;
|
|
8140
8471
|
}
|
|
8141
8472
|
}
|
|
8142
8473
|
return {
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
kind,
|
|
8147
|
-
node
|
|
8148
|
-
});
|
|
8149
|
-
if (kind === "setup") onSetupFunctionEnter(node);
|
|
8150
|
-
},
|
|
8151
|
-
":function:exit"(node) {
|
|
8152
|
-
const { kind } = functionEntries.at(-1) ?? {};
|
|
8153
|
-
if (kind === "setup") onSetupFunctionExit(node);
|
|
8154
|
-
functionEntries.pop();
|
|
8155
|
-
},
|
|
8156
|
-
CallExpression(node) {
|
|
8157
|
-
const setupFunction = setupFnRef.current;
|
|
8158
|
-
const entry = functionEntries.at(-1);
|
|
8159
|
-
if (entry == null || entry.node.async) return;
|
|
8160
|
-
match(getCallKind(node)).with("setState", () => {
|
|
8161
|
-
switch (true) {
|
|
8162
|
-
case entry.kind === "deferred":
|
|
8163
|
-
case entry.node.async: break;
|
|
8164
|
-
case entry.node === setupFunction:
|
|
8165
|
-
case entry.kind === "immediate" && Traverse.findParent(entry.node, Check.isFunction) === setupFunction: {
|
|
8166
|
-
const args0 = node.arguments.at(0);
|
|
8167
|
-
if (args0 == null) return;
|
|
8168
|
-
function isArgumentUsingRefValue(context, node) {
|
|
8169
|
-
const isUsingRefValue = (n) => {
|
|
8170
|
-
switch (n.type) {
|
|
8171
|
-
case AST_NODE_TYPES.Identifier: return isInitializedFromRef(context, n.name, context.sourceCode.getScope(n));
|
|
8172
|
-
case AST_NODE_TYPES.MemberExpression: return isUsingRefValue(n.object);
|
|
8173
|
-
case AST_NODE_TYPES.CallExpression: return isUsingRefValue(n.callee) || getNestedIdentifiers(n).some(isUsingRefValue);
|
|
8174
|
-
case AST_NODE_TYPES.BinaryExpression:
|
|
8175
|
-
case AST_NODE_TYPES.LogicalExpression: return isUsingRefValue(n.left) || isUsingRefValue(n.right);
|
|
8176
|
-
case AST_NODE_TYPES.UnaryExpression:
|
|
8177
|
-
case AST_NODE_TYPES.UpdateExpression: return isUsingRefValue(n.argument);
|
|
8178
|
-
case AST_NODE_TYPES.ConditionalExpression: return isUsingRefValue(n.consequent) || isUsingRefValue(n.alternate);
|
|
8179
|
-
case AST_NODE_TYPES.SequenceExpression: return n.expressions.some(isUsingRefValue);
|
|
8180
|
-
case AST_NODE_TYPES.AssignmentExpression: return isUsingRefValue(n.right);
|
|
8181
|
-
default: return false;
|
|
8182
|
-
}
|
|
8183
|
-
};
|
|
8184
|
-
if (isUsingRefValue(node)) return true;
|
|
8185
|
-
return Check.isFunction(node) && context.sourceCode.getScope(node.body).references.some((r) => isUsingRefValue(r.identifier));
|
|
8186
|
-
}
|
|
8187
|
-
if (isArgumentUsingRefValue(context, args0)) return;
|
|
8188
|
-
if (isRefGatedContext(context, node)) return;
|
|
8189
|
-
context.report({
|
|
8190
|
-
data: { name: context.sourceCode.getText(Extract.unwrap(node.callee)) },
|
|
8191
|
-
messageId: "default",
|
|
8192
|
-
node
|
|
8193
|
-
});
|
|
8194
|
-
return;
|
|
8195
|
-
}
|
|
8196
|
-
default: {
|
|
8197
|
-
const init = Traverse.findParent(node, isHookDecl)?.init;
|
|
8198
|
-
if (init == null) getOrInsertComputed(setStateCallsByFn, entry.node, () => []).push(node);
|
|
8199
|
-
else getOrInsertComputed(setStateInHookCallbacks, init, () => []).push(node);
|
|
8200
|
-
}
|
|
8201
|
-
}
|
|
8202
|
-
}).with("useEffect", () => {
|
|
8203
|
-
if (Check.isFunction(node.arguments.at(0))) return;
|
|
8204
|
-
setupFnIds.push(...getNestedIdentifiers(node));
|
|
8205
|
-
}).with("other", () => {
|
|
8206
|
-
if (entry.node !== setupFunction) return;
|
|
8207
|
-
trackedFnCalls.push(node);
|
|
8208
|
-
}).otherwise(constVoid);
|
|
8209
|
-
},
|
|
8210
|
-
Identifier(node) {
|
|
8211
|
-
if (node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee === node) return;
|
|
8212
|
-
if (!isIdFromUseStateCall(node, 1)) return;
|
|
8213
|
-
switch (node.parent.type) {
|
|
8214
|
-
case AST_NODE_TYPES.ArrowFunctionExpression: {
|
|
8215
|
-
const parent = node.parent.parent;
|
|
8216
|
-
if (parent.type !== AST_NODE_TYPES.CallExpression) break;
|
|
8217
|
-
if (!core.isUseMemoCall(context, parent)) break;
|
|
8218
|
-
const init = Traverse.findParent(parent, isHookDecl)?.init;
|
|
8219
|
-
if (init != null) getOrInsertComputed(setStateInEffectArg, init, () => []).push(node);
|
|
8220
|
-
break;
|
|
8221
|
-
}
|
|
8222
|
-
case AST_NODE_TYPES.CallExpression:
|
|
8223
|
-
if (node !== node.parent.arguments.at(0)) break;
|
|
8224
|
-
if (core.isUseCallbackCall(context, node.parent)) {
|
|
8225
|
-
const init = Traverse.findParent(node.parent, isHookDecl)?.init;
|
|
8226
|
-
if (init != null) getOrInsertComputed(setStateInEffectArg, init, () => []).push(node);
|
|
8227
|
-
break;
|
|
8228
|
-
}
|
|
8229
|
-
if (isUseEffectCall(node.parent)) getOrInsertComputed(setStateInEffectSetup, node.parent, () => []).push(node);
|
|
8230
|
-
}
|
|
8231
|
-
},
|
|
8232
|
-
"Program:exit"() {
|
|
8233
|
-
const getSetStateCalls = (context, id) => {
|
|
8234
|
-
const variable = findVariable(context.sourceCode.getScope(id), id);
|
|
8235
|
-
if (variable != null && variable.defs.some((def) => def.type === DefinitionType.Parameter)) return [];
|
|
8236
|
-
const node = resolveOrigin(context, id);
|
|
8237
|
-
switch (node?.type) {
|
|
8238
|
-
case AST_NODE_TYPES.ArrowFunctionExpression:
|
|
8239
|
-
case AST_NODE_TYPES.FunctionDeclaration:
|
|
8240
|
-
case AST_NODE_TYPES.FunctionExpression: return setStateCallsByFn.get(node) ?? [];
|
|
8241
|
-
case AST_NODE_TYPES.CallExpression: return setStateInHookCallbacks.get(node) ?? setStateInEffectArg.get(node) ?? [];
|
|
8242
|
-
}
|
|
8243
|
-
return [];
|
|
8244
|
-
};
|
|
8245
|
-
for (const [, calls] of setStateInEffectSetup) for (const call of calls) {
|
|
8246
|
-
if (isRefGatedContext(context, getSetStateCallExpression(call))) continue;
|
|
8247
|
-
context.report({
|
|
8248
|
-
data: { name: call.name },
|
|
8249
|
-
messageId: "default",
|
|
8250
|
-
node: call
|
|
8251
|
-
});
|
|
8252
|
-
}
|
|
8253
|
-
for (const { callee } of trackedFnCalls) {
|
|
8254
|
-
const unwrappedCallee = Extract.unwrap(callee);
|
|
8255
|
-
if (!Check.isIdentifier(unwrappedCallee)) continue;
|
|
8256
|
-
const setStateCalls = getSetStateCalls(context, unwrappedCallee);
|
|
8257
|
-
for (const setStateCall of setStateCalls) {
|
|
8258
|
-
if (isRefGatedContext(context, getSetStateCallExpression(setStateCall))) continue;
|
|
8259
|
-
context.report({
|
|
8260
|
-
data: { name: getCallName(setStateCall) },
|
|
8261
|
-
messageId: "default",
|
|
8262
|
-
node: setStateCall
|
|
8263
|
-
});
|
|
8264
|
-
}
|
|
8265
|
-
}
|
|
8266
|
-
for (const id of setupFnIds) {
|
|
8267
|
-
const setStateCalls = getSetStateCalls(context, id);
|
|
8268
|
-
for (const setStateCall of setStateCalls) {
|
|
8269
|
-
if (isRefGatedContext(context, getSetStateCallExpression(setStateCall))) continue;
|
|
8270
|
-
context.report({
|
|
8271
|
-
data: { name: getCallName(setStateCall) },
|
|
8272
|
-
messageId: "default",
|
|
8273
|
-
node: setStateCall
|
|
8274
|
-
});
|
|
8275
|
-
}
|
|
8276
|
-
}
|
|
8277
|
-
}
|
|
8474
|
+
isSetStateCall,
|
|
8475
|
+
isSetStateId,
|
|
8476
|
+
isUseStateCall
|
|
8278
8477
|
};
|
|
8279
8478
|
}
|
|
8280
8479
|
|
|
8281
8480
|
//#endregion
|
|
8282
|
-
//#region src/rules/set-state-in-render/
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
if (
|
|
8301
|
-
|
|
8481
|
+
//#region src/rules/set-state-in-render/effects.ts
|
|
8482
|
+
/**
|
|
8483
|
+
* Infer the setState calls that run unconditionally during render from the
|
|
8484
|
+
* collected facts: calls made directly in a component function body that are
|
|
8485
|
+
* neither guarded by a conditional nor positioned after an early return.
|
|
8486
|
+
* @param context The rule context
|
|
8487
|
+
* @param facts The collected facts
|
|
8488
|
+
* @returns The violations to report, in report order
|
|
8489
|
+
*/
|
|
8490
|
+
function inferViolations$1(context, facts) {
|
|
8491
|
+
const resolver = createSetStateResolver(context);
|
|
8492
|
+
const getText = (n) => context.sourceCode.getText(n);
|
|
8493
|
+
const earlyReturnPositions = /* @__PURE__ */ new Map();
|
|
8494
|
+
for (const { componentFunction, node } of facts.earlyReturns) getOrInsertComputed(earlyReturnPositions, componentFunction, () => []).push(node.range[0]);
|
|
8495
|
+
const violations = [];
|
|
8496
|
+
for (const { componentFunction, enclosingFunction, node } of facts.calls) {
|
|
8497
|
+
if (componentFunction == null) continue;
|
|
8498
|
+
if (enclosingFunction !== componentFunction) continue;
|
|
8499
|
+
if (!resolver.isSetStateCall(node)) continue;
|
|
8500
|
+
if (isInsideConditional(node, componentFunction)) continue;
|
|
8501
|
+
if (earlyReturnPositions.get(componentFunction)?.some((pos) => pos < node.range[0]) === true) continue;
|
|
8502
|
+
violations.push({
|
|
8503
|
+
name: getText(Extract.unwrap(node.callee)),
|
|
8504
|
+
node
|
|
8505
|
+
});
|
|
8302
8506
|
}
|
|
8303
|
-
return
|
|
8304
|
-
}
|
|
8305
|
-
function isComponentOrHookLikeFunction(node) {
|
|
8306
|
-
const id = core.getFunctionId(node);
|
|
8307
|
-
if (id == null) return false;
|
|
8308
|
-
if (Check.isIdentifier(id)) return core.isFunctionComponentName(id.name) || core.isHookName(id.name);
|
|
8309
|
-
if (id.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(id.property)) return core.isFunctionComponentName(id.property.name) || core.isHookName(id.property.name);
|
|
8310
|
-
return false;
|
|
8507
|
+
return violations;
|
|
8311
8508
|
}
|
|
8312
8509
|
|
|
8313
8510
|
//#endregion
|
|
@@ -8326,103 +8523,19 @@ var set_state_in_render_default = createRule({
|
|
|
8326
8523
|
});
|
|
8327
8524
|
function create$4(context) {
|
|
8328
8525
|
const { additionalStateHooks } = getSettingsFromContext(context);
|
|
8329
|
-
const
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
if (initNode == null) return false;
|
|
8338
|
-
if (initNode.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
8339
|
-
if (!isUseStateCall(initNode)) return false;
|
|
8340
|
-
const initNodeParent = initNode.parent;
|
|
8341
|
-
if (!("id" in initNodeParent) || initNodeParent.id?.type !== AST_NODE_TYPES.ArrayPattern) return true;
|
|
8342
|
-
return initNodeParent.id.elements.findIndex((e) => e != null && Check.isIdentifier(e, topLevelId.name)) === at;
|
|
8343
|
-
}
|
|
8344
|
-
function isSetStateCall(node) {
|
|
8345
|
-
const callee = Extract.unwrap(node.callee);
|
|
8346
|
-
switch (callee.type) {
|
|
8347
|
-
case AST_NODE_TYPES.CallExpression: {
|
|
8348
|
-
const innerCallee = Extract.unwrap(callee.callee);
|
|
8349
|
-
if (innerCallee.type !== AST_NODE_TYPES.MemberExpression) return false;
|
|
8350
|
-
if (!("name" in innerCallee.object)) return false;
|
|
8351
|
-
const isAt = Extract.getCalleeName(callee) === "at";
|
|
8352
|
-
const [index] = callee.arguments;
|
|
8353
|
-
if (!isAt || index == null) return false;
|
|
8354
|
-
const indexScope = context.sourceCode.getScope(node);
|
|
8355
|
-
return getStaticValue(index, indexScope)?.value === 1 && isIdFromUseStateCall(innerCallee.object);
|
|
8356
|
-
}
|
|
8357
|
-
case AST_NODE_TYPES.Identifier: return isIdFromUseStateCall(callee, 1);
|
|
8358
|
-
case AST_NODE_TYPES.MemberExpression: {
|
|
8359
|
-
if (!("name" in callee.object)) return false;
|
|
8360
|
-
const property = callee.property;
|
|
8361
|
-
const propertyScope = context.sourceCode.getScope(node);
|
|
8362
|
-
return getStaticValue(property, propertyScope)?.value === 1 && isIdFromUseStateCall(callee.object, 1);
|
|
8363
|
-
}
|
|
8364
|
-
default: return false;
|
|
8365
|
-
}
|
|
8366
|
-
}
|
|
8367
|
-
function getFunctionKind(node) {
|
|
8368
|
-
if (isComponentOrHookLikeFunction(node)) return "component";
|
|
8369
|
-
const parent = Traverse.findParent(node, not(Check.isTypeExpression)) ?? node.parent;
|
|
8370
|
-
if (parent.type === AST_NODE_TYPES.CallExpression && parent.callee !== node) return "callback";
|
|
8371
|
-
return "other";
|
|
8372
|
-
}
|
|
8373
|
-
return {
|
|
8374
|
-
":function"(node) {
|
|
8375
|
-
const kind = getFunctionKind(node);
|
|
8376
|
-
functionEntries.push({
|
|
8377
|
-
kind,
|
|
8378
|
-
node
|
|
8379
|
-
});
|
|
8380
|
-
if (kind === "component") {
|
|
8381
|
-
componentFnRef.current = node;
|
|
8382
|
-
componentHasEarlyReturn.current = false;
|
|
8383
|
-
}
|
|
8384
|
-
},
|
|
8385
|
-
":function:exit"(node) {
|
|
8386
|
-
if (functionEntries.at(-1)?.kind === "component" && componentFnRef.current === node) {
|
|
8387
|
-
componentFnRef.current = null;
|
|
8388
|
-
componentHasEarlyReturn.current = false;
|
|
8389
|
-
}
|
|
8390
|
-
functionEntries.pop();
|
|
8391
|
-
},
|
|
8392
|
-
CallExpression(node) {
|
|
8393
|
-
const componentFn = componentFnRef.current;
|
|
8394
|
-
if (componentFn == null) return;
|
|
8395
|
-
if (!isSetStateCall(node)) return;
|
|
8396
|
-
if (isInsideEventHandler(node, componentFn)) return;
|
|
8397
|
-
if (isInsideConditional(node, componentFn)) return;
|
|
8398
|
-
if (componentHasEarlyReturn.current) return;
|
|
8399
|
-
context.report({
|
|
8400
|
-
data: { name: context.sourceCode.getText(Extract.unwrap(node.callee)) },
|
|
8401
|
-
messageId: "default",
|
|
8402
|
-
node
|
|
8403
|
-
});
|
|
8404
|
-
},
|
|
8405
|
-
ReturnStatement(node) {
|
|
8406
|
-
const componentFn = componentFnRef.current;
|
|
8407
|
-
if (componentFn == null) return;
|
|
8408
|
-
const entry = functionEntries.at(-1);
|
|
8409
|
-
if (entry == null || entry.node !== componentFn) return;
|
|
8410
|
-
if (componentFn.body.type !== AST_NODE_TYPES.BlockStatement) return;
|
|
8411
|
-
const body = componentFn.body.body;
|
|
8412
|
-
let stmt = node;
|
|
8413
|
-
while (stmt.parent !== componentFn.body) {
|
|
8414
|
-
if (stmt.parent == null) return;
|
|
8415
|
-
stmt = stmt.parent;
|
|
8416
|
-
}
|
|
8417
|
-
const idx = body.indexOf(stmt);
|
|
8418
|
-
if (idx !== -1 && idx < body.length - 1) componentHasEarlyReturn.current = true;
|
|
8419
|
-
}
|
|
8420
|
-
};
|
|
8526
|
+
const facts = createFactCollector$2(additionalStateHooks);
|
|
8527
|
+
return merge(facts.visitor, { "Program:exit"() {
|
|
8528
|
+
for (const violation of inferViolations$1(context, facts.facts)) context.report({
|
|
8529
|
+
data: { name: violation.name },
|
|
8530
|
+
messageId: "default",
|
|
8531
|
+
node: violation.node
|
|
8532
|
+
});
|
|
8533
|
+
} });
|
|
8421
8534
|
}
|
|
8422
8535
|
|
|
8423
8536
|
//#endregion
|
|
8424
8537
|
//#region src/rules/static-components/collect.ts
|
|
8425
|
-
function createFactCollector() {
|
|
8538
|
+
function createFactCollector$1() {
|
|
8426
8539
|
const facts = { componentUsages: [] };
|
|
8427
8540
|
return {
|
|
8428
8541
|
facts,
|
|
@@ -8570,7 +8683,7 @@ var static_components_default = createRule({
|
|
|
8570
8683
|
docs: { description: "Validates that components are static, not recreated every render." },
|
|
8571
8684
|
messages: {
|
|
8572
8685
|
default: "Cannot create components during render. Components created during render will reset their state each time they are created. Declare components outside of render.",
|
|
8573
|
-
|
|
8686
|
+
"created-here": "The component is created during render here."
|
|
8574
8687
|
},
|
|
8575
8688
|
schema: []
|
|
8576
8689
|
},
|
|
@@ -8582,7 +8695,7 @@ function create$3(context) {
|
|
|
8582
8695
|
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;
|
|
8583
8696
|
const fc = core.getFunctionComponentCollector(context, { hint });
|
|
8584
8697
|
const cc = core.getClassComponentCollector(context);
|
|
8585
|
-
const facts = createFactCollector();
|
|
8698
|
+
const facts = createFactCollector$1();
|
|
8586
8699
|
return merge(fc.visitor, cc.visitor, facts.visitor, { "Program:exit"(program) {
|
|
8587
8700
|
const componentNodes = [...fc.api.getAllComponents(program), ...cc.api.getAllComponents(program)].map((component) => component.node);
|
|
8588
8701
|
const isInsideRender = createRenderBoundaryChecker(componentNodes);
|
|
@@ -8597,7 +8710,7 @@ function create$3(context) {
|
|
|
8597
8710
|
reportedCreations.add(effect.creationNode);
|
|
8598
8711
|
context.report({
|
|
8599
8712
|
data: { name: effect.name },
|
|
8600
|
-
messageId: "
|
|
8713
|
+
messageId: "created-here",
|
|
8601
8714
|
node: effect.creationNode
|
|
8602
8715
|
});
|
|
8603
8716
|
}
|
|
@@ -8681,14 +8794,17 @@ function create$2(context) {
|
|
|
8681
8794
|
}
|
|
8682
8795
|
|
|
8683
8796
|
//#endregion
|
|
8684
|
-
//#region src/rules/use-memo/
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8797
|
+
//#region src/rules/use-memo/helpers.ts
|
|
8798
|
+
/**
|
|
8799
|
+
* Check whether the result of a `useMemo` call is used (assigned, returned,
|
|
8800
|
+
* passed as an argument, etc.) rather than discarded as a bare statement.
|
|
8801
|
+
* @param node The `useMemo` call expression
|
|
8802
|
+
* @returns `true` when the call result flows into a surrounding expression
|
|
8803
|
+
*/
|
|
8804
|
+
function isResultUsed(node) {
|
|
8805
|
+
let parent = node.parent;
|
|
8806
|
+
while (Check.isTypeExpression(parent)) parent = parent.parent;
|
|
8807
|
+
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;
|
|
8692
8808
|
}
|
|
8693
8809
|
/**
|
|
8694
8810
|
* Gets the nested return statements in the node that are within the same function
|
|
@@ -8697,108 +8813,168 @@ function isDeclaredInsideCallback(variable, callback) {
|
|
|
8697
8813
|
*/
|
|
8698
8814
|
function getNestedReturnStatements(node) {
|
|
8699
8815
|
const statements = [];
|
|
8700
|
-
const
|
|
8816
|
+
const boundary = Check.isFunction(node) ? node : Traverse.findParent(node, Check.isFunction);
|
|
8701
8817
|
simpleTraverse(node, { enter(node) {
|
|
8702
8818
|
if (node.type !== AST_NODE_TYPES.ReturnStatement) return;
|
|
8703
|
-
if (Traverse.findParent(node, Check.isFunction, (n) => n ===
|
|
8819
|
+
if (Traverse.findParent(node, Check.isFunction, (n) => n === boundary) != null) return;
|
|
8704
8820
|
statements.push(node);
|
|
8705
8821
|
} });
|
|
8706
8822
|
return statements;
|
|
8707
8823
|
}
|
|
8708
8824
|
|
|
8709
8825
|
//#endregion
|
|
8710
|
-
//#region src/rules/use-memo/
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
messages: {
|
|
8717
|
-
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.",
|
|
8718
|
-
noAsyncOrGeneratorFunctions: "useMemo() callbacks may not be async or generator functions.\n\nuseMemo() callbacks are called once and must synchronously return a value.",
|
|
8719
|
-
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.",
|
|
8720
|
-
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.",
|
|
8721
|
-
resultMustBeUsed: "useMemo() result is unused.\n\nThis useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects."
|
|
8722
|
-
},
|
|
8723
|
-
schema: []
|
|
8724
|
-
},
|
|
8725
|
-
name: RULE_NAME$1,
|
|
8726
|
-
create: create$1,
|
|
8727
|
-
defaultOptions: []
|
|
8728
|
-
});
|
|
8729
|
-
function create$1(context) {
|
|
8730
|
-
if (!context.sourceCode.text.includes("useMemo")) return {};
|
|
8731
|
-
function validateNoOuterVariableReassignment(callback) {
|
|
8732
|
-
const violations = [];
|
|
8733
|
-
if (callback.body == null) return violations;
|
|
8734
|
-
function checkWriteTarget(node, target) {
|
|
8826
|
+
//#region src/rules/use-memo/collect.ts
|
|
8827
|
+
function createFactCollector(isUseMemoCall) {
|
|
8828
|
+
const facts = { calls: [] };
|
|
8829
|
+
function collectWrites(callback) {
|
|
8830
|
+
const writes = [];
|
|
8831
|
+
function pushWrite(node, target) {
|
|
8735
8832
|
if (!Check.isIdentifier(target)) return;
|
|
8736
8833
|
if (Traverse.findParent(node, Check.isFunction, (n) => n === callback) != null) return;
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
violations.push({
|
|
8741
|
-
messageId: "noReassigningOuterVariables",
|
|
8742
|
-
node: target
|
|
8834
|
+
writes.push({
|
|
8835
|
+
node,
|
|
8836
|
+
target
|
|
8743
8837
|
});
|
|
8744
8838
|
}
|
|
8745
8839
|
simpleTraverse(callback.body, { enter(node) {
|
|
8746
8840
|
switch (node.type) {
|
|
8747
8841
|
case AST_NODE_TYPES.AssignmentExpression:
|
|
8748
|
-
for (const target of Extract.getAssignmentTargets(node.left))
|
|
8842
|
+
for (const target of Extract.getAssignmentTargets(node.left)) pushWrite(node, target);
|
|
8749
8843
|
return;
|
|
8750
8844
|
case AST_NODE_TYPES.ForInStatement:
|
|
8751
8845
|
case AST_NODE_TYPES.ForOfStatement:
|
|
8752
8846
|
if (node.left.type === AST_NODE_TYPES.VariableDeclaration) return;
|
|
8753
|
-
for (const target of Extract.getAssignmentTargets(node.left))
|
|
8847
|
+
for (const target of Extract.getAssignmentTargets(node.left)) pushWrite(node, target);
|
|
8754
8848
|
return;
|
|
8755
8849
|
}
|
|
8756
8850
|
} });
|
|
8757
|
-
return
|
|
8851
|
+
return writes;
|
|
8758
8852
|
}
|
|
8759
|
-
return {
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8853
|
+
return {
|
|
8854
|
+
facts,
|
|
8855
|
+
visitor: { CallExpression(node) {
|
|
8856
|
+
if (!isUseMemoCall(node)) return;
|
|
8857
|
+
const callbackArg = node.arguments[0] ?? null;
|
|
8858
|
+
const callback = callbackArg != null ? Extract.unwrap(callbackArg) : null;
|
|
8859
|
+
const callbackFunction = callback != null && Check.isFunction(callback) ? callback : null;
|
|
8860
|
+
facts.calls.push({
|
|
8861
|
+
callback: callbackFunction,
|
|
8862
|
+
callbackArg,
|
|
8863
|
+
isResultUsed: isResultUsed(node),
|
|
8864
|
+
node,
|
|
8865
|
+
returnStatements: callbackFunction != null ? getNestedReturnStatements(callbackFunction) : [],
|
|
8866
|
+
writes: callbackFunction != null ? collectWrites(callbackFunction) : []
|
|
8767
8867
|
});
|
|
8768
|
-
|
|
8868
|
+
} }
|
|
8869
|
+
};
|
|
8870
|
+
}
|
|
8871
|
+
|
|
8872
|
+
//#endregion
|
|
8873
|
+
//#region src/rules/use-memo/origins.ts
|
|
8874
|
+
function isDeclaredInsideCallback(variable, callback) {
|
|
8875
|
+
let scope = variable.scope;
|
|
8876
|
+
while (scope != null) {
|
|
8877
|
+
if (scope.block === callback) return true;
|
|
8878
|
+
scope = scope.upper;
|
|
8879
|
+
}
|
|
8880
|
+
return false;
|
|
8881
|
+
}
|
|
8882
|
+
/**
|
|
8883
|
+
* Resolve whether an identifier written inside a `useMemo` callback refers to a
|
|
8884
|
+
* variable declared outside the callback. Unresolved identifiers and variables
|
|
8885
|
+
* without definitions are treated as outer variables.
|
|
8886
|
+
* @param context The rule context
|
|
8887
|
+
* @param target The written identifier
|
|
8888
|
+
* @param callback The `useMemo` callback the write occurs in
|
|
8889
|
+
* @returns `true` when the write reassigns an outer variable
|
|
8890
|
+
*/
|
|
8891
|
+
function isOuterVariable(context, target, callback) {
|
|
8892
|
+
const scope = context.sourceCode.getScope(target);
|
|
8893
|
+
const variable = findVariable(scope, target);
|
|
8894
|
+
if (variable == null || variable.defs.length === 0) return true;
|
|
8895
|
+
return !isDeclaredInsideCallback(variable, callback);
|
|
8896
|
+
}
|
|
8897
|
+
|
|
8898
|
+
//#endregion
|
|
8899
|
+
//#region src/rules/use-memo/effects.ts
|
|
8900
|
+
/**
|
|
8901
|
+
* Infer the `useMemo` usage violations from the collected facts: unused
|
|
8902
|
+
* results, callbacks with parameters, async or generator callbacks, outer
|
|
8903
|
+
* variable reassignments, and callbacks that never return a value.
|
|
8904
|
+
* @param context The rule context
|
|
8905
|
+
* @param facts The collected facts
|
|
8906
|
+
* @returns The violations to report, in report order
|
|
8907
|
+
*/
|
|
8908
|
+
function inferViolations(context, facts) {
|
|
8909
|
+
const violations = [];
|
|
8910
|
+
for (const fact of facts.calls) {
|
|
8911
|
+
if (!fact.isResultUsed) {
|
|
8912
|
+
violations.push({
|
|
8913
|
+
kind: "result-must-be-used",
|
|
8914
|
+
node: fact.node
|
|
8915
|
+
});
|
|
8916
|
+
continue;
|
|
8769
8917
|
}
|
|
8770
|
-
const
|
|
8771
|
-
if (callbackArg == null)
|
|
8772
|
-
const callback = Extract.unwrap(callbackArg);
|
|
8773
|
-
if (!Check.isFunction(callback)) return;
|
|
8918
|
+
const { callback, callbackArg } = fact;
|
|
8919
|
+
if (callbackArg == null || callback == null) continue;
|
|
8774
8920
|
if (callback.params.length > 0) {
|
|
8775
8921
|
const firstParam = callback.params[0];
|
|
8776
|
-
if (firstParam == null)
|
|
8777
|
-
|
|
8778
|
-
|
|
8922
|
+
if (firstParam == null) continue;
|
|
8923
|
+
violations.push({
|
|
8924
|
+
kind: "no-parameters",
|
|
8779
8925
|
node: Check.isIdentifier(firstParam) ? firstParam : callback
|
|
8780
8926
|
});
|
|
8781
8927
|
}
|
|
8782
|
-
if (callback.async || callback.generator)
|
|
8783
|
-
|
|
8928
|
+
if (callback.async || callback.generator) violations.push({
|
|
8929
|
+
kind: "no-async-or-generator-functions",
|
|
8784
8930
|
node: callback
|
|
8785
8931
|
});
|
|
8786
|
-
for (const
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
context.report({
|
|
8792
|
-
messageId: "mustReturnAValue",
|
|
8793
|
-
node: callbackArg
|
|
8932
|
+
for (const write of fact.writes) {
|
|
8933
|
+
if (!isOuterVariable(context, write.target, callback)) continue;
|
|
8934
|
+
violations.push({
|
|
8935
|
+
kind: "no-reassigning-outer-variables",
|
|
8936
|
+
node: write.target
|
|
8794
8937
|
});
|
|
8795
|
-
return;
|
|
8796
8938
|
}
|
|
8797
|
-
if (
|
|
8798
|
-
|
|
8939
|
+
if (callback.type === AST_NODE_TYPES.ArrowFunctionExpression && callback.body.type !== AST_NODE_TYPES.BlockStatement) continue;
|
|
8940
|
+
if (callback.body.type !== AST_NODE_TYPES.BlockStatement) continue;
|
|
8941
|
+
if (!fact.returnStatements.some((stmt) => stmt.argument != null)) violations.push({
|
|
8942
|
+
kind: "must-return-a-value",
|
|
8799
8943
|
node: callbackArg
|
|
8800
8944
|
});
|
|
8801
|
-
}
|
|
8945
|
+
}
|
|
8946
|
+
return violations;
|
|
8947
|
+
}
|
|
8948
|
+
|
|
8949
|
+
//#endregion
|
|
8950
|
+
//#region src/rules/use-memo/use-memo.ts
|
|
8951
|
+
const RULE_NAME$1 = "use-memo";
|
|
8952
|
+
var use_memo_default = createRule({
|
|
8953
|
+
meta: {
|
|
8954
|
+
type: "problem",
|
|
8955
|
+
docs: { description: "Validates that 'useMemo' is called with a callback that returns a value." },
|
|
8956
|
+
messages: {
|
|
8957
|
+
"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.",
|
|
8958
|
+
"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.",
|
|
8959
|
+
"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.",
|
|
8960
|
+
"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.",
|
|
8961
|
+
"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."
|
|
8962
|
+
},
|
|
8963
|
+
schema: []
|
|
8964
|
+
},
|
|
8965
|
+
name: RULE_NAME$1,
|
|
8966
|
+
create: create$1,
|
|
8967
|
+
defaultOptions: []
|
|
8968
|
+
});
|
|
8969
|
+
function create$1(context) {
|
|
8970
|
+
if (!context.sourceCode.text.includes("useMemo")) return {};
|
|
8971
|
+
const facts = createFactCollector(core.isUseMemoCall(context));
|
|
8972
|
+
return merge(facts.visitor, { "Program:exit"() {
|
|
8973
|
+
for (const violation of inferViolations(context, facts.facts)) context.report({
|
|
8974
|
+
messageId: violation.kind,
|
|
8975
|
+
node: violation.node
|
|
8976
|
+
});
|
|
8977
|
+
} });
|
|
8802
8978
|
}
|
|
8803
8979
|
|
|
8804
8980
|
//#endregion
|
|
@@ -8808,6 +8984,59 @@ const LAZY_INIT_ALLOW_LIST = [
|
|
|
8808
8984
|
"String",
|
|
8809
8985
|
"Number"
|
|
8810
8986
|
];
|
|
8987
|
+
const EXPRESSION_VISITOR_KEYS = {
|
|
8988
|
+
[AST_NODE_TYPES.ArrayExpression]: ["elements"],
|
|
8989
|
+
[AST_NODE_TYPES.ArrayPattern]: ["elements"],
|
|
8990
|
+
[AST_NODE_TYPES.AssignmentExpression]: ["left", "right"],
|
|
8991
|
+
[AST_NODE_TYPES.AssignmentPattern]: ["left", "right"],
|
|
8992
|
+
[AST_NODE_TYPES.AwaitExpression]: ["argument"],
|
|
8993
|
+
[AST_NODE_TYPES.BinaryExpression]: ["left", "right"],
|
|
8994
|
+
[AST_NODE_TYPES.CallExpression]: ["arguments", "callee"],
|
|
8995
|
+
[AST_NODE_TYPES.ChainExpression]: ["expression"],
|
|
8996
|
+
[AST_NODE_TYPES.ConditionalExpression]: [
|
|
8997
|
+
"test",
|
|
8998
|
+
"consequent",
|
|
8999
|
+
"alternate"
|
|
9000
|
+
],
|
|
9001
|
+
[AST_NODE_TYPES.Decorator]: ["expression"],
|
|
9002
|
+
[AST_NODE_TYPES.DoWhileStatement]: ["test"],
|
|
9003
|
+
[AST_NODE_TYPES.ExpressionStatement]: ["expression"],
|
|
9004
|
+
[AST_NODE_TYPES.ForInStatement]: ["left", "right"],
|
|
9005
|
+
[AST_NODE_TYPES.ForOfStatement]: ["left", "right"],
|
|
9006
|
+
[AST_NODE_TYPES.ForStatement]: ["test"],
|
|
9007
|
+
[AST_NODE_TYPES.IfStatement]: [
|
|
9008
|
+
"test",
|
|
9009
|
+
"consequent",
|
|
9010
|
+
"alternate"
|
|
9011
|
+
],
|
|
9012
|
+
[AST_NODE_TYPES.ImportExpression]: ["source"],
|
|
9013
|
+
[AST_NODE_TYPES.JSXExpressionContainer]: ["expression"],
|
|
9014
|
+
[AST_NODE_TYPES.JSXSpreadChild]: ["expression"],
|
|
9015
|
+
[AST_NODE_TYPES.LogicalExpression]: ["left", "right"],
|
|
9016
|
+
[AST_NODE_TYPES.MemberExpression]: ["object", "property"],
|
|
9017
|
+
[AST_NODE_TYPES.NewExpression]: ["arguments", "callee"],
|
|
9018
|
+
[AST_NODE_TYPES.ObjectExpression]: ["properties"],
|
|
9019
|
+
[AST_NODE_TYPES.ObjectPattern]: ["properties"],
|
|
9020
|
+
[AST_NODE_TYPES.Property]: ["value"],
|
|
9021
|
+
[AST_NODE_TYPES.SequenceExpression]: ["expressions"],
|
|
9022
|
+
[AST_NODE_TYPES.SpreadElement]: ["argument"],
|
|
9023
|
+
[AST_NODE_TYPES.SwitchCase]: ["test", "consequent"],
|
|
9024
|
+
[AST_NODE_TYPES.TaggedTemplateExpression]: ["tag", "quasi"],
|
|
9025
|
+
[AST_NODE_TYPES.TemplateLiteral]: ["expressions"],
|
|
9026
|
+
[AST_NODE_TYPES.TSAsExpression]: ["expression"],
|
|
9027
|
+
[AST_NODE_TYPES.TSClassImplements]: ["expression"],
|
|
9028
|
+
[AST_NODE_TYPES.TSExportAssignment]: ["expression"],
|
|
9029
|
+
[AST_NODE_TYPES.TSExternalModuleReference]: ["expression"],
|
|
9030
|
+
[AST_NODE_TYPES.TSInstantiationExpression]: ["expression"],
|
|
9031
|
+
[AST_NODE_TYPES.TSInterfaceHeritage]: ["expression"],
|
|
9032
|
+
[AST_NODE_TYPES.TSNonNullExpression]: ["expression"],
|
|
9033
|
+
[AST_NODE_TYPES.TSSatisfiesExpression]: ["expression"],
|
|
9034
|
+
[AST_NODE_TYPES.TSTypeAssertion]: ["expression"],
|
|
9035
|
+
[AST_NODE_TYPES.UnaryExpression]: ["argument"],
|
|
9036
|
+
[AST_NODE_TYPES.UpdateExpression]: ["argument"],
|
|
9037
|
+
[AST_NODE_TYPES.WhileStatement]: ["test"],
|
|
9038
|
+
[AST_NODE_TYPES.YieldExpression]: ["argument"]
|
|
9039
|
+
};
|
|
8811
9040
|
/**
|
|
8812
9041
|
* Get all nested expressions of type T in an expression like node
|
|
8813
9042
|
* @param type The type of the expression to retrieve within the node
|
|
@@ -8818,72 +9047,13 @@ function getNestedExpressionsOfType(type) {
|
|
|
8818
9047
|
const isNodeOfType = Check.is(type);
|
|
8819
9048
|
const recurse = (node) => {
|
|
8820
9049
|
const expressions = [];
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
expressions.push(...chunk);
|
|
8829
|
-
}
|
|
8830
|
-
if ("left" in node) {
|
|
8831
|
-
const chunk = recurse(node.left);
|
|
8832
|
-
expressions.push(...chunk);
|
|
8833
|
-
}
|
|
8834
|
-
if ("right" in node) {
|
|
8835
|
-
const chunk = recurse(node.right);
|
|
8836
|
-
expressions.push(...chunk);
|
|
8837
|
-
}
|
|
8838
|
-
if ("test" in node && node.test != null) {
|
|
8839
|
-
const chunk = recurse(node.test);
|
|
8840
|
-
expressions.push(...chunk);
|
|
8841
|
-
}
|
|
8842
|
-
if ("consequent" in node) {
|
|
8843
|
-
const chunk = Array.isArray(node.consequent) ? node.consequent.flatMap(recurse) : recurse(node.consequent);
|
|
8844
|
-
expressions.push(...chunk);
|
|
8845
|
-
}
|
|
8846
|
-
if ("alternate" in node && node.alternate != null) {
|
|
8847
|
-
const chunk = Array.isArray(node.alternate) ? node.alternate.flatMap(recurse) : recurse(node.alternate);
|
|
8848
|
-
expressions.push(...chunk);
|
|
8849
|
-
}
|
|
8850
|
-
if ("elements" in node) {
|
|
8851
|
-
const chunk = node.elements.filter((x) => x != null).flatMap(recurse);
|
|
8852
|
-
expressions.push(...chunk);
|
|
8853
|
-
}
|
|
8854
|
-
if ("properties" in node) {
|
|
8855
|
-
const chunk = node.properties.flatMap(recurse);
|
|
8856
|
-
expressions.push(...chunk);
|
|
8857
|
-
}
|
|
8858
|
-
if ("expressions" in node) {
|
|
8859
|
-
const chunk = node.expressions.flatMap(recurse);
|
|
8860
|
-
expressions.push(...chunk);
|
|
8861
|
-
}
|
|
8862
|
-
if (node.type === AST_NODE_TYPES.Property) {
|
|
8863
|
-
const chunk = recurse(node.value);
|
|
8864
|
-
expressions.push(...chunk);
|
|
8865
|
-
}
|
|
8866
|
-
if (node.type === AST_NODE_TYPES.SpreadElement) {
|
|
8867
|
-
const chunk = recurse(node.argument);
|
|
8868
|
-
expressions.push(...chunk);
|
|
8869
|
-
}
|
|
8870
|
-
if (node.type === AST_NODE_TYPES.MemberExpression) {
|
|
8871
|
-
expressions.push(...recurse(node.object));
|
|
8872
|
-
if (node.computed) expressions.push(...recurse(node.property));
|
|
8873
|
-
}
|
|
8874
|
-
if (node.type === AST_NODE_TYPES.UnaryExpression) {
|
|
8875
|
-
const chunk = recurse(node.argument);
|
|
8876
|
-
expressions.push(...chunk);
|
|
8877
|
-
}
|
|
8878
|
-
if (node.type === AST_NODE_TYPES.AwaitExpression) expressions.push(...recurse(node.argument));
|
|
8879
|
-
if (node.type === AST_NODE_TYPES.YieldExpression && node.argument != null) expressions.push(...recurse(node.argument));
|
|
8880
|
-
if (node.type === AST_NODE_TYPES.UpdateExpression) expressions.push(...recurse(node.argument));
|
|
8881
|
-
if (node.type === AST_NODE_TYPES.CallExpression || node.type === AST_NODE_TYPES.NewExpression) expressions.push(...recurse(node.callee));
|
|
8882
|
-
if (node.type === AST_NODE_TYPES.TaggedTemplateExpression) {
|
|
8883
|
-
expressions.push(...recurse(node.tag));
|
|
8884
|
-
expressions.push(...recurse(node.quasi));
|
|
8885
|
-
}
|
|
8886
|
-
if (node.type === AST_NODE_TYPES.ImportExpression) expressions.push(...recurse(node.source));
|
|
9050
|
+
simpleTraverse(node, {
|
|
9051
|
+
enter(node, parent) {
|
|
9052
|
+
if (parent?.type === AST_NODE_TYPES.MemberExpression && !parent.computed && parent.property === node) return;
|
|
9053
|
+
if (isNodeOfType(node)) expressions.push(node);
|
|
9054
|
+
},
|
|
9055
|
+
visitorKeys: EXPRESSION_VISITOR_KEYS
|
|
9056
|
+
});
|
|
8887
9057
|
return expressions;
|
|
8888
9058
|
};
|
|
8889
9059
|
return recurse;
|
|
@@ -8932,9 +9102,9 @@ var use_state_default = createRule({
|
|
|
8932
9102
|
type: "suggestion",
|
|
8933
9103
|
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." },
|
|
8934
9104
|
messages: {
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
9105
|
+
"invalid-assignment": "useState should be destructured into a value and setter pair, e.g., const [state, setState] = useState(...).",
|
|
9106
|
+
"invalid-initialization": "To prevent re-computation, consider using lazy initial state for useState calls that involve function calls. Ex: 'useState(() => getValue())'.",
|
|
9107
|
+
"invalid-setter-name": "The setter should be named 'set' followed by the capitalized state variable name, e.g., 'setState' for 'state'."
|
|
8938
9108
|
},
|
|
8939
9109
|
schema
|
|
8940
9110
|
},
|
|
@@ -8955,7 +9125,7 @@ function create(context) {
|
|
|
8955
9125
|
if (LAZY_INIT_ALLOW_LIST.includes(expr.callee.name)) continue;
|
|
8956
9126
|
if (Traverse.findParent(expr, (n) => core.isUseCall(context, n)) != null) continue;
|
|
8957
9127
|
context.report({
|
|
8958
|
-
messageId: "
|
|
9128
|
+
messageId: "invalid-initialization",
|
|
8959
9129
|
node: expr
|
|
8960
9130
|
});
|
|
8961
9131
|
}
|
|
@@ -8965,7 +9135,7 @@ function create(context) {
|
|
|
8965
9135
|
if (LAZY_INIT_ALLOW_LIST.includes(expr.callee.name)) continue;
|
|
8966
9136
|
if (Traverse.findParent(expr, (n) => core.isUseCall(context, n)) != null) continue;
|
|
8967
9137
|
context.report({
|
|
8968
|
-
messageId: "
|
|
9138
|
+
messageId: "invalid-initialization",
|
|
8969
9139
|
node: expr
|
|
8970
9140
|
});
|
|
8971
9141
|
}
|
|
@@ -8978,7 +9148,7 @@ function create(context) {
|
|
|
8978
9148
|
if (parent.type !== AST_NODE_TYPES.VariableDeclarator) {
|
|
8979
9149
|
if (!enforceAssignment) return;
|
|
8980
9150
|
context.report({
|
|
8981
|
-
messageId: "
|
|
9151
|
+
messageId: "invalid-assignment",
|
|
8982
9152
|
node
|
|
8983
9153
|
});
|
|
8984
9154
|
return;
|
|
@@ -8987,7 +9157,7 @@ function create(context) {
|
|
|
8987
9157
|
if (id?.type !== AST_NODE_TYPES.ArrayPattern) {
|
|
8988
9158
|
if (!enforceAssignment) return;
|
|
8989
9159
|
context.report({
|
|
8990
|
-
messageId: "
|
|
9160
|
+
messageId: "invalid-assignment",
|
|
8991
9161
|
node: id ?? node
|
|
8992
9162
|
});
|
|
8993
9163
|
return;
|
|
@@ -8996,7 +9166,7 @@ function create(context) {
|
|
|
8996
9166
|
if (value == null) {
|
|
8997
9167
|
if (!enforceAssignment) return;
|
|
8998
9168
|
context.report({
|
|
8999
|
-
messageId: "
|
|
9169
|
+
messageId: "invalid-assignment",
|
|
9000
9170
|
node: id
|
|
9001
9171
|
});
|
|
9002
9172
|
return;
|
|
@@ -9004,20 +9174,20 @@ function create(context) {
|
|
|
9004
9174
|
if (setter == null || !enforceSetterName) return;
|
|
9005
9175
|
if (value.type !== AST_NODE_TYPES.Identifier) {
|
|
9006
9176
|
context.report({
|
|
9007
|
-
messageId: "
|
|
9177
|
+
messageId: "invalid-assignment",
|
|
9008
9178
|
node: value
|
|
9009
9179
|
});
|
|
9010
9180
|
return;
|
|
9011
9181
|
}
|
|
9012
9182
|
if (setter.type !== AST_NODE_TYPES.Identifier || !setter.name.startsWith("set")) {
|
|
9013
9183
|
context.report({
|
|
9014
|
-
messageId: "
|
|
9184
|
+
messageId: "invalid-setter-name",
|
|
9015
9185
|
node: setter
|
|
9016
9186
|
});
|
|
9017
9187
|
return;
|
|
9018
9188
|
}
|
|
9019
9189
|
if (snakeCase(setter.name) !== `set_${snakeCase(value.name)}`) context.report({
|
|
9020
|
-
messageId: "
|
|
9190
|
+
messageId: "invalid-setter-name",
|
|
9021
9191
|
node: setter
|
|
9022
9192
|
});
|
|
9023
9193
|
} };
|