eslint-plugin-react-x 3.0.0-next.53 → 3.0.0-next.55
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 +99 -137
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ESLintUtils } from "@typescript-eslint/utils";
|
|
|
5
5
|
import { P, isMatching, match } from "ts-pattern";
|
|
6
6
|
import ts from "typescript";
|
|
7
7
|
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
8
|
-
import { findEnclosingAssignmentTarget, findVariable,
|
|
8
|
+
import { findEnclosingAssignmentTarget, findVariable, getObjectType, getVariableInitializer, isAssignmentTargetEqual } from "@eslint-react/var";
|
|
9
9
|
import { DefinitionType } from "@typescript-eslint/scope-manager";
|
|
10
10
|
import { constFalse, constTrue, constVoid, flow, getOrElseUpdate, identity, not, unit } from "@eslint-react/eff";
|
|
11
11
|
import { compare } from "compare-versions";
|
|
@@ -68,7 +68,7 @@ const rules$7 = {
|
|
|
68
68
|
//#endregion
|
|
69
69
|
//#region package.json
|
|
70
70
|
var name$6 = "eslint-plugin-react-x";
|
|
71
|
-
var version = "3.0.0-next.
|
|
71
|
+
var version = "3.0.0-next.55";
|
|
72
72
|
|
|
73
73
|
//#endregion
|
|
74
74
|
//#region src/utils/create-rule.ts
|
|
@@ -3173,6 +3173,9 @@ function create$21(context) {
|
|
|
3173
3173
|
return initNode;
|
|
3174
3174
|
}).otherwise(() => null);
|
|
3175
3175
|
if (arg0Node == null) return;
|
|
3176
|
+
function getChildScopes(scope) {
|
|
3177
|
+
return scope.childScopes.reduce((acc, child) => [...acc, ...getChildScopes(child)], [scope]);
|
|
3178
|
+
}
|
|
3176
3179
|
if (!getChildScopes(context.sourceCode.getScope(arg0Node)).flatMap((x) => x.references).some((x) => x.resolved?.scope.block === component)) {
|
|
3177
3180
|
context.report({
|
|
3178
3181
|
messageId: "default",
|
|
@@ -3254,6 +3257,9 @@ function create$20(context) {
|
|
|
3254
3257
|
return variableNode;
|
|
3255
3258
|
}).otherwise(() => null);
|
|
3256
3259
|
if (arg0Node == null) return;
|
|
3260
|
+
function getChildScopes(scope) {
|
|
3261
|
+
return scope.childScopes.reduce((acc, child) => [...acc, ...getChildScopes(child)], [scope]);
|
|
3262
|
+
}
|
|
3257
3263
|
if (!getChildScopes(context.sourceCode.getScope(arg0Node)).flatMap((x) => x.references).some((x) => x.resolved?.scope.block === component)) {
|
|
3258
3264
|
context.report({
|
|
3259
3265
|
messageId: "default",
|
|
@@ -4219,126 +4225,6 @@ function create$6(context) {
|
|
|
4219
4225
|
//#endregion
|
|
4220
4226
|
//#region src/rules/refs/refs.ts
|
|
4221
4227
|
const RULE_NAME$5 = "refs";
|
|
4222
|
-
function isWriteAccess(node) {
|
|
4223
|
-
const { parent } = node;
|
|
4224
|
-
if (parent.type === AST_NODE_TYPES.AssignmentExpression && parent.left === node) return true;
|
|
4225
|
-
if (parent.type === AST_NODE_TYPES.UpdateExpression && parent.argument === node) return true;
|
|
4226
|
-
return false;
|
|
4227
|
-
}
|
|
4228
|
-
function isInsideNestedFunction(node, boundary) {
|
|
4229
|
-
let current = node.parent;
|
|
4230
|
-
while (current != null && current !== boundary) {
|
|
4231
|
-
if (ast.isFunction(current)) return true;
|
|
4232
|
-
current = current.parent;
|
|
4233
|
-
}
|
|
4234
|
-
return false;
|
|
4235
|
-
}
|
|
4236
|
-
/**
|
|
4237
|
-
* Check if a ref.current access is part of a lazy initialization pattern.
|
|
4238
|
-
*
|
|
4239
|
-
* Standard:
|
|
4240
|
-
* if (ref.current === null) { ref.current = value; }
|
|
4241
|
-
*
|
|
4242
|
-
* Inverted (with early return):
|
|
4243
|
-
* if (ref.current !== null) { return ...; }
|
|
4244
|
-
* ref.current = computeValue();
|
|
4245
|
-
* @param node The MemberExpression node for ref.current
|
|
4246
|
-
* @param isWrite Whether this access is a write (assignment/update) or a read
|
|
4247
|
-
* @returns true if this access is part of a lazy initialization pattern and should be allowed during render
|
|
4248
|
-
*/
|
|
4249
|
-
function isPartOfLazyInitialization(node, isWrite) {
|
|
4250
|
-
if (node.object.type !== AST_NODE_TYPES.Identifier) return false;
|
|
4251
|
-
const refName = node.object.name;
|
|
4252
|
-
if (isInNullCheckTest(node)) return true;
|
|
4253
|
-
if (findEnclosingRefNullCheckIf(node, refName) != null) return true;
|
|
4254
|
-
if (isWrite && isWriteAfterNullCheckIf(node, refName)) return true;
|
|
4255
|
-
return false;
|
|
4256
|
-
}
|
|
4257
|
-
function isNullCheckOperator(operator) {
|
|
4258
|
-
return operator === "===" || operator === "==" || operator === "!==" || operator === "!=";
|
|
4259
|
-
}
|
|
4260
|
-
/**
|
|
4261
|
-
* Check if a test expression is a null check on `ref.current` for a given ref name.
|
|
4262
|
-
* @param test The test expression to check
|
|
4263
|
-
* @param refName The name of the ref variable (e.g. "myRef") to check against
|
|
4264
|
-
* @returns true if the test is of the form `ref.current === null` or `null === ref.current`
|
|
4265
|
-
*/
|
|
4266
|
-
function isRefCurrentNullCheck(test, refName) {
|
|
4267
|
-
if (test.type !== AST_NODE_TYPES.BinaryExpression) return false;
|
|
4268
|
-
if (!isNullCheckOperator(test.operator)) return false;
|
|
4269
|
-
const { left, right } = test;
|
|
4270
|
-
const isRefSide = (side) => side.type === AST_NODE_TYPES.MemberExpression && side.object.type === AST_NODE_TYPES.Identifier && side.object.name === refName && side.property.type === AST_NODE_TYPES.Identifier && side.property.name === "current";
|
|
4271
|
-
const isNullSide = (side) => side.type === AST_NODE_TYPES.Literal && side.value == null;
|
|
4272
|
-
return isRefSide(left) && isNullSide(right) || isRefSide(right) && isNullSide(left);
|
|
4273
|
-
}
|
|
4274
|
-
/**
|
|
4275
|
-
* Check if the node is the operand of a `ref.current === null` test inside an IfStatement.
|
|
4276
|
-
* @param node The MemberExpression node for ref.current
|
|
4277
|
-
* @returns true if the node is part of a null check test in an if statement
|
|
4278
|
-
*/
|
|
4279
|
-
function isInNullCheckTest(node) {
|
|
4280
|
-
const { parent } = node;
|
|
4281
|
-
if (parent.type !== AST_NODE_TYPES.BinaryExpression) return false;
|
|
4282
|
-
if (!isNullCheckOperator(parent.operator)) return false;
|
|
4283
|
-
const otherSide = parent.left === node ? parent.right : parent.left;
|
|
4284
|
-
if (otherSide.type !== AST_NODE_TYPES.Literal || otherSide.value != null) return false;
|
|
4285
|
-
return parent.parent.type === AST_NODE_TYPES.IfStatement && parent.parent.test === parent;
|
|
4286
|
-
}
|
|
4287
|
-
/**
|
|
4288
|
-
* Walk up from the node to find a containing IfStatement whose test is a null-check
|
|
4289
|
-
* on `ref.current` with the given ref name.
|
|
4290
|
-
* @param node The MemberExpression node for ref.current
|
|
4291
|
-
* @param refName The name of the ref variable (e.g. "myRef") to check against
|
|
4292
|
-
* @returns the enclosing IfStatement node if found, or null if not found
|
|
4293
|
-
*/
|
|
4294
|
-
function findEnclosingRefNullCheckIf(node, refName) {
|
|
4295
|
-
let current = node.parent;
|
|
4296
|
-
while (current != null) {
|
|
4297
|
-
if (current.type === AST_NODE_TYPES.IfStatement) return isRefCurrentNullCheck(current.test, refName) ? current : null;
|
|
4298
|
-
switch (current.type) {
|
|
4299
|
-
case AST_NODE_TYPES.ExpressionStatement:
|
|
4300
|
-
case AST_NODE_TYPES.BlockStatement:
|
|
4301
|
-
case AST_NODE_TYPES.ReturnStatement:
|
|
4302
|
-
case AST_NODE_TYPES.JSXExpressionContainer:
|
|
4303
|
-
case AST_NODE_TYPES.JSXElement:
|
|
4304
|
-
case AST_NODE_TYPES.JSXOpeningElement:
|
|
4305
|
-
case AST_NODE_TYPES.JSXClosingElement:
|
|
4306
|
-
case AST_NODE_TYPES.AssignmentExpression:
|
|
4307
|
-
case AST_NODE_TYPES.VariableDeclaration:
|
|
4308
|
-
case AST_NODE_TYPES.VariableDeclarator:
|
|
4309
|
-
case AST_NODE_TYPES.MemberExpression:
|
|
4310
|
-
case AST_NODE_TYPES.ChainExpression:
|
|
4311
|
-
case AST_NODE_TYPES.CallExpression: break;
|
|
4312
|
-
default: return null;
|
|
4313
|
-
}
|
|
4314
|
-
current = current.parent;
|
|
4315
|
-
}
|
|
4316
|
-
return null;
|
|
4317
|
-
}
|
|
4318
|
-
/**
|
|
4319
|
-
* Check if a write to `ref.current` comes after a sibling if-statement that null-checks
|
|
4320
|
-
* the same ref (inverted lazy init with early return pattern):
|
|
4321
|
-
*
|
|
4322
|
-
* if (ref.current !== null) { return ...; }
|
|
4323
|
-
* ref.current = value; // ← this write
|
|
4324
|
-
* @param node The MemberExpression node for ref.current being written to
|
|
4325
|
-
* @param refName The name of the ref variable (e.g. "myRef") to check against
|
|
4326
|
-
* @returns true if there is a preceding sibling if-statement that null-checks the same ref
|
|
4327
|
-
*/
|
|
4328
|
-
function isWriteAfterNullCheckIf(node, refName) {
|
|
4329
|
-
let stmt = node;
|
|
4330
|
-
while (stmt.parent != null && stmt.parent.type !== AST_NODE_TYPES.BlockStatement) stmt = stmt.parent;
|
|
4331
|
-
if (stmt.parent?.type !== AST_NODE_TYPES.BlockStatement) return false;
|
|
4332
|
-
const block = stmt.parent;
|
|
4333
|
-
const stmtIdx = block.body.indexOf(stmt);
|
|
4334
|
-
if (stmtIdx < 0) return false;
|
|
4335
|
-
for (let i = stmtIdx - 1; i >= 0; i--) {
|
|
4336
|
-
const sibling = block.body[i];
|
|
4337
|
-
if (sibling == null) continue;
|
|
4338
|
-
if (sibling.type === AST_NODE_TYPES.IfStatement && isRefCurrentNullCheck(sibling.test, refName)) return true;
|
|
4339
|
-
}
|
|
4340
|
-
return false;
|
|
4341
|
-
}
|
|
4342
4228
|
var refs_default = createRule({
|
|
4343
4229
|
meta: {
|
|
4344
4230
|
type: "problem",
|
|
@@ -4358,34 +4244,110 @@ function create$5(context) {
|
|
|
4358
4244
|
const cCollector = core.useComponentCollector(context);
|
|
4359
4245
|
const refAccesses = [];
|
|
4360
4246
|
const jsxRefIdentifiers = /* @__PURE__ */ new Set();
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4247
|
+
/**
|
|
4248
|
+
* Check if the node is the operand of a `ref.current === null` test inside an IfStatement.
|
|
4249
|
+
* @param node The MemberExpression node for ref.current
|
|
4250
|
+
* @returns true if the node is part of a null check test in an if statement
|
|
4251
|
+
*/
|
|
4252
|
+
function isInNullCheckTest(node) {
|
|
4253
|
+
const { parent } = node;
|
|
4254
|
+
if (!isMatching({
|
|
4255
|
+
type: AST_NODE_TYPES.BinaryExpression,
|
|
4256
|
+
operator: P.union("===", "==", "!==", "!=")
|
|
4257
|
+
}, parent)) return false;
|
|
4258
|
+
const otherSide = parent.left === node ? parent.right : parent.left;
|
|
4259
|
+
if (otherSide.type !== AST_NODE_TYPES.Literal || otherSide.value != null) return false;
|
|
4260
|
+
return parent.parent.type === AST_NODE_TYPES.IfStatement && parent.parent.test === parent;
|
|
4261
|
+
}
|
|
4262
|
+
/**
|
|
4263
|
+
* Check if a test expression is a null check on `ref.current` for a given ref name.
|
|
4264
|
+
* Matches forms like `ref.current === null`, `null === ref.current`, and their != variants.
|
|
4265
|
+
*/
|
|
4266
|
+
function isRefCurrentNullCheck(test, refName) {
|
|
4267
|
+
if (test.type !== AST_NODE_TYPES.BinaryExpression) return false;
|
|
4268
|
+
const op = test.operator;
|
|
4269
|
+
if (op !== "===" && op !== "==" && op !== "!==" && op !== "!=") return false;
|
|
4270
|
+
const { left, right } = test;
|
|
4271
|
+
const checkSides = (a, b) => a.type === AST_NODE_TYPES.MemberExpression && a.object.type === AST_NODE_TYPES.Identifier && a.object.name === refName && a.property.type === AST_NODE_TYPES.Identifier && a.property.name === "current" && b.type === AST_NODE_TYPES.Literal && b.value == null;
|
|
4272
|
+
return checkSides(left, right) || checkSides(right, left);
|
|
4366
4273
|
}
|
|
4367
4274
|
return defineRuleListener(hCollector.visitor, cCollector.visitor, {
|
|
4368
4275
|
JSXAttribute(node) {
|
|
4369
|
-
|
|
4276
|
+
switch (true) {
|
|
4277
|
+
case node.name.type === AST_NODE_TYPES.JSXIdentifier && node.name.name === "ref" && node.value?.type === AST_NODE_TYPES.JSXExpressionContainer && node.value.expression.type === AST_NODE_TYPES.Identifier:
|
|
4278
|
+
jsxRefIdentifiers.add(node.value.expression.name);
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4370
4281
|
},
|
|
4371
4282
|
MemberExpression(node) {
|
|
4372
|
-
if (
|
|
4283
|
+
if (!ast.isIdentifier(node.property, "current")) return;
|
|
4373
4284
|
refAccesses.push({
|
|
4374
4285
|
node,
|
|
4375
|
-
isWrite:
|
|
4286
|
+
isWrite: match(node.parent).with({ type: AST_NODE_TYPES.AssignmentExpression }, (p) => p.left === node).with({ type: AST_NODE_TYPES.UpdateExpression }, (p) => p.argument === node).otherwise(() => false)
|
|
4376
4287
|
});
|
|
4377
4288
|
},
|
|
4378
4289
|
"Program:exit"(program) {
|
|
4379
|
-
const
|
|
4290
|
+
const comps = cCollector.ctx.getAllComponents(program);
|
|
4380
4291
|
const hooks = hCollector.ctx.getAllHooks(program);
|
|
4381
|
-
const
|
|
4382
|
-
const
|
|
4292
|
+
const funcs = new Set([...comps.map((c) => c.node), ...hooks.map((h) => h.node)]);
|
|
4293
|
+
const isCompOrHookFn = (n) => ast.isFunction(n) && funcs.has(n);
|
|
4383
4294
|
for (const { node, isWrite } of refAccesses) {
|
|
4384
|
-
|
|
4385
|
-
|
|
4295
|
+
const obj = node.object;
|
|
4296
|
+
if (obj.type !== AST_NODE_TYPES.Identifier) continue;
|
|
4297
|
+
switch (true) {
|
|
4298
|
+
case core.isRefLikeName(obj.name):
|
|
4299
|
+
case jsxRefIdentifiers.has(obj.name):
|
|
4300
|
+
case core.isInitializedFromRef(obj.name, context.sourceCode.getScope(node.object)): break;
|
|
4301
|
+
default: continue;
|
|
4302
|
+
}
|
|
4303
|
+
const boundary = ast.findParentNode(node, isCompOrHookFn);
|
|
4386
4304
|
if (boundary == null) continue;
|
|
4387
|
-
if (
|
|
4388
|
-
|
|
4305
|
+
if (ast.findParentNode(node, ast.isFunction) !== boundary) continue;
|
|
4306
|
+
const refName = obj.name;
|
|
4307
|
+
let isLazyInit = isInNullCheckTest(node);
|
|
4308
|
+
if (!isLazyInit) {
|
|
4309
|
+
let current = node.parent;
|
|
4310
|
+
findIf: while (current != null) {
|
|
4311
|
+
if (current.type === AST_NODE_TYPES.IfStatement) {
|
|
4312
|
+
if (isRefCurrentNullCheck(current.test, refName)) isLazyInit = true;
|
|
4313
|
+
break;
|
|
4314
|
+
}
|
|
4315
|
+
switch (current.type) {
|
|
4316
|
+
case AST_NODE_TYPES.ExpressionStatement:
|
|
4317
|
+
case AST_NODE_TYPES.BlockStatement:
|
|
4318
|
+
case AST_NODE_TYPES.ReturnStatement:
|
|
4319
|
+
case AST_NODE_TYPES.JSXExpressionContainer:
|
|
4320
|
+
case AST_NODE_TYPES.JSXElement:
|
|
4321
|
+
case AST_NODE_TYPES.JSXOpeningElement:
|
|
4322
|
+
case AST_NODE_TYPES.JSXClosingElement:
|
|
4323
|
+
case AST_NODE_TYPES.AssignmentExpression:
|
|
4324
|
+
case AST_NODE_TYPES.VariableDeclaration:
|
|
4325
|
+
case AST_NODE_TYPES.VariableDeclarator:
|
|
4326
|
+
case AST_NODE_TYPES.MemberExpression:
|
|
4327
|
+
case AST_NODE_TYPES.ChainExpression:
|
|
4328
|
+
case AST_NODE_TYPES.CallExpression: break;
|
|
4329
|
+
default: break findIf;
|
|
4330
|
+
}
|
|
4331
|
+
current = current.parent;
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
if (!isLazyInit && isWrite) {
|
|
4335
|
+
let stmt = node;
|
|
4336
|
+
while (stmt.parent != null && stmt.parent.type !== AST_NODE_TYPES.BlockStatement) stmt = stmt.parent;
|
|
4337
|
+
if (stmt.parent?.type === AST_NODE_TYPES.BlockStatement) {
|
|
4338
|
+
const block = stmt.parent;
|
|
4339
|
+
const stmtIdx = block.body.indexOf(stmt);
|
|
4340
|
+
if (stmtIdx >= 0) for (let i = stmtIdx - 1; i >= 0; i--) {
|
|
4341
|
+
const sibling = block.body[i];
|
|
4342
|
+
if (sibling == null) continue;
|
|
4343
|
+
if (sibling.type === AST_NODE_TYPES.IfStatement && isRefCurrentNullCheck(sibling.test, refName)) {
|
|
4344
|
+
isLazyInit = true;
|
|
4345
|
+
break;
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
if (isLazyInit) continue;
|
|
4389
4351
|
context.report({
|
|
4390
4352
|
messageId: isWrite ? "writeDuringRender" : "readDuringRender",
|
|
4391
4353
|
node
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-x",
|
|
3
|
-
"version": "3.0.0-next.
|
|
3
|
+
"version": "3.0.0-next.55",
|
|
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,17 +45,17 @@
|
|
|
45
45
|
"string-ts": "^2.3.1",
|
|
46
46
|
"ts-api-utils": "^2.4.0",
|
|
47
47
|
"ts-pattern": "^5.9.0",
|
|
48
|
-
"@eslint-react/ast": "3.0.0-next.
|
|
49
|
-
"@eslint-react/
|
|
50
|
-
"@eslint-react/
|
|
51
|
-
"@eslint-react/
|
|
52
|
-
"@eslint-react/
|
|
48
|
+
"@eslint-react/ast": "3.0.0-next.55",
|
|
49
|
+
"@eslint-react/eff": "3.0.0-next.55",
|
|
50
|
+
"@eslint-react/shared": "3.0.0-next.55",
|
|
51
|
+
"@eslint-react/var": "3.0.0-next.55",
|
|
52
|
+
"@eslint-react/core": "3.0.0-next.55"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/react": "^19.2.14",
|
|
56
56
|
"@types/react-dom": "^19.2.3",
|
|
57
|
-
"tsdown": "^0.
|
|
58
|
-
"tsl-dx": "^0.
|
|
57
|
+
"tsdown": "^0.21.0-beta.1",
|
|
58
|
+
"tsl-dx": "^0.8.0",
|
|
59
59
|
"@local/configs": "0.0.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|