eslint-plugin-react-x 5.13.2 → 5.14.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 +351 -300
- 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.14.1";
|
|
148
148
|
|
|
149
149
|
//#endregion
|
|
150
150
|
//#region src/utils/create-rule.ts
|
|
@@ -1331,7 +1331,7 @@ function resolveToFunctionNode(context, node, seen = /* @__PURE__ */ new Set())
|
|
|
1331
1331
|
* Refs are mutable by design and exempted from immutability checks.
|
|
1332
1332
|
* @param name The identifier name to check.
|
|
1333
1333
|
*/
|
|
1334
|
-
function isRefLikeName(name) {
|
|
1334
|
+
function isRefLikeName$1(name) {
|
|
1335
1335
|
return name === "ref" || name.endsWith("Ref");
|
|
1336
1336
|
}
|
|
1337
1337
|
/**
|
|
@@ -1340,10 +1340,10 @@ function isRefLikeName(name) {
|
|
|
1340
1340
|
* @param node The AST node to inspect.
|
|
1341
1341
|
*/
|
|
1342
1342
|
function hasRefLikeNameInChain(node) {
|
|
1343
|
-
if (node.type === AST_NODE_TYPES.Identifier) return isRefLikeName(node.name);
|
|
1343
|
+
if (node.type === AST_NODE_TYPES.Identifier) return isRefLikeName$1(node.name);
|
|
1344
1344
|
if (node.type === AST_NODE_TYPES.MemberExpression) {
|
|
1345
1345
|
const propName = Extract.getPropertyName(node.property);
|
|
1346
|
-
if (propName != null && isRefLikeName(propName)) return true;
|
|
1346
|
+
if (propName != null && isRefLikeName$1(propName)) return true;
|
|
1347
1347
|
return hasRefLikeNameInChain(node.object);
|
|
1348
1348
|
}
|
|
1349
1349
|
return false;
|
|
@@ -1411,7 +1411,7 @@ function create$48(context) {
|
|
|
1411
1411
|
const left = Extract.unwrap(node.left);
|
|
1412
1412
|
switch (left.type) {
|
|
1413
1413
|
case AST_NODE_TYPES.Identifier:
|
|
1414
|
-
if (isRefLikeName(left.name)) return;
|
|
1414
|
+
if (isRefLikeName$1(left.name)) return;
|
|
1415
1415
|
pushMutationSite(node, left);
|
|
1416
1416
|
return;
|
|
1417
1417
|
case AST_NODE_TYPES.MemberExpression: {
|
|
@@ -1495,7 +1495,7 @@ function create$48(context) {
|
|
|
1495
1495
|
const arg = Extract.unwrap(node.argument);
|
|
1496
1496
|
switch (arg.type) {
|
|
1497
1497
|
case AST_NODE_TYPES.Identifier:
|
|
1498
|
-
if (isRefLikeName(arg.name)) return;
|
|
1498
|
+
if (isRefLikeName$1(arg.name)) return;
|
|
1499
1499
|
pushMutationSite(node, arg);
|
|
1500
1500
|
return;
|
|
1501
1501
|
case AST_NODE_TYPES.MemberExpression: {
|
|
@@ -4266,83 +4266,64 @@ function create$7(context) {
|
|
|
4266
4266
|
|
|
4267
4267
|
//#endregion
|
|
4268
4268
|
//#region src/rules/refs/lib.ts
|
|
4269
|
+
const SYNC_ARRAY_CALLBACKS = /* @__PURE__ */ new Set([
|
|
4270
|
+
"every",
|
|
4271
|
+
"filter",
|
|
4272
|
+
"find",
|
|
4273
|
+
"findIndex",
|
|
4274
|
+
"findLast",
|
|
4275
|
+
"findLastIndex",
|
|
4276
|
+
"flatMap",
|
|
4277
|
+
"forEach",
|
|
4278
|
+
"map",
|
|
4279
|
+
"reduce",
|
|
4280
|
+
"reduceRight",
|
|
4281
|
+
"some",
|
|
4282
|
+
"sort"
|
|
4283
|
+
]);
|
|
4284
|
+
function getLatestValue(events, position) {
|
|
4285
|
+
let latest = null;
|
|
4286
|
+
for (const event of events ?? []) {
|
|
4287
|
+
if (event.position > position) continue;
|
|
4288
|
+
if (latest == null || event.position >= latest.position) latest = event;
|
|
4289
|
+
}
|
|
4290
|
+
return latest;
|
|
4291
|
+
}
|
|
4269
4292
|
function isFunctionExpressionLike(node) {
|
|
4270
4293
|
return node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression;
|
|
4271
4294
|
}
|
|
4272
|
-
function
|
|
4273
|
-
|
|
4274
|
-
while (aliases.has(name) && !seen.has(name)) {
|
|
4275
|
-
seen.add(name);
|
|
4276
|
-
name = aliases.get(name);
|
|
4277
|
-
}
|
|
4278
|
-
return name;
|
|
4279
|
-
}
|
|
4280
|
-
/**
|
|
4281
|
-
* Check if the node is the operand of a `ref.current === null` test inside an IfStatement.
|
|
4282
|
-
* @param node The MemberExpression node for ref.current
|
|
4283
|
-
* @returns true if the node is part of a null check test in an if statement
|
|
4284
|
-
*/
|
|
4285
|
-
function isInNullCheckTest(node) {
|
|
4286
|
-
let parent = node.parent;
|
|
4287
|
-
while (Check.isTypeExpression(parent)) parent = parent.parent;
|
|
4288
|
-
const isNullCompareBinary = (n) => {
|
|
4289
|
-
return n.type === AST_NODE_TYPES.BinaryExpression && /^(===|==|!==|!=)$/.test(n.operator);
|
|
4290
|
-
};
|
|
4291
|
-
const isLiteralNull = (n) => {
|
|
4292
|
-
return n.type === AST_NODE_TYPES.Literal && n.value == null;
|
|
4293
|
-
};
|
|
4294
|
-
const isIfTest = (testNode) => {
|
|
4295
|
-
return testNode.parent?.type === AST_NODE_TYPES.IfStatement && testNode.parent.test === testNode;
|
|
4296
|
-
};
|
|
4297
|
-
if (isNullCompareBinary(parent)) {
|
|
4298
|
-
if (!isLiteralNull(parent.left === node || Extract.unwrap(parent.left) === node ? parent.right : parent.left)) return false;
|
|
4299
|
-
if (isIfTest(parent)) return true;
|
|
4300
|
-
const grandparent = parent.parent;
|
|
4301
|
-
if (grandparent.type === AST_NODE_TYPES.UnaryExpression && grandparent.operator === "!" && isIfTest(grandparent)) return true;
|
|
4302
|
-
return false;
|
|
4303
|
-
}
|
|
4304
|
-
if (parent.type === AST_NODE_TYPES.UnaryExpression && parent.operator === "!") return isIfTest(parent);
|
|
4305
|
-
return false;
|
|
4295
|
+
function isRefLikeName(name) {
|
|
4296
|
+
return name === "ref" || name.endsWith("Ref");
|
|
4306
4297
|
}
|
|
4307
|
-
function
|
|
4308
|
-
|
|
4309
|
-
if (
|
|
4310
|
-
if (Extract.getPropertyName(
|
|
4311
|
-
|
|
4312
|
-
return obj.type === AST_NODE_TYPES.Identifier && obj.name === refName && b.type === AST_NODE_TYPES.Literal && b.value == null;
|
|
4298
|
+
function getCalleeName(node) {
|
|
4299
|
+
const callee = Extract.unwrap(node.callee);
|
|
4300
|
+
if (callee.type === AST_NODE_TYPES.Identifier) return callee.name;
|
|
4301
|
+
if (callee.type === AST_NODE_TYPES.MemberExpression) return Extract.getPropertyName(callee.property);
|
|
4302
|
+
return null;
|
|
4313
4303
|
}
|
|
4314
|
-
function
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4304
|
+
function getSynchronousCallbackIndexes(node) {
|
|
4305
|
+
const callee = Extract.unwrap(node.callee);
|
|
4306
|
+
const name = getCalleeName(node);
|
|
4307
|
+
if (name == null) return [];
|
|
4308
|
+
if (callee.type === AST_NODE_TYPES.MemberExpression && SYNC_ARRAY_CALLBACKS.has(name)) return [0];
|
|
4309
|
+
switch (name) {
|
|
4310
|
+
case "useMemo":
|
|
4311
|
+
case "useState": return [0];
|
|
4312
|
+
case "useReducer": return [0, 2];
|
|
4313
|
+
default: return [];
|
|
4314
|
+
}
|
|
4319
4315
|
}
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
*/
|
|
4326
|
-
function getRefCurrentNullCheckBranch(test, refName) {
|
|
4327
|
-
if (isBinaryNullCheck(test, refName)) return test.operator === "===" || test.operator === "==" ? "consequent" : "alternate";
|
|
4328
|
-
if (test.type === AST_NODE_TYPES.UnaryExpression && test.operator === "!") {
|
|
4329
|
-
const arg = Extract.unwrap(test.argument);
|
|
4330
|
-
if (arg.type === AST_NODE_TYPES.MemberExpression) {
|
|
4331
|
-
const obj = Extract.unwrap(arg.object);
|
|
4332
|
-
return obj.type === AST_NODE_TYPES.Identifier && obj.name === refName && Extract.getPropertyName(arg.property) === "current" ? "consequent" : null;
|
|
4333
|
-
}
|
|
4334
|
-
if (arg.type === AST_NODE_TYPES.BinaryExpression) {
|
|
4335
|
-
const inner = getRefCurrentNullCheckBranch(arg, refName);
|
|
4336
|
-
if (inner === "consequent") return "alternate";
|
|
4337
|
-
if (inner === "alternate") return "consequent";
|
|
4338
|
-
}
|
|
4316
|
+
function isReachedThroughFunctions(node, boundary, reached) {
|
|
4317
|
+
let current = node.parent;
|
|
4318
|
+
while (current != null && current !== boundary) {
|
|
4319
|
+
if (Check.isFunction(current) && !reached.has(current)) return false;
|
|
4320
|
+
current = current.parent;
|
|
4339
4321
|
}
|
|
4340
|
-
return
|
|
4322
|
+
return current === boundary;
|
|
4341
4323
|
}
|
|
4342
4324
|
/**
|
|
4343
4325
|
* Check whether `node` (a `ref.current` MemberExpression) is being written to indirectly through
|
|
4344
4326
|
* a nested property write, e.g. `ref.current.inner = value` or `ref.current.inner++`.
|
|
4345
|
-
* @param node The MemberExpression node for ref.current
|
|
4346
4327
|
*/
|
|
4347
4328
|
function isNestedRefCurrentWrite(node) {
|
|
4348
4329
|
let outer = node;
|
|
@@ -4361,19 +4342,87 @@ function isNestedRefCurrentWrite(node) {
|
|
|
4361
4342
|
return false;
|
|
4362
4343
|
}
|
|
4363
4344
|
}
|
|
4364
|
-
function
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4345
|
+
function getRefAccess(node) {
|
|
4346
|
+
let parent = node.parent;
|
|
4347
|
+
while (Check.isTypeExpression(parent)) parent = parent.parent;
|
|
4348
|
+
const isDirectWrite = parent.type === AST_NODE_TYPES.AssignmentExpression ? parent.left === node || Extract.unwrap(parent.left) === node : parent.type === AST_NODE_TYPES.UpdateExpression ? parent.argument === node || Extract.unwrap(parent.argument) === node : false;
|
|
4349
|
+
return {
|
|
4350
|
+
isInitializationWrite: parent.type === AST_NODE_TYPES.AssignmentExpression && parent.operator === "=" && (parent.left === node || Extract.unwrap(parent.left) === node),
|
|
4351
|
+
isWrite: isDirectWrite || isNestedRefCurrentWrite(node),
|
|
4352
|
+
node
|
|
4353
|
+
};
|
|
4354
|
+
}
|
|
4355
|
+
/**
|
|
4356
|
+
* Parse an exact nullish check and return the branch where the checked value is null/undefined.
|
|
4357
|
+
* Truthiness checks such as `!ref.current` are deliberately excluded: falsy ref values are not
|
|
4358
|
+
* necessarily uninitialized.
|
|
4359
|
+
*/
|
|
4360
|
+
function getNullCheckBranch(test, isCheckedValue, isNullishValue) {
|
|
4361
|
+
const unwrapped = Extract.unwrap(test);
|
|
4362
|
+
if (unwrapped.type === AST_NODE_TYPES.UnaryExpression && unwrapped.operator === "!") {
|
|
4363
|
+
const inner = Extract.unwrap(unwrapped.argument);
|
|
4364
|
+
if (inner.type !== AST_NODE_TYPES.BinaryExpression) return null;
|
|
4365
|
+
const branch = getNullCheckBranch(inner, isCheckedValue, isNullishValue);
|
|
4366
|
+
if (branch === "consequent") return "alternate";
|
|
4367
|
+
if (branch === "alternate") return "consequent";
|
|
4368
|
+
return null;
|
|
4369
|
+
}
|
|
4370
|
+
if (unwrapped.type !== AST_NODE_TYPES.BinaryExpression || !/^(===|==|!==|!=)$/.test(unwrapped.operator)) return null;
|
|
4371
|
+
const matches = (left, right) => {
|
|
4372
|
+
return isCheckedValue(Extract.unwrap(left)) && isNullishValue(Extract.unwrap(right));
|
|
4373
|
+
};
|
|
4374
|
+
if (!matches(unwrapped.left, unwrapped.right) && !matches(unwrapped.right, unwrapped.left)) return null;
|
|
4375
|
+
return unwrapped.operator === "===" || unwrapped.operator === "==" ? "consequent" : "alternate";
|
|
4376
|
+
}
|
|
4377
|
+
function isGuardTestAccess(node, getNullBranch) {
|
|
4378
|
+
let current = node;
|
|
4379
|
+
while (Check.isTypeExpression(current.parent)) current = current.parent;
|
|
4380
|
+
if (current.parent.type === AST_NODE_TYPES.BinaryExpression) current = current.parent;
|
|
4381
|
+
if (current.parent.type === AST_NODE_TYPES.UnaryExpression && current.parent.operator === "!") current = current.parent;
|
|
4382
|
+
return current.parent.type === AST_NODE_TYPES.IfStatement && current.parent.test === current && getNullBranch(current.parent.test) != null;
|
|
4383
|
+
}
|
|
4384
|
+
function getGuardDisposition(node, getNullBranch) {
|
|
4385
|
+
let child = node;
|
|
4386
|
+
let current = node.parent;
|
|
4387
|
+
while (current != null && !Check.isFunction(current)) {
|
|
4388
|
+
if (current.type === AST_NODE_TYPES.IfStatement) {
|
|
4389
|
+
const actualBranch = current.consequent === child ? "consequent" : current.alternate === child ? "alternate" : null;
|
|
4390
|
+
const nullBranch = getNullBranch(current.test);
|
|
4391
|
+
if (actualBranch != null && nullBranch != null) return { inNullBranch: actualBranch === nullBranch };
|
|
4392
|
+
}
|
|
4393
|
+
child = current;
|
|
4394
|
+
current = current.parent;
|
|
4395
|
+
}
|
|
4396
|
+
return null;
|
|
4397
|
+
}
|
|
4398
|
+
/** Return whether every path through a statement exits the current function. */
|
|
4399
|
+
function isUnconditionallyTerminating(statement) {
|
|
4400
|
+
switch (statement.type) {
|
|
4401
|
+
case AST_NODE_TYPES.ReturnStatement:
|
|
4402
|
+
case AST_NODE_TYPES.ThrowStatement: return true;
|
|
4403
|
+
case AST_NODE_TYPES.BlockStatement: {
|
|
4404
|
+
const last = statement.body.at(-1);
|
|
4405
|
+
return last != null && isUnconditionallyTerminating(last);
|
|
4376
4406
|
}
|
|
4407
|
+
case AST_NODE_TYPES.IfStatement: return statement.alternate != null && isUnconditionallyTerminating(statement.consequent) && isUnconditionallyTerminating(statement.alternate);
|
|
4408
|
+
default: return false;
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
function isAfterTerminatingNonNullGuard(node, getNullBranch) {
|
|
4412
|
+
let statement = node;
|
|
4413
|
+
while (statement.parent?.type !== AST_NODE_TYPES.BlockStatement) {
|
|
4414
|
+
if (statement.parent == null || Check.isFunction(statement.parent)) return false;
|
|
4415
|
+
statement = statement.parent;
|
|
4416
|
+
}
|
|
4417
|
+
const block = statement.parent;
|
|
4418
|
+
const index = block.body.indexOf(statement);
|
|
4419
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
4420
|
+
const sibling = block.body[i];
|
|
4421
|
+
if (sibling?.type !== AST_NODE_TYPES.IfStatement) continue;
|
|
4422
|
+
const nullBranch = getNullBranch(sibling.test);
|
|
4423
|
+
if (nullBranch == null) continue;
|
|
4424
|
+
const nonNullBranch = nullBranch === "alternate" ? sibling.consequent : sibling.alternate;
|
|
4425
|
+
return nonNullBranch != null && isUnconditionallyTerminating(nonNullBranch);
|
|
4377
4426
|
}
|
|
4378
4427
|
return false;
|
|
4379
4428
|
}
|
|
@@ -4397,251 +4446,253 @@ var refs_default = createRule({
|
|
|
4397
4446
|
create: create$6,
|
|
4398
4447
|
defaultOptions: []
|
|
4399
4448
|
});
|
|
4400
|
-
/**
|
|
4401
|
-
* Phase 2 (part): compute the set of functions that are "reached" during render for a given
|
|
4402
|
-
* component/hook boundary. A function is reached if it is the boundary itself, or if it is
|
|
4403
|
-
* directly called (possibly through a simple variable alias, e.g. `const b = a; b();`) from
|
|
4404
|
-
* somewhere that is itself reached. This lets us detect ref mutations/reads that happen inside
|
|
4405
|
-
* a helper function which is invoked synchronously during render (see "Ref Mutation in Called
|
|
4406
|
-
* Function" in refs.spec.md), as opposed to functions that are merely defined and handed off
|
|
4407
|
-
* to be called later (e.g. event handlers, effect callbacks).
|
|
4408
|
-
*/
|
|
4409
|
-
function computeReachedFunctions(boundary, isCompOrHookFn, functionVarBindings, directCallSites, aliases) {
|
|
4410
|
-
const reached = /* @__PURE__ */ new Set([boundary]);
|
|
4411
|
-
const relevantCalls = directCallSites.filter((c) => Traverse.findParent(c.node, isCompOrHookFn) === boundary);
|
|
4412
|
-
let changed = true;
|
|
4413
|
-
for (let iterations = 0; changed && iterations < 50; iterations++) {
|
|
4414
|
-
changed = false;
|
|
4415
|
-
for (const { calleeName, node: callNode } of relevantCalls) {
|
|
4416
|
-
const hostFn = Traverse.findParent(callNode, Check.isFunction) ?? boundary;
|
|
4417
|
-
if (!reached.has(hostFn)) continue;
|
|
4418
|
-
const resolvedName = resolveAlias(calleeName, aliases);
|
|
4419
|
-
const target = functionVarBindings.get(resolvedName);
|
|
4420
|
-
if (target == null || reached.has(target)) continue;
|
|
4421
|
-
if (Traverse.findParent(target, isCompOrHookFn) !== boundary) continue;
|
|
4422
|
-
reached.add(target);
|
|
4423
|
-
changed = true;
|
|
4424
|
-
}
|
|
4425
|
-
}
|
|
4426
|
-
return reached;
|
|
4427
|
-
}
|
|
4428
|
-
/**
|
|
4429
|
-
* Phase 2 (part): determine whether a ref access node is reached unconditionally during render,
|
|
4430
|
-
* i.e. every function boundary between the access and the component/hook `boundary` is itself
|
|
4431
|
-
* a function that gets invoked during render (see `computeReachedFunctions`).
|
|
4432
|
-
*/
|
|
4433
|
-
function isReachedDuringRender(node, boundary, reached, alwaysReached) {
|
|
4434
|
-
let current = node.parent;
|
|
4435
|
-
while (current != null && current !== boundary) {
|
|
4436
|
-
if (Check.isFunction(current) && !reached.has(current) && !alwaysReached.has(current)) return false;
|
|
4437
|
-
current = current.parent;
|
|
4438
|
-
}
|
|
4439
|
-
return true;
|
|
4440
|
-
}
|
|
4441
4449
|
function create$6(context) {
|
|
4442
4450
|
const hc = core.getHookCollector(context);
|
|
4443
4451
|
const fc = core.getFunctionComponentCollector(context);
|
|
4452
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
4453
|
+
const memberBindings = /* @__PURE__ */ new Map();
|
|
4454
|
+
const jsxRefs = /* @__PURE__ */ new Set();
|
|
4455
|
+
const calls = [];
|
|
4444
4456
|
const refAccesses = [];
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4457
|
+
function getVariable(node) {
|
|
4458
|
+
return findVariable(context.sourceCode.getScope(node), node) ?? null;
|
|
4459
|
+
}
|
|
4460
|
+
function addBinding(variable, value, position) {
|
|
4461
|
+
const events = bindings.get(variable) ?? [];
|
|
4462
|
+
bindings.set(variable, events);
|
|
4463
|
+
events.push({
|
|
4464
|
+
position,
|
|
4465
|
+
value
|
|
4466
|
+
});
|
|
4467
|
+
}
|
|
4468
|
+
function addIdentifierBinding(node, value, position = node.range[0]) {
|
|
4469
|
+
const variable = getVariable(node);
|
|
4470
|
+
if (variable != null) addBinding(variable, value, position);
|
|
4471
|
+
}
|
|
4472
|
+
function addMemberBinding(object, property, value, position) {
|
|
4473
|
+
const variable = getVariable(object);
|
|
4474
|
+
if (variable == null) return;
|
|
4475
|
+
const properties = memberBindings.get(variable) ?? /* @__PURE__ */ new Map();
|
|
4476
|
+
memberBindings.set(variable, properties);
|
|
4477
|
+
const events = properties.get(property) ?? [];
|
|
4478
|
+
properties.set(property, events);
|
|
4479
|
+
events.push({
|
|
4480
|
+
position,
|
|
4481
|
+
value
|
|
4482
|
+
});
|
|
4483
|
+
}
|
|
4484
|
+
function bindingValueFrom(node) {
|
|
4485
|
+
const value = Extract.unwrap(node);
|
|
4486
|
+
if (value.type === AST_NODE_TYPES.Identifier) {
|
|
4487
|
+
const variable = getVariable(value);
|
|
4488
|
+
return variable == null ? { kind: "unknown" } : {
|
|
4489
|
+
kind: "variable",
|
|
4490
|
+
variable
|
|
4491
|
+
};
|
|
4492
|
+
}
|
|
4493
|
+
if (isFunctionExpressionLike(value)) return {
|
|
4494
|
+
kind: "function",
|
|
4495
|
+
node: value
|
|
4496
|
+
};
|
|
4497
|
+
if (value.type === AST_NODE_TYPES.CallExpression && (core.isUseRefCall(context, value) || core.isCreateRefCall(context, value))) return { kind: "ref" };
|
|
4498
|
+
if (value.type === AST_NODE_TYPES.MemberExpression) {
|
|
4499
|
+
const property = Extract.getPropertyName(value.property);
|
|
4500
|
+
if (property != null && isRefLikeName(property)) return { kind: "ref" };
|
|
4501
|
+
}
|
|
4502
|
+
return { kind: "unknown" };
|
|
4503
|
+
}
|
|
4504
|
+
function resolveRef(variable, position, seen = /* @__PURE__ */ new Set()) {
|
|
4505
|
+
if (seen.has(variable)) return null;
|
|
4506
|
+
seen.add(variable);
|
|
4507
|
+
if (jsxRefs.has(variable) || isRefLikeName(variable.name)) return variable;
|
|
4508
|
+
const event = getLatestValue(bindings.get(variable), position);
|
|
4509
|
+
if (event != null) switch (event.value.kind) {
|
|
4510
|
+
case "ref": return variable;
|
|
4511
|
+
case "variable": return resolveRef(event.value.variable, event.position, seen);
|
|
4512
|
+
case "function":
|
|
4513
|
+
case "unknown": return null;
|
|
4514
|
+
}
|
|
4515
|
+
return null;
|
|
4516
|
+
}
|
|
4517
|
+
function resolveFunction(variable, position, seen = /* @__PURE__ */ new Set()) {
|
|
4518
|
+
if (seen.has(variable)) return null;
|
|
4519
|
+
seen.add(variable);
|
|
4520
|
+
const event = getLatestValue(bindings.get(variable), position);
|
|
4521
|
+
if (event == null) return null;
|
|
4522
|
+
switch (event.value.kind) {
|
|
4523
|
+
case "function": return event.value.node;
|
|
4524
|
+
case "variable": return resolveFunction(event.value.variable, event.position, seen);
|
|
4525
|
+
case "ref":
|
|
4526
|
+
case "unknown": return null;
|
|
4527
|
+
}
|
|
4528
|
+
}
|
|
4529
|
+
function resolveCallable(node, position) {
|
|
4530
|
+
const callee = Extract.unwrap(node);
|
|
4531
|
+
if (isFunctionExpressionLike(callee)) return callee;
|
|
4532
|
+
if (callee.type === AST_NODE_TYPES.Identifier) {
|
|
4533
|
+
const variable = getVariable(callee);
|
|
4534
|
+
return variable == null ? null : resolveFunction(variable, position);
|
|
4535
|
+
}
|
|
4536
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression) return null;
|
|
4537
|
+
const object = Extract.unwrap(callee.object);
|
|
4538
|
+
const property = Extract.getPropertyName(callee.property);
|
|
4539
|
+
if (object.type !== AST_NODE_TYPES.Identifier || property == null) return null;
|
|
4540
|
+
const variable = getVariable(object);
|
|
4541
|
+
if (variable == null) return null;
|
|
4542
|
+
const event = getLatestValue(memberBindings.get(variable)?.get(property), position);
|
|
4543
|
+
if (event == null) return null;
|
|
4544
|
+
if (event.value.kind === "function") return event.value.node;
|
|
4545
|
+
if (event.value.kind === "variable") return resolveFunction(event.value.variable, event.position);
|
|
4546
|
+
return null;
|
|
4547
|
+
}
|
|
4548
|
+
function getRefTarget(node) {
|
|
4549
|
+
const object = Extract.unwrap(node.object);
|
|
4550
|
+
if (object.type === AST_NODE_TYPES.Identifier) {
|
|
4551
|
+
const variable = getVariable(object);
|
|
4552
|
+
if (variable == null) return null;
|
|
4553
|
+
const identity = resolveRef(variable, node.range[0]);
|
|
4554
|
+
return identity == null ? null : { identity };
|
|
4555
|
+
}
|
|
4556
|
+
if (object.type === AST_NODE_TYPES.MemberExpression) {
|
|
4557
|
+
const property = Extract.getPropertyName(object.property);
|
|
4558
|
+
if (property != null && isRefLikeName(property)) return { identity: null };
|
|
4559
|
+
}
|
|
4560
|
+
return null;
|
|
4561
|
+
}
|
|
4562
|
+
function getNullBranch(test, identity) {
|
|
4563
|
+
return getNullCheckBranch(test, (candidate) => {
|
|
4564
|
+
if (candidate.type !== AST_NODE_TYPES.MemberExpression || Extract.getPropertyName(candidate.property) !== "current") return false;
|
|
4565
|
+
return getRefTarget(candidate)?.identity === identity;
|
|
4566
|
+
}, (candidate) => {
|
|
4567
|
+
if (candidate.type === AST_NODE_TYPES.Literal) return candidate.value == null;
|
|
4568
|
+
if (candidate.type === AST_NODE_TYPES.UnaryExpression && candidate.operator === "void") return true;
|
|
4569
|
+
if (candidate.type !== AST_NODE_TYPES.Identifier || candidate.name !== "undefined") return false;
|
|
4570
|
+
const variable = getVariable(candidate);
|
|
4571
|
+
return variable == null || variable.defs.length === 0;
|
|
4572
|
+
});
|
|
4573
|
+
}
|
|
4451
4574
|
return merge(hc.visitor, fc.visitor, {
|
|
4452
4575
|
AssignmentExpression(node) {
|
|
4453
4576
|
if (node.operator !== "=") return;
|
|
4454
4577
|
const left = Extract.unwrap(node.left);
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
const leftObj = Extract.unwrap(left.object);
|
|
4458
|
-
const propName = Extract.getPropertyName(left.property);
|
|
4459
|
-
if (leftObj.type === AST_NODE_TYPES.Identifier && propName != null && isFunctionExpressionLike(right)) functionVarBindings.set(`${leftObj.name}.${propName}`, right);
|
|
4460
|
-
return;
|
|
4461
|
-
}
|
|
4462
|
-
if (left.type !== AST_NODE_TYPES.Identifier) return;
|
|
4463
|
-
if (right.type === AST_NODE_TYPES.Identifier) {
|
|
4464
|
-
aliases.set(left.name, right.name);
|
|
4578
|
+
if (left.type === AST_NODE_TYPES.Identifier) {
|
|
4579
|
+
addIdentifierBinding(left, bindingValueFrom(node.right), node.range[0]);
|
|
4465
4580
|
return;
|
|
4466
4581
|
}
|
|
4467
|
-
if (
|
|
4582
|
+
if (left.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
4583
|
+
const object = Extract.unwrap(left.object);
|
|
4584
|
+
const property = Extract.getPropertyName(left.property);
|
|
4585
|
+
if (object.type === AST_NODE_TYPES.Identifier && property != null) addMemberBinding(object, property, bindingValueFrom(node.right), node.range[0]);
|
|
4468
4586
|
},
|
|
4469
4587
|
CallExpression(node) {
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4588
|
+
calls.push(node);
|
|
4589
|
+
},
|
|
4590
|
+
FunctionDeclaration(node) {
|
|
4591
|
+
if (node.id != null) addIdentifierBinding(node.id, {
|
|
4592
|
+
kind: "function",
|
|
4474
4593
|
node
|
|
4475
|
-
});
|
|
4476
|
-
else if (callee.type === AST_NODE_TYPES.MemberExpression) {
|
|
4477
|
-
const calleeObj = Extract.unwrap(callee.object);
|
|
4478
|
-
if (calleeObj.type === AST_NODE_TYPES.Identifier && calleeName != null) directCallSites.push({
|
|
4479
|
-
calleeName: `${calleeObj.name}.${calleeName}`,
|
|
4480
|
-
node
|
|
4481
|
-
});
|
|
4482
|
-
}
|
|
4483
|
-
if (calleeName === "useState") {
|
|
4484
|
-
const [firstArg] = node.arguments;
|
|
4485
|
-
if (firstArg != null) {
|
|
4486
|
-
const unwrappedFirstArg = Extract.unwrap(firstArg);
|
|
4487
|
-
if (isFunctionExpressionLike(unwrappedFirstArg)) stateInitializerFunctions.add(unwrappedFirstArg);
|
|
4488
|
-
}
|
|
4489
|
-
}
|
|
4490
|
-
if (calleeName != null && (calleeName.startsWith("use") || calleeName === "mergeRefs" || calleeName === "render")) return;
|
|
4491
|
-
for (const arg of node.arguments) {
|
|
4492
|
-
const unwrapped = Extract.unwrap(arg);
|
|
4493
|
-
if (unwrapped.type !== AST_NODE_TYPES.Identifier) continue;
|
|
4494
|
-
const resolvedName = resolveAlias(unwrapped.name, aliases);
|
|
4495
|
-
if (resolvedName === "ref" || resolvedName.endsWith("Ref") || jsxRefIdentifiers.has(resolvedName) || isInitializedFromRef$1(context, resolvedName, context.sourceCode.getScope(unwrapped))) refPassedToFunctions.push({
|
|
4496
|
-
callNode: node,
|
|
4497
|
-
node: unwrapped
|
|
4498
|
-
});
|
|
4499
|
-
}
|
|
4594
|
+
}, -1);
|
|
4500
4595
|
},
|
|
4501
4596
|
JSXAttribute(node) {
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
}
|
|
4508
|
-
}
|
|
4597
|
+
if (node.name.type !== AST_NODE_TYPES.JSXIdentifier || node.name.name !== "ref" || node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
|
|
4598
|
+
const expression = Extract.unwrap(node.value.expression);
|
|
4599
|
+
if (expression.type !== AST_NODE_TYPES.Identifier) return;
|
|
4600
|
+
const variable = getVariable(expression);
|
|
4601
|
+
if (variable != null) jsxRefs.add(variable);
|
|
4509
4602
|
},
|
|
4510
4603
|
MemberExpression(node) {
|
|
4511
4604
|
if (Extract.getPropertyName(node.property) !== "current") return;
|
|
4512
|
-
refAccesses.push(
|
|
4513
|
-
isWrite: (() => {
|
|
4514
|
-
let parent = node.parent;
|
|
4515
|
-
while (Check.isTypeExpression(parent)) parent = parent.parent;
|
|
4516
|
-
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);
|
|
4517
|
-
})(),
|
|
4518
|
-
node
|
|
4519
|
-
});
|
|
4605
|
+
refAccesses.push(getRefAccess(node));
|
|
4520
4606
|
},
|
|
4521
4607
|
"Program:exit"(program) {
|
|
4522
|
-
const
|
|
4523
|
-
const
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
const
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
case jsxRefIdentifiers.has(resolvedName):
|
|
4544
|
-
case isInitializedFromRef$1(context, resolvedName, context.sourceCode.getScope(node.object)):
|
|
4545
|
-
refName = resolvedName;
|
|
4546
|
-
break;
|
|
4547
|
-
default: continue;
|
|
4608
|
+
const boundaries = /* @__PURE__ */ new Set([...fc.api.getAllComponents(program).map((component) => component.node), ...hc.api.getAllHooks(program).map((hook) => hook.node)]);
|
|
4609
|
+
const isBoundary = (node) => {
|
|
4610
|
+
return Check.isFunction(node) && boundaries.has(node);
|
|
4611
|
+
};
|
|
4612
|
+
const reachedByBoundary = /* @__PURE__ */ new Map();
|
|
4613
|
+
for (const boundary of boundaries) reachedByBoundary.set(boundary, /* @__PURE__ */ new Set([boundary]));
|
|
4614
|
+
let changed = true;
|
|
4615
|
+
while (changed) {
|
|
4616
|
+
changed = false;
|
|
4617
|
+
for (const call of calls) {
|
|
4618
|
+
const boundary = Traverse.findParent(call, isBoundary);
|
|
4619
|
+
if (boundary == null) continue;
|
|
4620
|
+
const reached = reachedByBoundary.get(boundary);
|
|
4621
|
+
if (reached == null) continue;
|
|
4622
|
+
const host = Traverse.findParent(call, Check.isFunction) ?? boundary;
|
|
4623
|
+
if (!reached.has(host)) continue;
|
|
4624
|
+
const targets = [resolveCallable(call.callee, call.range[0])];
|
|
4625
|
+
for (const index of getSynchronousCallbackIndexes(call)) {
|
|
4626
|
+
const argument = call.arguments[index];
|
|
4627
|
+
if (argument == null || argument.type === AST_NODE_TYPES.SpreadElement) continue;
|
|
4628
|
+
targets.push(resolveCallable(argument, call.range[0]));
|
|
4548
4629
|
}
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
const boundary = Traverse.findParent(node, isCompOrHookFn);
|
|
4554
|
-
if (boundary == null) continue;
|
|
4555
|
-
if (!isReachedDuringRender(node, boundary, getReached(boundary), stateInitializerFunctions)) continue;
|
|
4556
|
-
let isLazyInit = refName != null && isInNullCheckTest(node);
|
|
4557
|
-
let matchedGuardIf = false;
|
|
4558
|
-
if (!isLazyInit && refName != null) {
|
|
4559
|
-
let current = node.parent;
|
|
4560
|
-
let prevChild = node;
|
|
4561
|
-
findLoop: while (true) {
|
|
4562
|
-
if (current.type === AST_NODE_TYPES.IfStatement) {
|
|
4563
|
-
const nullBranch = getRefCurrentNullCheckBranch(current.test, refName);
|
|
4564
|
-
const actualBranch = current.consequent === prevChild ? "consequent" : current.alternate === prevChild ? "alternate" : null;
|
|
4565
|
-
if (nullBranch != null && actualBranch != null) {
|
|
4566
|
-
matchedGuardIf = true;
|
|
4567
|
-
isLazyInit = actualBranch === nullBranch ? isWrite : !isWrite;
|
|
4568
|
-
}
|
|
4569
|
-
break;
|
|
4570
|
-
}
|
|
4571
|
-
switch (current.type) {
|
|
4572
|
-
case AST_NODE_TYPES.ExpressionStatement:
|
|
4573
|
-
case AST_NODE_TYPES.BlockStatement:
|
|
4574
|
-
case AST_NODE_TYPES.ReturnStatement:
|
|
4575
|
-
case AST_NODE_TYPES.JSXExpressionContainer:
|
|
4576
|
-
case AST_NODE_TYPES.JSXElement:
|
|
4577
|
-
case AST_NODE_TYPES.JSXOpeningElement:
|
|
4578
|
-
case AST_NODE_TYPES.JSXClosingElement:
|
|
4579
|
-
case AST_NODE_TYPES.AssignmentExpression:
|
|
4580
|
-
case AST_NODE_TYPES.VariableDeclaration:
|
|
4581
|
-
case AST_NODE_TYPES.VariableDeclarator:
|
|
4582
|
-
case AST_NODE_TYPES.MemberExpression:
|
|
4583
|
-
case AST_NODE_TYPES.ChainExpression:
|
|
4584
|
-
case AST_NODE_TYPES.CallExpression: break;
|
|
4585
|
-
default: break findLoop;
|
|
4586
|
-
}
|
|
4587
|
-
prevChild = current;
|
|
4588
|
-
current = current.parent;
|
|
4630
|
+
for (const target of targets) {
|
|
4631
|
+
if (target == null || reached.has(target) || Traverse.findParent(target, isBoundary) !== boundary) continue;
|
|
4632
|
+
reached.add(target);
|
|
4633
|
+
changed = true;
|
|
4589
4634
|
}
|
|
4590
4635
|
}
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4636
|
+
}
|
|
4637
|
+
function isReachedDuringRender(node, boundary) {
|
|
4638
|
+
const reached = reachedByBoundary.get(boundary);
|
|
4639
|
+
return reached != null && isReachedThroughFunctions(node, boundary, reached);
|
|
4640
|
+
}
|
|
4641
|
+
const initializedRefs = /* @__PURE__ */ new Map();
|
|
4642
|
+
for (const access of refAccesses.toSorted((a, b) => a.node.range[0] - b.node.range[0])) {
|
|
4643
|
+
const target = getRefTarget(access.node);
|
|
4644
|
+
if (target == null) continue;
|
|
4645
|
+
const boundary = Traverse.findParent(access.node, isBoundary);
|
|
4646
|
+
if (boundary == null || !isReachedDuringRender(access.node, boundary)) continue;
|
|
4647
|
+
let isLazyInitialization = false;
|
|
4648
|
+
if (target.identity != null) {
|
|
4649
|
+
const identity = target.identity;
|
|
4650
|
+
const getTargetNullBranch = (test) => getNullBranch(test, identity);
|
|
4651
|
+
if (isGuardTestAccess(access.node, getTargetNullBranch)) continue;
|
|
4652
|
+
const guard = getGuardDisposition(access.node, getTargetNullBranch);
|
|
4653
|
+
if (guard != null) {
|
|
4654
|
+
if (guard.inNullBranch && access.isInitializationWrite) isLazyInitialization = true;
|
|
4655
|
+
else if (!guard.inNullBranch && !access.isWrite) continue;
|
|
4656
|
+
} else if (access.isInitializationWrite && isAfterTerminatingNonNullGuard(access.node, getTargetNullBranch)) isLazyInitialization = true;
|
|
4606
4657
|
}
|
|
4607
|
-
if (
|
|
4608
|
-
const
|
|
4609
|
-
|
|
4610
|
-
if (
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
}
|
|
4617
|
-
seen.add(refName);
|
|
4658
|
+
if (isLazyInitialization && target.identity != null) {
|
|
4659
|
+
const initialized = initializedRefs.get(boundary) ?? /* @__PURE__ */ new Set();
|
|
4660
|
+
initializedRefs.set(boundary, initialized);
|
|
4661
|
+
if (initialized.has(target.identity)) context.report({
|
|
4662
|
+
messageId: "duplicateRefInit",
|
|
4663
|
+
node: access.node
|
|
4664
|
+
});
|
|
4665
|
+
else initialized.add(target.identity);
|
|
4666
|
+
continue;
|
|
4618
4667
|
}
|
|
4619
|
-
if (isLazyInit) continue;
|
|
4620
4668
|
context.report({
|
|
4621
|
-
messageId: isWrite ? "writeDuringRender" : "readDuringRender",
|
|
4622
|
-
node
|
|
4669
|
+
messageId: access.isWrite ? "writeDuringRender" : "readDuringRender",
|
|
4670
|
+
node: access.node
|
|
4623
4671
|
});
|
|
4624
4672
|
}
|
|
4625
|
-
for (const
|
|
4626
|
-
const boundary = Traverse.findParent(
|
|
4627
|
-
if (boundary == null) continue;
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4673
|
+
for (const call of calls) {
|
|
4674
|
+
const boundary = Traverse.findParent(call, isBoundary);
|
|
4675
|
+
if (boundary == null || !isReachedDuringRender(call, boundary)) continue;
|
|
4676
|
+
const callee = Extract.unwrap(call.callee);
|
|
4677
|
+
const calleeName = getCalleeName(call);
|
|
4678
|
+
const callArguments = call.arguments;
|
|
4679
|
+
if (core.isHookCall(call) || calleeName === "mergeRefs" || calleeName === "render" && callee.type === AST_NODE_TYPES.MemberExpression) continue;
|
|
4680
|
+
for (const argument of callArguments) {
|
|
4681
|
+
if (argument.type === AST_NODE_TYPES.SpreadElement) continue;
|
|
4682
|
+
const value = Extract.unwrap(argument);
|
|
4683
|
+
if (value.type !== AST_NODE_TYPES.Identifier) continue;
|
|
4684
|
+
const variable = getVariable(value);
|
|
4685
|
+
if (variable == null || resolveRef(variable, value.range[0]) == null) continue;
|
|
4686
|
+
context.report({
|
|
4687
|
+
messageId: "refPassedToFunction",
|
|
4688
|
+
node: value
|
|
4689
|
+
});
|
|
4690
|
+
}
|
|
4633
4691
|
}
|
|
4634
4692
|
},
|
|
4635
4693
|
VariableDeclarator(node) {
|
|
4636
|
-
if (node.init == null) return;
|
|
4637
|
-
|
|
4638
|
-
if (id.type !== AST_NODE_TYPES.Identifier) return;
|
|
4639
|
-
const init = Extract.unwrap(node.init);
|
|
4640
|
-
if (init.type === AST_NODE_TYPES.Identifier) {
|
|
4641
|
-
aliases.set(id.name, init.name);
|
|
4642
|
-
return;
|
|
4643
|
-
}
|
|
4644
|
-
if (isFunctionExpressionLike(init)) functionVarBindings.set(id.name, init);
|
|
4694
|
+
if (node.id.type !== AST_NODE_TYPES.Identifier || node.init == null) return;
|
|
4695
|
+
addIdentifierBinding(node.id, bindingValueFrom(node.init), node.range[0]);
|
|
4645
4696
|
}
|
|
4646
4697
|
});
|
|
4647
4698
|
}
|
|
@@ -7496,9 +7547,9 @@ function create$2(context) {
|
|
|
7496
7547
|
});
|
|
7497
7548
|
},
|
|
7498
7549
|
"Program:exit"(node) {
|
|
7499
|
-
const
|
|
7550
|
+
const comps = fc.api.getAllComponents(node);
|
|
7500
7551
|
const hooks = hc.api.getAllHooks(node);
|
|
7501
|
-
const funcs = [...
|
|
7552
|
+
const funcs = [...comps, ...hooks];
|
|
7502
7553
|
for (const { func, node } of evalCalls) {
|
|
7503
7554
|
if (!funcs.some((f) => f.node === func)) continue;
|
|
7504
7555
|
context.report({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-x",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.14.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/
|
|
52
|
-
"@eslint-react/shared": "5.
|
|
53
|
-
"@eslint-react/var": "5.
|
|
48
|
+
"@eslint-react/ast": "5.14.1",
|
|
49
|
+
"@eslint-react/jsx": "5.14.1",
|
|
50
|
+
"@eslint-react/core": "5.14.1",
|
|
51
|
+
"@eslint-react/eslint": "5.14.1",
|
|
52
|
+
"@eslint-react/shared": "5.14.1",
|
|
53
|
+
"@eslint-react/var": "5.14.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/react": "^19.2.17",
|