eslint-plugin-react-x 5.12.1 → 5.13.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 +90 -40
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -144,7 +144,7 @@ const rules$6 = {
|
|
|
144
144
|
//#endregion
|
|
145
145
|
//#region package.json
|
|
146
146
|
var name$6 = "eslint-plugin-react-x";
|
|
147
|
-
var version = "5.
|
|
147
|
+
var version = "5.13.1";
|
|
148
148
|
|
|
149
149
|
//#endregion
|
|
150
150
|
//#region src/utils/create-rule.ts
|
|
@@ -4239,6 +4239,17 @@ function create$7(context) {
|
|
|
4239
4239
|
|
|
4240
4240
|
//#endregion
|
|
4241
4241
|
//#region src/rules/refs/lib.ts
|
|
4242
|
+
function isFunctionExpressionLike(node) {
|
|
4243
|
+
return node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression;
|
|
4244
|
+
}
|
|
4245
|
+
function resolveAlias(name, aliases) {
|
|
4246
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4247
|
+
while (aliases.has(name) && !seen.has(name)) {
|
|
4248
|
+
seen.add(name);
|
|
4249
|
+
name = aliases.get(name);
|
|
4250
|
+
}
|
|
4251
|
+
return name;
|
|
4252
|
+
}
|
|
4242
4253
|
/**
|
|
4243
4254
|
* Check if the node is the operand of a `ref.current === null` test inside an IfStatement.
|
|
4244
4255
|
* @param node The MemberExpression node for ref.current
|
|
@@ -4266,37 +4277,62 @@ function isInNullCheckTest(node) {
|
|
|
4266
4277
|
if (parent.type === AST_NODE_TYPES.UnaryExpression && parent.operator === "!") return isIfTest(parent);
|
|
4267
4278
|
return false;
|
|
4268
4279
|
}
|
|
4280
|
+
function isBinaryNullCheckOperand(a, b, refName) {
|
|
4281
|
+
a = Check.isTypeExpression(a) ? Extract.unwrap(a) : a;
|
|
4282
|
+
if (a.type !== AST_NODE_TYPES.MemberExpression) return false;
|
|
4283
|
+
if (Extract.getPropertyName(a.property) !== "current") return false;
|
|
4284
|
+
const obj = Extract.unwrap(a.object);
|
|
4285
|
+
return obj.type === AST_NODE_TYPES.Identifier && obj.name === refName && b.type === AST_NODE_TYPES.Literal && b.value == null;
|
|
4286
|
+
}
|
|
4269
4287
|
function isBinaryNullCheck(test, refName) {
|
|
4270
4288
|
if (test.type !== AST_NODE_TYPES.BinaryExpression) return false;
|
|
4271
4289
|
if (!/^(===|==|!==|!=)$/.test(test.operator)) return false;
|
|
4272
4290
|
const { left, right } = test;
|
|
4273
|
-
|
|
4274
|
-
a = Check.isTypeExpression(a) ? Extract.unwrap(a) : a;
|
|
4275
|
-
if (a.type !== AST_NODE_TYPES.MemberExpression) return false;
|
|
4276
|
-
if (Extract.getPropertyName(a.property) !== "current") return false;
|
|
4277
|
-
const obj = Extract.unwrap(a.object);
|
|
4278
|
-
return obj.type === AST_NODE_TYPES.Identifier && obj.name === refName && b.type === AST_NODE_TYPES.Literal && b.value == null;
|
|
4279
|
-
};
|
|
4280
|
-
return checkSides(left, right) || checkSides(right, left);
|
|
4291
|
+
return isBinaryNullCheckOperand(left, right, refName) || isBinaryNullCheckOperand(right, left, refName);
|
|
4281
4292
|
}
|
|
4282
4293
|
/**
|
|
4283
|
-
*
|
|
4284
|
-
*
|
|
4285
|
-
* `!(ref.current === null)`, and their != variants.
|
|
4294
|
+
* Determine which branch of an `if` statement is guaranteed to see `ref.current` as null, given
|
|
4295
|
+
* a test expression. Returns `null` if the test isn't a recognized null check on `refName`.
|
|
4286
4296
|
* @param test The test expression to check.
|
|
4287
4297
|
* @param refName The name of the ref variable.
|
|
4288
4298
|
*/
|
|
4289
|
-
function
|
|
4290
|
-
if (isBinaryNullCheck(test, refName)) return
|
|
4299
|
+
function getRefCurrentNullCheckBranch(test, refName) {
|
|
4300
|
+
if (isBinaryNullCheck(test, refName)) return test.operator === "===" || test.operator === "==" ? "consequent" : "alternate";
|
|
4291
4301
|
if (test.type === AST_NODE_TYPES.UnaryExpression && test.operator === "!") {
|
|
4292
4302
|
const arg = Extract.unwrap(test.argument);
|
|
4293
4303
|
if (arg.type === AST_NODE_TYPES.MemberExpression) {
|
|
4294
4304
|
const obj = Extract.unwrap(arg.object);
|
|
4295
|
-
return obj.type === AST_NODE_TYPES.Identifier && obj.name === refName && Extract.getPropertyName(arg.property) === "current";
|
|
4305
|
+
return obj.type === AST_NODE_TYPES.Identifier && obj.name === refName && Extract.getPropertyName(arg.property) === "current" ? "consequent" : null;
|
|
4306
|
+
}
|
|
4307
|
+
if (arg.type === AST_NODE_TYPES.BinaryExpression) {
|
|
4308
|
+
const inner = getRefCurrentNullCheckBranch(arg, refName);
|
|
4309
|
+
if (inner === "consequent") return "alternate";
|
|
4310
|
+
if (inner === "alternate") return "consequent";
|
|
4296
4311
|
}
|
|
4297
|
-
return isBinaryNullCheck(arg, refName);
|
|
4298
4312
|
}
|
|
4299
|
-
return
|
|
4313
|
+
return null;
|
|
4314
|
+
}
|
|
4315
|
+
/**
|
|
4316
|
+
* Check whether `node` (a `ref.current` MemberExpression) is being written to indirectly through
|
|
4317
|
+
* a nested property write, e.g. `ref.current.inner = value` or `ref.current.inner++`.
|
|
4318
|
+
* @param node The MemberExpression node for ref.current
|
|
4319
|
+
*/
|
|
4320
|
+
function isNestedRefCurrentWrite(node) {
|
|
4321
|
+
let outer = node;
|
|
4322
|
+
let sawNesting = false;
|
|
4323
|
+
while (true) {
|
|
4324
|
+
let parent = outer.parent;
|
|
4325
|
+
while (Check.isTypeExpression(parent)) parent = parent.parent;
|
|
4326
|
+
if (parent.type === AST_NODE_TYPES.MemberExpression && (parent.object === outer || Extract.unwrap(parent.object) === outer)) {
|
|
4327
|
+
outer = parent;
|
|
4328
|
+
sawNesting = true;
|
|
4329
|
+
continue;
|
|
4330
|
+
}
|
|
4331
|
+
if (!sawNesting) return false;
|
|
4332
|
+
if (parent.type === AST_NODE_TYPES.AssignmentExpression) return parent.left === outer || Extract.unwrap(parent.left) === outer;
|
|
4333
|
+
if (parent.type === AST_NODE_TYPES.UpdateExpression) return parent.argument === outer || Extract.unwrap(parent.argument) === outer;
|
|
4334
|
+
return false;
|
|
4335
|
+
}
|
|
4300
4336
|
}
|
|
4301
4337
|
function isInitializedFromRef$1(context, name, initialScope) {
|
|
4302
4338
|
for (const { node } of findVariable(initialScope, name)?.defs ?? []) {
|
|
@@ -4324,9 +4360,9 @@ var refs_default = createRule({
|
|
|
4324
4360
|
docs: { description: "Validates correct usage of refs by checking that 'ref.current' is not read or written during render." },
|
|
4325
4361
|
messages: {
|
|
4326
4362
|
duplicateRefInit: "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.",
|
|
4327
|
-
readDuringRender: "
|
|
4328
|
-
refPassedToFunction: "Passing a ref to a function may
|
|
4329
|
-
writeDuringRender: "
|
|
4363
|
+
readDuringRender: "Cannot access refs during render",
|
|
4364
|
+
refPassedToFunction: "Passing a ref to a function may read its value during render",
|
|
4365
|
+
writeDuringRender: "Cannot update ref during render"
|
|
4330
4366
|
},
|
|
4331
4367
|
schema: []
|
|
4332
4368
|
},
|
|
@@ -4334,17 +4370,6 @@ var refs_default = createRule({
|
|
|
4334
4370
|
create: create$6,
|
|
4335
4371
|
defaultOptions: []
|
|
4336
4372
|
});
|
|
4337
|
-
function resolveAlias(name, aliases) {
|
|
4338
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4339
|
-
while (aliases.has(name) && !seen.has(name)) {
|
|
4340
|
-
seen.add(name);
|
|
4341
|
-
name = aliases.get(name);
|
|
4342
|
-
}
|
|
4343
|
-
return name;
|
|
4344
|
-
}
|
|
4345
|
-
function isFunctionExpressionLike(node) {
|
|
4346
|
-
return node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression;
|
|
4347
|
-
}
|
|
4348
4373
|
/**
|
|
4349
4374
|
* Phase 2 (part): compute the set of functions that are "reached" during render for a given
|
|
4350
4375
|
* component/hook boundary. A function is reached if it is the boundary itself, or if it is
|
|
@@ -4378,10 +4403,10 @@ function computeReachedFunctions(boundary, isCompOrHookFn, functionVarBindings,
|
|
|
4378
4403
|
* i.e. every function boundary between the access and the component/hook `boundary` is itself
|
|
4379
4404
|
* a function that gets invoked during render (see `computeReachedFunctions`).
|
|
4380
4405
|
*/
|
|
4381
|
-
function isReachedDuringRender(node, boundary, reached) {
|
|
4406
|
+
function isReachedDuringRender(node, boundary, reached, alwaysReached) {
|
|
4382
4407
|
let current = node.parent;
|
|
4383
4408
|
while (current != null && current !== boundary) {
|
|
4384
|
-
if (Check.isFunction(current) && !reached.has(current)) return false;
|
|
4409
|
+
if (Check.isFunction(current) && !reached.has(current) && !alwaysReached.has(current)) return false;
|
|
4385
4410
|
current = current.parent;
|
|
4386
4411
|
}
|
|
4387
4412
|
return true;
|
|
@@ -4395,11 +4420,18 @@ function create$6(context) {
|
|
|
4395
4420
|
const refPassedToFunctions = [];
|
|
4396
4421
|
const functionVarBindings = /* @__PURE__ */ new Map();
|
|
4397
4422
|
const directCallSites = [];
|
|
4423
|
+
const stateInitializerFunctions = /* @__PURE__ */ new Set();
|
|
4398
4424
|
return merge(hc.visitor, fc.visitor, {
|
|
4399
4425
|
AssignmentExpression(node) {
|
|
4400
4426
|
if (node.operator !== "=") return;
|
|
4401
4427
|
const left = Extract.unwrap(node.left);
|
|
4402
4428
|
const right = Extract.unwrap(node.right);
|
|
4429
|
+
if (left.type === AST_NODE_TYPES.MemberExpression) {
|
|
4430
|
+
const leftObj = Extract.unwrap(left.object);
|
|
4431
|
+
const propName = Extract.getPropertyName(left.property);
|
|
4432
|
+
if (leftObj.type === AST_NODE_TYPES.Identifier && propName != null && isFunctionExpressionLike(right)) functionVarBindings.set(`${leftObj.name}.${propName}`, right);
|
|
4433
|
+
return;
|
|
4434
|
+
}
|
|
4403
4435
|
if (left.type !== AST_NODE_TYPES.Identifier) return;
|
|
4404
4436
|
if (right.type === AST_NODE_TYPES.Identifier) {
|
|
4405
4437
|
aliases.set(left.name, right.name);
|
|
@@ -4414,7 +4446,21 @@ function create$6(context) {
|
|
|
4414
4446
|
calleeName: callee.name,
|
|
4415
4447
|
node
|
|
4416
4448
|
});
|
|
4417
|
-
if (
|
|
4449
|
+
else if (callee.type === AST_NODE_TYPES.MemberExpression) {
|
|
4450
|
+
const calleeObj = Extract.unwrap(callee.object);
|
|
4451
|
+
if (calleeObj.type === AST_NODE_TYPES.Identifier && calleeName != null) directCallSites.push({
|
|
4452
|
+
calleeName: `${calleeObj.name}.${calleeName}`,
|
|
4453
|
+
node
|
|
4454
|
+
});
|
|
4455
|
+
}
|
|
4456
|
+
if (calleeName === "useState") {
|
|
4457
|
+
const [firstArg] = node.arguments;
|
|
4458
|
+
if (firstArg != null) {
|
|
4459
|
+
const unwrappedFirstArg = Extract.unwrap(firstArg);
|
|
4460
|
+
if (isFunctionExpressionLike(unwrappedFirstArg)) stateInitializerFunctions.add(unwrappedFirstArg);
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
if (calleeName != null && (calleeName.startsWith("use") || calleeName === "mergeRefs" || calleeName === "render")) return;
|
|
4418
4464
|
for (const arg of node.arguments) {
|
|
4419
4465
|
const unwrapped = Extract.unwrap(arg);
|
|
4420
4466
|
if (unwrapped.type !== AST_NODE_TYPES.Identifier) continue;
|
|
@@ -4440,7 +4486,7 @@ function create$6(context) {
|
|
|
4440
4486
|
isWrite: (() => {
|
|
4441
4487
|
let parent = node.parent;
|
|
4442
4488
|
while (Check.isTypeExpression(parent)) parent = parent.parent;
|
|
4443
|
-
return match(parent).with({ type: AST_NODE_TYPES.AssignmentExpression }, (p) => p.left === node || Extract.unwrap(p.left) === node).with({ type: AST_NODE_TYPES.UpdateExpression }, (p) => p.argument === node || Extract.unwrap(p.argument) === node).otherwise(() => false);
|
|
4489
|
+
return match(parent).with({ type: AST_NODE_TYPES.AssignmentExpression }, (p) => p.left === node || Extract.unwrap(p.left) === node).with({ type: AST_NODE_TYPES.UpdateExpression }, (p) => p.argument === node || Extract.unwrap(p.argument) === node).otherwise(() => false) || isNestedRefCurrentWrite(node);
|
|
4444
4490
|
})(),
|
|
4445
4491
|
node
|
|
4446
4492
|
});
|
|
@@ -4479,16 +4525,19 @@ function create$6(context) {
|
|
|
4479
4525
|
} else continue;
|
|
4480
4526
|
const boundary = Traverse.findParent(node, isCompOrHookFn);
|
|
4481
4527
|
if (boundary == null) continue;
|
|
4482
|
-
if (!isReachedDuringRender(node, boundary, getReached(boundary))) continue;
|
|
4528
|
+
if (!isReachedDuringRender(node, boundary, getReached(boundary), stateInitializerFunctions)) continue;
|
|
4483
4529
|
let isLazyInit = refName != null && isInNullCheckTest(node);
|
|
4484
4530
|
let matchedGuardIf = false;
|
|
4485
4531
|
if (!isLazyInit && refName != null) {
|
|
4486
4532
|
let current = node.parent;
|
|
4533
|
+
let prevChild = node;
|
|
4487
4534
|
findLoop: while (true) {
|
|
4488
4535
|
if (current.type === AST_NODE_TYPES.IfStatement) {
|
|
4489
|
-
|
|
4490
|
-
|
|
4536
|
+
const nullBranch = getRefCurrentNullCheckBranch(current.test, refName);
|
|
4537
|
+
const actualBranch = current.consequent === prevChild ? "consequent" : current.alternate === prevChild ? "alternate" : null;
|
|
4538
|
+
if (nullBranch != null && actualBranch != null) {
|
|
4491
4539
|
matchedGuardIf = true;
|
|
4540
|
+
isLazyInit = actualBranch === nullBranch ? isWrite : !isWrite;
|
|
4492
4541
|
}
|
|
4493
4542
|
break;
|
|
4494
4543
|
}
|
|
@@ -4508,6 +4557,7 @@ function create$6(context) {
|
|
|
4508
4557
|
case AST_NODE_TYPES.CallExpression: break;
|
|
4509
4558
|
default: break findLoop;
|
|
4510
4559
|
}
|
|
4560
|
+
prevChild = current;
|
|
4511
4561
|
current = current.parent;
|
|
4512
4562
|
}
|
|
4513
4563
|
}
|
|
@@ -4520,7 +4570,7 @@ function create$6(context) {
|
|
|
4520
4570
|
if (stmtIdx >= 0) for (let i = stmtIdx - 1; i >= 0; i--) {
|
|
4521
4571
|
const sibling = block.body[i];
|
|
4522
4572
|
if (sibling == null) continue;
|
|
4523
|
-
if (sibling.type === AST_NODE_TYPES.IfStatement &&
|
|
4573
|
+
if (sibling.type === AST_NODE_TYPES.IfStatement && getRefCurrentNullCheckBranch(sibling.test, refName) === "alternate") {
|
|
4524
4574
|
isLazyInit = true;
|
|
4525
4575
|
break;
|
|
4526
4576
|
}
|
|
@@ -4548,7 +4598,7 @@ function create$6(context) {
|
|
|
4548
4598
|
for (const { node } of refPassedToFunctions) {
|
|
4549
4599
|
const boundary = Traverse.findParent(node, isCompOrHookFn);
|
|
4550
4600
|
if (boundary == null) continue;
|
|
4551
|
-
if (!isReachedDuringRender(node, boundary, getReached(boundary))) continue;
|
|
4601
|
+
if (!isReachedDuringRender(node, boundary, getReached(boundary), stateInitializerFunctions)) continue;
|
|
4552
4602
|
context.report({
|
|
4553
4603
|
messageId: "refPassedToFunction",
|
|
4554
4604
|
node
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-x",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.13.1",
|
|
4
4
|
"description": "A set of composable ESLint rules for libraries and frameworks that use React as a UI runtime.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"string-ts": "^2.3.1",
|
|
46
46
|
"ts-api-utils": "^2.5.0",
|
|
47
47
|
"ts-pattern": "^5.9.0",
|
|
48
|
-
"@eslint-react/ast": "5.
|
|
49
|
-
"@eslint-react/
|
|
50
|
-
"@eslint-react/
|
|
51
|
-
"@eslint-react/var": "5.
|
|
52
|
-
"@eslint-react/shared": "5.
|
|
53
|
-
"@eslint-react/
|
|
48
|
+
"@eslint-react/ast": "5.13.1",
|
|
49
|
+
"@eslint-react/core": "5.13.1",
|
|
50
|
+
"@eslint-react/eslint": "5.13.1",
|
|
51
|
+
"@eslint-react/var": "5.13.1",
|
|
52
|
+
"@eslint-react/shared": "5.13.1",
|
|
53
|
+
"@eslint-react/jsx": "5.13.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/react": "^19.2.17",
|