oxlint-plugin-react-doctor 0.2.17-dev.5614b32 → 0.2.18-dev.08e1d55
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.d.ts +659 -2
- package/dist/index.js +262 -88
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2574,7 +2574,7 @@ const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
|
|
|
2574
2574
|
* (`FUNCTION_LIKE_TYPES.has(node.type)`) and as a type-guard. The
|
|
2575
2575
|
* type-guard form covers both shapes without callers paying a cast.
|
|
2576
2576
|
*/
|
|
2577
|
-
const isFunctionLike$
|
|
2577
|
+
const isFunctionLike$3 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
|
|
2578
2578
|
//#endregion
|
|
2579
2579
|
//#region src/plugin/utils/is-inline-function-expression.ts
|
|
2580
2580
|
/**
|
|
@@ -2597,7 +2597,7 @@ const findFirstAwaitOutsideNestedFunctions = (block) => {
|
|
|
2597
2597
|
let firstAwait = null;
|
|
2598
2598
|
walkAst(block, (child) => {
|
|
2599
2599
|
if (firstAwait) return false;
|
|
2600
|
-
if (child !== block && isFunctionLike$
|
|
2600
|
+
if (child !== block && isFunctionLike$3(child)) return false;
|
|
2601
2601
|
if (isNodeOfType(child, "AwaitExpression")) firstAwait = child;
|
|
2602
2602
|
});
|
|
2603
2603
|
return firstAwait;
|
|
@@ -3054,13 +3054,13 @@ const asyncDeferAwait = defineRule({
|
|
|
3054
3054
|
const inspectAllStatementBlocks = (functionBody) => {
|
|
3055
3055
|
if (!functionBody) return;
|
|
3056
3056
|
walkAst(functionBody, (descendant) => {
|
|
3057
|
-
if (isFunctionLike$
|
|
3057
|
+
if (isFunctionLike$3(descendant)) return false;
|
|
3058
3058
|
if (isNodeOfType(descendant, "BlockStatement")) inspectStatements(descendant.body ?? []);
|
|
3059
3059
|
else if (isNodeOfType(descendant, "SwitchCase")) inspectStatements(descendant.consequent ?? []);
|
|
3060
3060
|
});
|
|
3061
3061
|
};
|
|
3062
3062
|
const enterFunction = (node) => {
|
|
3063
|
-
if (!isFunctionLike$
|
|
3063
|
+
if (!isFunctionLike$3(node)) return;
|
|
3064
3064
|
if (!node.async) return;
|
|
3065
3065
|
if (!isNodeOfType(node.body, "BlockStatement")) return;
|
|
3066
3066
|
inspectAllStatementBlocks(node.body);
|
|
@@ -4509,7 +4509,7 @@ const displayName = defineRule({
|
|
|
4509
4509
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
4510
4510
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
4511
4511
|
if (!node || typeof node !== "object") return;
|
|
4512
|
-
if (isFunctionLike$
|
|
4512
|
+
if (isFunctionLike$3(node)) return;
|
|
4513
4513
|
visitor(node);
|
|
4514
4514
|
const nodeRecord = node;
|
|
4515
4515
|
for (const key of Object.keys(nodeRecord)) {
|
|
@@ -4597,7 +4597,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4597
4597
|
let didFindRelease = false;
|
|
4598
4598
|
walkAst(node, (child) => {
|
|
4599
4599
|
if (didFindRelease) return false;
|
|
4600
|
-
if (child !== node && isFunctionLike$
|
|
4600
|
+
if (child !== node && isFunctionLike$3(child) && !isIteratorCallbackArgument(child)) return false;
|
|
4601
4601
|
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
4602
4602
|
didFindRelease = true;
|
|
4603
4603
|
return false;
|
|
@@ -4606,14 +4606,20 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4606
4606
|
return didFindRelease;
|
|
4607
4607
|
};
|
|
4608
4608
|
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
4609
|
-
if (!isFunctionLike$
|
|
4609
|
+
if (!isFunctionLike$3(node)) return false;
|
|
4610
4610
|
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
4611
4611
|
};
|
|
4612
|
-
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
4612
|
+
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
4613
4613
|
if (!returnedValue) return false;
|
|
4614
4614
|
const unwrappedValue = unwrapChainExpression$2(returnedValue);
|
|
4615
|
-
if (isNodeOfType(unwrappedValue, "
|
|
4615
|
+
if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
|
|
4616
|
+
if (isNodeOfType(unwrappedValue, "Identifier")) {
|
|
4617
|
+
if (unwrappedValue.name === "undefined") return false;
|
|
4618
|
+
if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
|
|
4619
|
+
return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
|
|
4620
|
+
}
|
|
4616
4621
|
if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
4622
|
+
if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
4617
4623
|
if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
|
|
4618
4624
|
return false;
|
|
4619
4625
|
};
|
|
@@ -4634,12 +4640,14 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
4634
4640
|
if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
|
|
4635
4641
|
usages.push({
|
|
4636
4642
|
kind: "timer",
|
|
4643
|
+
node: child,
|
|
4637
4644
|
resourceName: child.callee.name
|
|
4638
4645
|
});
|
|
4639
4646
|
return;
|
|
4640
4647
|
}
|
|
4641
4648
|
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
|
|
4642
4649
|
kind: "subscribe",
|
|
4650
|
+
node: child,
|
|
4643
4651
|
resourceName: child.callee.property.name
|
|
4644
4652
|
});
|
|
4645
4653
|
});
|
|
@@ -4686,7 +4694,20 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
4686
4694
|
});
|
|
4687
4695
|
return bindings;
|
|
4688
4696
|
};
|
|
4689
|
-
const
|
|
4697
|
+
const getRangeStart = (node) => {
|
|
4698
|
+
const rangeStart = node.range?.[0];
|
|
4699
|
+
return typeof rangeStart === "number" ? rangeStart : null;
|
|
4700
|
+
};
|
|
4701
|
+
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
4702
|
+
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
4703
|
+
const returnStart = getRangeStart(returnStatement);
|
|
4704
|
+
if (returnStart === null) return true;
|
|
4705
|
+
return usages.some((usage) => {
|
|
4706
|
+
const usageStart = getRangeStart(usage.node);
|
|
4707
|
+
return usageStart === null || usageStart < returnStart;
|
|
4708
|
+
});
|
|
4709
|
+
};
|
|
4710
|
+
const effectHasCleanupReturn = (callback, usages) => {
|
|
4690
4711
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
4691
4712
|
if (!isNodeOfType(callback.body, "BlockStatement")) return isCleanupReturningSubscribeLikeCallExpression(callback.body);
|
|
4692
4713
|
const cleanupBindings = collectCleanupBindings(callback);
|
|
@@ -4694,7 +4715,8 @@ const effectHasCleanupRelease = (callback) => {
|
|
|
4694
4715
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
4695
4716
|
if (didFindCleanupReturn) return;
|
|
4696
4717
|
if (!isNodeOfType(child, "ReturnStatement")) return;
|
|
4697
|
-
if (
|
|
4718
|
+
if (!cleanupReturnRunsAfterUsage(child, usages)) return;
|
|
4719
|
+
if (isCleanupReturn(child.argument, cleanupBindings.cleanupFunctionNames, cleanupBindings.subscriptionNames, { allowOpaqueReturn: true })) didFindCleanupReturn = true;
|
|
4698
4720
|
});
|
|
4699
4721
|
return didFindCleanupReturn;
|
|
4700
4722
|
};
|
|
@@ -4710,7 +4732,7 @@ const effectNeedsCleanup = defineRule({
|
|
|
4710
4732
|
if (!callback) return;
|
|
4711
4733
|
const usages = findSubscribeLikeUsages(callback);
|
|
4712
4734
|
if (usages.length === 0) return;
|
|
4713
|
-
if (
|
|
4735
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
4714
4736
|
const firstUsage = usages[0];
|
|
4715
4737
|
const verb = firstUsage.kind === "timer" ? "schedules" : "subscribes via";
|
|
4716
4738
|
context.report({
|
|
@@ -4848,7 +4870,7 @@ const recordReference = (state, identifier, flag) => {
|
|
|
4848
4870
|
};
|
|
4849
4871
|
const isFunctionBodyBlock = (block) => {
|
|
4850
4872
|
if (!block.parent) return false;
|
|
4851
|
-
return isFunctionLike$
|
|
4873
|
+
return isFunctionLike$3(block.parent);
|
|
4852
4874
|
};
|
|
4853
4875
|
const isCatchClauseBlock = (block) => block.parent !== null && block.parent !== void 0 && block.parent.type === "CatchClause";
|
|
4854
4876
|
const handleVariableDeclaration = (declaration, state) => {
|
|
@@ -5003,7 +5025,7 @@ const walkParameterReferences = (pattern, state) => {
|
|
|
5003
5025
|
if (isNodeOfType(pattern, "RestElement")) walkParameterReferences(pattern.argument, state);
|
|
5004
5026
|
};
|
|
5005
5027
|
const walk = (node, state) => {
|
|
5006
|
-
if (isFunctionLike$
|
|
5028
|
+
if (isFunctionLike$3(node)) {
|
|
5007
5029
|
if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
|
|
5008
5030
|
setNodeScope(node, state);
|
|
5009
5031
|
const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
|
|
@@ -5249,7 +5271,7 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
5249
5271
|
const out = [];
|
|
5250
5272
|
const seen = /* @__PURE__ */ new Set();
|
|
5251
5273
|
const visit = (node) => {
|
|
5252
|
-
if (node !== functionNode && isFunctionLike$
|
|
5274
|
+
if (node !== functionNode && isFunctionLike$3(node)) {
|
|
5253
5275
|
const innerCaptures = closureCaptures(node, scopes);
|
|
5254
5276
|
for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
|
|
5255
5277
|
if (!seen.has(reference.id)) {
|
|
@@ -7346,6 +7368,13 @@ const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) =>
|
|
|
7346
7368
|
if (!info) return false;
|
|
7347
7369
|
return info.source === moduleSource;
|
|
7348
7370
|
};
|
|
7371
|
+
const isNamespaceImportFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
7372
|
+
const lookup = getImportLookup(contextNode);
|
|
7373
|
+
if (!lookup) return false;
|
|
7374
|
+
const info = lookup.get(localIdentifierName);
|
|
7375
|
+
if (!info) return false;
|
|
7376
|
+
return info.isNamespace && info.source === moduleSource;
|
|
7377
|
+
};
|
|
7349
7378
|
const getImportedNameFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
7350
7379
|
const lookup = getImportLookup(contextNode);
|
|
7351
7380
|
if (!lookup) return null;
|
|
@@ -7362,7 +7391,7 @@ const isAtomFromJotai = (callExpression) => {
|
|
|
7362
7391
|
if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
|
|
7363
7392
|
return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
|
|
7364
7393
|
};
|
|
7365
|
-
const isFunctionLike$
|
|
7394
|
+
const isFunctionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
7366
7395
|
const getFirstParameterName = (fn) => {
|
|
7367
7396
|
const parameters = fn.params ?? [];
|
|
7368
7397
|
if (parameters.length !== 1) return null;
|
|
@@ -7481,7 +7510,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
|
7481
7510
|
const args = node.arguments ?? [];
|
|
7482
7511
|
if (args.length === 0) return;
|
|
7483
7512
|
const reader = args[0];
|
|
7484
|
-
if (!isFunctionLike$
|
|
7513
|
+
if (!isFunctionLike$2(reader)) return;
|
|
7485
7514
|
const getParameterName = getFirstParameterName(reader);
|
|
7486
7515
|
if (!getParameterName) return;
|
|
7487
7516
|
const freshReturn = getFreshReturnForFunction(reader);
|
|
@@ -7737,7 +7766,7 @@ const isInsideLoopContext = (node) => {
|
|
|
7737
7766
|
let current = node.parent;
|
|
7738
7767
|
while (current) {
|
|
7739
7768
|
if (isNodeOfType(current, "ForStatement") || isNodeOfType(current, "ForInStatement") || isNodeOfType(current, "ForOfStatement") || isNodeOfType(current, "WhileStatement") || isNodeOfType(current, "DoWhileStatement")) return true;
|
|
7740
|
-
if (isFunctionLike$
|
|
7769
|
+
if (isFunctionLike$3(current)) {
|
|
7741
7770
|
if (isIteratorCallback(current)) return true;
|
|
7742
7771
|
return false;
|
|
7743
7772
|
}
|
|
@@ -8098,7 +8127,7 @@ const jsHoistIntl = defineRule({
|
|
|
8098
8127
|
let cursor = node.parent ?? null;
|
|
8099
8128
|
let inFunctionBody = false;
|
|
8100
8129
|
while (cursor) {
|
|
8101
|
-
if (isFunctionLike$
|
|
8130
|
+
if (isFunctionLike$3(cursor)) {
|
|
8102
8131
|
inFunctionBody = true;
|
|
8103
8132
|
const fnParent = cursor.parent;
|
|
8104
8133
|
if (fnParent && isNodeOfType(fnParent, "CallExpression") && fnParent.arguments?.[0] === cursor) {
|
|
@@ -9696,6 +9725,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
9696
9725
|
title: "Unstable context provider value",
|
|
9697
9726
|
tags: ["react-jsx-only"],
|
|
9698
9727
|
severity: "warn",
|
|
9728
|
+
disabledBy: ["react-compiler"],
|
|
9699
9729
|
recommendation: "Wrap the context value in `useMemo`, or move it outside the component.",
|
|
9700
9730
|
category: "Performance",
|
|
9701
9731
|
create: (context) => {
|
|
@@ -13052,7 +13082,7 @@ const collectChainedGetHandlerBodies = (initNode) => {
|
|
|
13052
13082
|
};
|
|
13053
13083
|
const resolveBodiesFromExpression = (expression, resolveBinding, remainingDepth) => {
|
|
13054
13084
|
if (remainingDepth <= 0) return [];
|
|
13055
|
-
if (isFunctionLike$
|
|
13085
|
+
if (isFunctionLike$3(expression)) return expression.body ? [expression.body] : [];
|
|
13056
13086
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
13057
13087
|
for (const callArgument of expression.arguments ?? []) {
|
|
13058
13088
|
if (isNodeOfType(callArgument, "ArrowFunctionExpression") || isNodeOfType(callArgument, "FunctionExpression")) {
|
|
@@ -13522,7 +13552,7 @@ const getEffectFn = (analysis, node) => {
|
|
|
13522
13552
|
if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
|
|
13523
13553
|
if (isNodeOfType(fn, "Identifier")) {
|
|
13524
13554
|
const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
|
|
13525
|
-
if (definitionNode && isFunctionLike$
|
|
13555
|
+
if (definitionNode && isFunctionLike$3(definitionNode)) return definitionNode;
|
|
13526
13556
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
13527
13557
|
const initializer = definitionNode.init;
|
|
13528
13558
|
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
|
|
@@ -13615,14 +13645,14 @@ const getUseStateDecl = (analysis, ref) => {
|
|
|
13615
13645
|
return node ?? null;
|
|
13616
13646
|
};
|
|
13617
13647
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
13618
|
-
if (isFunctionLike$
|
|
13648
|
+
if (isFunctionLike$3(node)) return true;
|
|
13619
13649
|
if (isNodeOfType(node, "MemberExpression")) return true;
|
|
13620
13650
|
if (isNodeOfType(node, "Identifier")) {
|
|
13621
13651
|
const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
|
|
13622
|
-
if (definitionNode && isFunctionLike$
|
|
13652
|
+
if (definitionNode && isFunctionLike$3(definitionNode)) return true;
|
|
13623
13653
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
13624
13654
|
const initializer = definitionNode.init;
|
|
13625
|
-
return isFunctionLike$
|
|
13655
|
+
return isFunctionLike$3(initializer);
|
|
13626
13656
|
}
|
|
13627
13657
|
}
|
|
13628
13658
|
if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
|
|
@@ -13632,7 +13662,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
13632
13662
|
if (visited.has(node)) return false;
|
|
13633
13663
|
visited.add(node);
|
|
13634
13664
|
if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
|
|
13635
|
-
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$
|
|
13665
|
+
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$3(node)) return false;
|
|
13636
13666
|
const record = node;
|
|
13637
13667
|
for (const [key, value] of Object.entries(record)) {
|
|
13638
13668
|
if (key === "parent") continue;
|
|
@@ -13644,7 +13674,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
13644
13674
|
};
|
|
13645
13675
|
const hasCleanup = (analysis, node) => {
|
|
13646
13676
|
const fn = getEffectFn(analysis, node);
|
|
13647
|
-
if (!isFunctionLike$
|
|
13677
|
+
if (!isFunctionLike$3(fn)) return false;
|
|
13648
13678
|
if (!isNodeOfType(fn.body, "BlockStatement")) return false;
|
|
13649
13679
|
return hasCleanupReturn(analysis, fn.body);
|
|
13650
13680
|
};
|
|
@@ -13994,7 +14024,7 @@ const isInsideStaticPlaceholderMap = (node) => {
|
|
|
13994
14024
|
let current = node;
|
|
13995
14025
|
while (current.parent) {
|
|
13996
14026
|
const parent = current.parent;
|
|
13997
|
-
if (isFunctionLike$
|
|
14027
|
+
if (isFunctionLike$3(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
13998
14028
|
const callee = parent.callee;
|
|
13999
14029
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach")) return isStaticPlaceholderReceiver(callee.object);
|
|
14000
14030
|
if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
|
|
@@ -14013,7 +14043,7 @@ const findIteratorItemName$1 = (node) => {
|
|
|
14013
14043
|
let current = node;
|
|
14014
14044
|
while (current.parent) {
|
|
14015
14045
|
const parent = current.parent;
|
|
14016
|
-
if (isFunctionLike$
|
|
14046
|
+
if (isFunctionLike$3(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
14017
14047
|
const callee = parent.callee;
|
|
14018
14048
|
const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
|
|
14019
14049
|
const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
|
|
@@ -14958,7 +14988,7 @@ const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
|
14958
14988
|
const nearestEnclosingFunction = (node) => {
|
|
14959
14989
|
let cursor = node.parent;
|
|
14960
14990
|
while (cursor) {
|
|
14961
|
-
if (isFunctionLike$
|
|
14991
|
+
if (isFunctionLike$3(cursor)) return cursor;
|
|
14962
14992
|
cursor = cursor.parent ?? null;
|
|
14963
14993
|
}
|
|
14964
14994
|
return null;
|
|
@@ -15641,7 +15671,7 @@ const extractDestructuredPropNames = (params) => {
|
|
|
15641
15671
|
};
|
|
15642
15672
|
const getInlineFunctionNode = (node) => {
|
|
15643
15673
|
if (!node) return null;
|
|
15644
|
-
if (isFunctionLike$
|
|
15674
|
+
if (isFunctionLike$3(node)) return node;
|
|
15645
15675
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
15646
15676
|
for (const argument of node.arguments ?? []) {
|
|
15647
15677
|
const inlineFunctionNode = getInlineFunctionNode(argument);
|
|
@@ -15652,7 +15682,7 @@ const getInlineFunctionNode = (node) => {
|
|
|
15652
15682
|
const getNearestComponentFunction = (node) => {
|
|
15653
15683
|
let cursor = node.parent ?? null;
|
|
15654
15684
|
while (cursor) {
|
|
15655
|
-
if (isFunctionLike$
|
|
15685
|
+
if (isFunctionLike$3(cursor)) return cursor;
|
|
15656
15686
|
cursor = cursor.parent ?? null;
|
|
15657
15687
|
}
|
|
15658
15688
|
return null;
|
|
@@ -15982,7 +16012,7 @@ const collectFunctionLocalBindings = (functionNode) => {
|
|
|
15982
16012
|
const walkComponentRespectingShadows = (node, shadowedStateNames, visit) => {
|
|
15983
16013
|
if (!node || typeof node !== "object") return;
|
|
15984
16014
|
let nextShadowedStateNames = shadowedStateNames;
|
|
15985
|
-
if (isFunctionLike$
|
|
16015
|
+
if (isFunctionLike$3(node)) {
|
|
15986
16016
|
const localBindings = collectFunctionLocalBindings(node);
|
|
15987
16017
|
if (localBindings.size > 0) {
|
|
15988
16018
|
const merged = new Set(shadowedStateNames);
|
|
@@ -18239,7 +18269,7 @@ const isInsideClassBody = (node) => {
|
|
|
18239
18269
|
let current = node.parent;
|
|
18240
18270
|
while (current) {
|
|
18241
18271
|
if (isNodeOfType(current, "ClassBody")) return true;
|
|
18242
|
-
if (isFunctionLike$
|
|
18272
|
+
if (isFunctionLike$3(current)) return false;
|
|
18243
18273
|
current = current.parent;
|
|
18244
18274
|
}
|
|
18245
18275
|
return false;
|
|
@@ -18943,7 +18973,7 @@ const isLodashMutatorCall = (callExpression) => {
|
|
|
18943
18973
|
};
|
|
18944
18974
|
//#endregion
|
|
18945
18975
|
//#region src/plugin/utils/find-exported-function-body.ts
|
|
18946
|
-
const isFunctionLike = (node) => {
|
|
18976
|
+
const isFunctionLike$1 = (node) => {
|
|
18947
18977
|
if (!node) return false;
|
|
18948
18978
|
return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
|
|
18949
18979
|
};
|
|
@@ -18958,7 +18988,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
18958
18988
|
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
18959
18989
|
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
18960
18990
|
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
18961
|
-
if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
|
|
18991
|
+
if (initializer && isFunctionLike$1(initializer)) localBindings.set(declarator.id.name, initializer);
|
|
18962
18992
|
}
|
|
18963
18993
|
};
|
|
18964
18994
|
for (const statement of programRoot.body ?? []) {
|
|
@@ -19002,7 +19032,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
19002
19032
|
defaultExport = declaration;
|
|
19003
19033
|
continue;
|
|
19004
19034
|
}
|
|
19005
|
-
if (isFunctionLike(declaration)) {
|
|
19035
|
+
if (isFunctionLike$1(declaration)) {
|
|
19006
19036
|
defaultExport = declaration;
|
|
19007
19037
|
continue;
|
|
19008
19038
|
}
|
|
@@ -19163,7 +19193,7 @@ const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
|
|
|
19163
19193
|
const resolveReducerFunction = (node, currentFilename) => {
|
|
19164
19194
|
if (!node) return null;
|
|
19165
19195
|
const unwrappedNode = stripParenExpression(node);
|
|
19166
|
-
if (isFunctionLike$
|
|
19196
|
+
if (isFunctionLike$3(unwrappedNode)) return {
|
|
19167
19197
|
functionNode: unwrappedNode,
|
|
19168
19198
|
crossFileSourceDisplay: null
|
|
19169
19199
|
};
|
|
@@ -19171,7 +19201,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
19171
19201
|
const initializer = findVariableInitializer(unwrappedNode, unwrappedNode.name)?.initializer;
|
|
19172
19202
|
if (!initializer) return null;
|
|
19173
19203
|
const unwrappedInitializer = stripParenExpression(initializer);
|
|
19174
|
-
if (isFunctionLike$
|
|
19204
|
+
if (isFunctionLike$3(unwrappedInitializer)) return {
|
|
19175
19205
|
functionNode: unwrappedInitializer,
|
|
19176
19206
|
crossFileSourceDisplay: null
|
|
19177
19207
|
};
|
|
@@ -19285,11 +19315,11 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
19285
19315
|
return false;
|
|
19286
19316
|
};
|
|
19287
19317
|
const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
19288
|
-
if (isFunctionLike$
|
|
19318
|
+
if (isFunctionLike$3(node)) return [];
|
|
19289
19319
|
const mutations = [];
|
|
19290
19320
|
walkAst(node, (child) => {
|
|
19291
19321
|
const unwrappedChild = stripParenExpression(child);
|
|
19292
|
-
if (child !== node && isFunctionLike$
|
|
19322
|
+
if (child !== node && isFunctionLike$3(unwrappedChild)) return false;
|
|
19293
19323
|
if (isNodeOfType(unwrappedChild, "AssignmentExpression")) {
|
|
19294
19324
|
if (isNodeOfType(stripParenExpression(unwrappedChild.left), "MemberExpression") && isExpressionRootedInMutableReducerStateSource(unwrappedChild.left, state)) mutations.push({ node: unwrappedChild });
|
|
19295
19325
|
return;
|
|
@@ -19394,7 +19424,7 @@ const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) =>
|
|
|
19394
19424
|
if (isExpressionReachableFromOriginalReducerState(assignment.right, state)) state.mutableStateSourceNames.add(name);
|
|
19395
19425
|
};
|
|
19396
19426
|
const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, reportedNodes, options) => {
|
|
19397
|
-
if (!isFunctionLike$
|
|
19427
|
+
if (!isFunctionLike$3(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return;
|
|
19398
19428
|
const firstParam = functionNode.params?.[0];
|
|
19399
19429
|
const stateName = isNodeOfType(firstParam, "Identifier") ? firstParam.name : isNodeOfType(firstParam, "AssignmentPattern") && isNodeOfType(firstParam.left, "Identifier") ? firstParam.left.name : null;
|
|
19400
19430
|
if (!stateName) return;
|
|
@@ -20172,6 +20202,12 @@ const getReactDoctorStringSetting = (settings, settingName) => {
|
|
|
20172
20202
|
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
20173
20203
|
return typeof settingValue === "string" ? settingValue : void 0;
|
|
20174
20204
|
};
|
|
20205
|
+
const getReactDoctorNumberSetting = (settings, settingName) => {
|
|
20206
|
+
const bag = readReactDoctorSettingsBag(settings);
|
|
20207
|
+
if (!bag) return void 0;
|
|
20208
|
+
const settingValue = readOwnPropertyValue(bag, settingName);
|
|
20209
|
+
return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
|
|
20210
|
+
};
|
|
20175
20211
|
const getReactDoctorStringArraySetting = (settings, settingName) => {
|
|
20176
20212
|
const bag = readReactDoctorSettingsBag(settings);
|
|
20177
20213
|
if (!bag) return [];
|
|
@@ -21237,7 +21273,7 @@ const isTanStackServerFnHandler = (node) => {
|
|
|
21237
21273
|
const isInsideServerOnlyScope = (node) => {
|
|
21238
21274
|
let currentNode = node.parent ?? null;
|
|
21239
21275
|
while (currentNode) {
|
|
21240
|
-
if (isFunctionLike$
|
|
21276
|
+
if (isFunctionLike$3(currentNode)) {
|
|
21241
21277
|
if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
|
|
21242
21278
|
}
|
|
21243
21279
|
currentNode = currentNode.parent ?? null;
|
|
@@ -23392,7 +23428,7 @@ const isReactClassComponent = (classNode) => {
|
|
|
23392
23428
|
const findEnclosingComponent = (node) => {
|
|
23393
23429
|
let walker = node.parent;
|
|
23394
23430
|
while (walker) {
|
|
23395
|
-
if (isFunctionLike$
|
|
23431
|
+
if (isFunctionLike$3(walker)) {
|
|
23396
23432
|
const componentName = inferFunctionLikeName(walker);
|
|
23397
23433
|
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
23398
23434
|
component: walker,
|
|
@@ -23644,6 +23680,35 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
23644
23680
|
});
|
|
23645
23681
|
//#endregion
|
|
23646
23682
|
//#region src/plugin/rules/design/no-wide-letter-spacing.ts
|
|
23683
|
+
const getJsxAttributeStringValue = (attributeValue) => {
|
|
23684
|
+
if (!attributeValue) return null;
|
|
23685
|
+
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return attributeValue.value;
|
|
23686
|
+
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
23687
|
+
const expression = attributeValue.expression;
|
|
23688
|
+
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
|
|
23689
|
+
}
|
|
23690
|
+
return null;
|
|
23691
|
+
};
|
|
23692
|
+
const isTruthyBooleanJsxAttribute = (attributeValue) => {
|
|
23693
|
+
if (attributeValue === null) return true;
|
|
23694
|
+
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
23695
|
+
const expression = attributeValue.expression;
|
|
23696
|
+
return isNodeOfType(expression, "Literal") && expression.value === true;
|
|
23697
|
+
}
|
|
23698
|
+
return false;
|
|
23699
|
+
};
|
|
23700
|
+
const hasUppercaseSiblingProp = (styleAttribute) => {
|
|
23701
|
+
const openingElement = styleAttribute.parent;
|
|
23702
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
23703
|
+
for (const attribute of openingElement.attributes ?? []) {
|
|
23704
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
23705
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
23706
|
+
const attributeName = attribute.name.name;
|
|
23707
|
+
if (attributeName === "uppercase" && isTruthyBooleanJsxAttribute(attribute.value)) return true;
|
|
23708
|
+
if (attributeName === "textTransform" && getJsxAttributeStringValue(attribute.value) === "uppercase") return true;
|
|
23709
|
+
}
|
|
23710
|
+
return false;
|
|
23711
|
+
};
|
|
23647
23712
|
const noWideLetterSpacing = defineRule({
|
|
23648
23713
|
id: "no-wide-letter-spacing",
|
|
23649
23714
|
title: "Wide letter spacing on body text",
|
|
@@ -23653,6 +23718,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
23653
23718
|
create: (context) => ({ JSXAttribute(node) {
|
|
23654
23719
|
const expression = getInlineStyleExpression(node);
|
|
23655
23720
|
if (!expression) return;
|
|
23721
|
+
if (hasUppercaseSiblingProp(node)) return;
|
|
23656
23722
|
let isUppercase = false;
|
|
23657
23723
|
let letterSpacingProperty = null;
|
|
23658
23724
|
let letterSpacingEm = null;
|
|
@@ -24815,6 +24881,7 @@ const preferModuleScopeStaticValue = defineRule({
|
|
|
24815
24881
|
tags: ["test-noise"],
|
|
24816
24882
|
severity: "warn",
|
|
24817
24883
|
category: "Architecture",
|
|
24884
|
+
disabledBy: ["react-compiler"],
|
|
24818
24885
|
recommendation: "Move the value above the component, at the top of the file. It doesn't use local state, so rebuilding it each update is wasted and makes it look new every time.",
|
|
24819
24886
|
create: (context) => ({ VariableDeclarator(node) {
|
|
24820
24887
|
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
@@ -25627,7 +25694,7 @@ const reduxUseselectorInlineDerivation = defineRule({
|
|
|
25627
25694
|
if (!body) return;
|
|
25628
25695
|
const returnedExpressions = [];
|
|
25629
25696
|
if (isNodeOfType(body, "BlockStatement")) walkAst(body, (node) => {
|
|
25630
|
-
if (node !== body && isFunctionLike$
|
|
25697
|
+
if (node !== body && isFunctionLike$3(node)) return false;
|
|
25631
25698
|
if (isNodeOfType(node, "ReturnStatement")) {
|
|
25632
25699
|
if (node.argument) returnedExpressions.push(node.argument);
|
|
25633
25700
|
return false;
|
|
@@ -25970,7 +26037,7 @@ const hasOwnAwait = (functionBody) => {
|
|
|
25970
26037
|
let found = false;
|
|
25971
26038
|
walkAst(functionBody, (child) => {
|
|
25972
26039
|
if (found) return;
|
|
25973
|
-
if (child !== functionBody && isFunctionLike$
|
|
26040
|
+
if (child !== functionBody && isFunctionLike$3(child)) return false;
|
|
25974
26041
|
if (isNodeOfType(child, "AwaitExpression")) found = true;
|
|
25975
26042
|
});
|
|
25976
26043
|
return found;
|
|
@@ -25989,7 +26056,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
|
25989
26056
|
let found = false;
|
|
25990
26057
|
walkAst(componentBody, (child) => {
|
|
25991
26058
|
if (found) return;
|
|
25992
|
-
if (!isFunctionLike$
|
|
26059
|
+
if (!isFunctionLike$3(child)) return;
|
|
25993
26060
|
const functionBody = child.body;
|
|
25994
26061
|
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
25995
26062
|
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
@@ -26772,7 +26839,8 @@ const rerenderTransitionsScroll = defineRule({
|
|
|
26772
26839
|
});
|
|
26773
26840
|
//#endregion
|
|
26774
26841
|
//#region src/plugin/rules/react-native/rn-animate-layout-property.ts
|
|
26775
|
-
const
|
|
26842
|
+
const REANIMATED_MODULE = "react-native-reanimated";
|
|
26843
|
+
const REANIMATED_LAYOUT_STYLE_PROPERTIES = new Set([
|
|
26776
26844
|
"width",
|
|
26777
26845
|
"height",
|
|
26778
26846
|
"top",
|
|
@@ -26810,12 +26878,91 @@ const REANIMATED_LAYOUT_KEYS = new Set([
|
|
|
26810
26878
|
"lineHeight",
|
|
26811
26879
|
"letterSpacing"
|
|
26812
26880
|
]);
|
|
26881
|
+
const REANIMATED_ANIMATION_HELPERS = new Set([
|
|
26882
|
+
"withTiming",
|
|
26883
|
+
"withSpring",
|
|
26884
|
+
"withDecay",
|
|
26885
|
+
"withDelay",
|
|
26886
|
+
"withRepeat",
|
|
26887
|
+
"withSequence",
|
|
26888
|
+
"withClamp"
|
|
26889
|
+
]);
|
|
26890
|
+
const findImportDeclaration = (symbol) => {
|
|
26891
|
+
let currentNode = symbol.declarationNode.parent;
|
|
26892
|
+
while (currentNode && !isNodeOfType(currentNode, "ImportDeclaration")) currentNode = currentNode.parent ?? null;
|
|
26893
|
+
return currentNode && isNodeOfType(currentNode, "ImportDeclaration") ? currentNode : null;
|
|
26894
|
+
};
|
|
26895
|
+
const isReanimatedImport = (symbol) => {
|
|
26896
|
+
const importDeclaration = findImportDeclaration(symbol);
|
|
26897
|
+
if (!importDeclaration) return false;
|
|
26898
|
+
return importDeclaration.source.value === REANIMATED_MODULE;
|
|
26899
|
+
};
|
|
26900
|
+
const getReanimatedNamedImport = (node, context) => {
|
|
26901
|
+
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26902
|
+
const symbol = context.scopes.symbolFor(node);
|
|
26903
|
+
if (!symbol || symbol.kind !== "import") return null;
|
|
26904
|
+
if (!isReanimatedImport(symbol)) return null;
|
|
26905
|
+
const declarationNode = symbol.declarationNode;
|
|
26906
|
+
if (!isNodeOfType(declarationNode, "ImportSpecifier")) return null;
|
|
26907
|
+
const importedName = declarationNode.imported;
|
|
26908
|
+
if (isNodeOfType(importedName, "Identifier")) return importedName.name;
|
|
26909
|
+
if (isNodeOfType(importedName, "Literal") && typeof importedName.value === "string") return importedName.value;
|
|
26910
|
+
return null;
|
|
26911
|
+
};
|
|
26912
|
+
const isReanimatedNamespace = (node, context) => {
|
|
26913
|
+
if (!isNodeOfType(node, "Identifier")) return false;
|
|
26914
|
+
const symbol = context.scopes.symbolFor(node);
|
|
26915
|
+
if (!symbol || symbol.kind !== "import") return false;
|
|
26916
|
+
if (!isReanimatedImport(symbol)) return false;
|
|
26917
|
+
return isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
26918
|
+
};
|
|
26919
|
+
const isUseAnimatedStyleCallee = (callee, context) => {
|
|
26920
|
+
if (isNodeOfType(callee, "Identifier")) return getReanimatedNamedImport(callee, context) === "useAnimatedStyle";
|
|
26921
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
26922
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
26923
|
+
if (callee.property.name !== "useAnimatedStyle") return false;
|
|
26924
|
+
return isReanimatedNamespace(callee.object, context);
|
|
26925
|
+
};
|
|
26926
|
+
const isReanimatedAnimationHelperCallee = (callee, context) => {
|
|
26927
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
26928
|
+
const importedName = getReanimatedNamedImport(callee, context);
|
|
26929
|
+
return importedName !== null && REANIMATED_ANIMATION_HELPERS.has(importedName);
|
|
26930
|
+
}
|
|
26931
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
26932
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
26933
|
+
if (!REANIMATED_ANIMATION_HELPERS.has(callee.property.name)) return false;
|
|
26934
|
+
return isReanimatedNamespace(callee.object, context);
|
|
26935
|
+
};
|
|
26936
|
+
const isFunctionLike = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration");
|
|
26937
|
+
const containsReanimatedAnimationHelperCall = (expression, context) => {
|
|
26938
|
+
const rootExpression = stripParenExpression(expression);
|
|
26939
|
+
let didFindAnimationHelper = false;
|
|
26940
|
+
walkAst(rootExpression, (node) => {
|
|
26941
|
+
if (didFindAnimationHelper) return false;
|
|
26942
|
+
if (node !== rootExpression && isFunctionLike(node)) return false;
|
|
26943
|
+
if (isNodeOfType(node, "CallExpression") && isReanimatedAnimationHelperCallee(node.callee, context)) {
|
|
26944
|
+
didFindAnimationHelper = true;
|
|
26945
|
+
return false;
|
|
26946
|
+
}
|
|
26947
|
+
});
|
|
26948
|
+
return didFindAnimationHelper;
|
|
26949
|
+
};
|
|
26813
26950
|
const findReturnedObject = (callback) => {
|
|
26814
26951
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return null;
|
|
26815
|
-
const body = callback.body;
|
|
26952
|
+
const body = stripParenExpression(callback.body);
|
|
26816
26953
|
if (isNodeOfType(body, "ObjectExpression")) return body;
|
|
26817
26954
|
if (!isNodeOfType(body, "BlockStatement")) return null;
|
|
26818
|
-
for (const
|
|
26955
|
+
for (const statement of body.body ?? []) {
|
|
26956
|
+
if (!isNodeOfType(statement, "ReturnStatement")) continue;
|
|
26957
|
+
if (!statement.argument) continue;
|
|
26958
|
+
const returnArgument = stripParenExpression(statement.argument);
|
|
26959
|
+
if (isNodeOfType(returnArgument, "ObjectExpression")) return returnArgument;
|
|
26960
|
+
}
|
|
26961
|
+
return null;
|
|
26962
|
+
};
|
|
26963
|
+
const getStaticPropertyName$1 = (property) => {
|
|
26964
|
+
if (!property.computed && isNodeOfType(property.key, "Identifier")) return property.key.name;
|
|
26965
|
+
if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") return property.key.value;
|
|
26819
26966
|
return null;
|
|
26820
26967
|
};
|
|
26821
26968
|
const rnAnimateLayoutProperty = defineRule({
|
|
@@ -26823,21 +26970,25 @@ const rnAnimateLayoutProperty = defineRule({
|
|
|
26823
26970
|
title: "Animating a layout property",
|
|
26824
26971
|
tags: ["test-noise"],
|
|
26825
26972
|
requires: ["react-native"],
|
|
26826
|
-
severity: "
|
|
26827
|
-
|
|
26973
|
+
severity: "warn",
|
|
26974
|
+
category: "Performance",
|
|
26975
|
+
recommendation: "Prefer transform or opacity when a Reanimated animation helper drives purely visual motion; use layout-affecting styles only when the layout itself must change.",
|
|
26828
26976
|
create: (context) => ({ CallExpression(node) {
|
|
26829
|
-
if (!
|
|
26977
|
+
if (!isUseAnimatedStyleCallee(node.callee, context)) return;
|
|
26830
26978
|
const callback = node.arguments?.[0];
|
|
26831
26979
|
if (!callback) return;
|
|
26832
26980
|
const returnedObject = findReturnedObject(callback);
|
|
26833
26981
|
if (!returnedObject) return;
|
|
26834
26982
|
for (const property of returnedObject.properties ?? []) {
|
|
26835
26983
|
if (!isNodeOfType(property, "Property")) continue;
|
|
26836
|
-
|
|
26837
|
-
if (
|
|
26984
|
+
const propertyName = getStaticPropertyName$1(property);
|
|
26985
|
+
if (propertyName === null) continue;
|
|
26986
|
+
if (!REANIMATED_LAYOUT_STYLE_PROPERTIES.has(propertyName)) continue;
|
|
26987
|
+
const propertyValue = property.value;
|
|
26988
|
+
if (!containsReanimatedAnimationHelperCall(propertyValue, context)) continue;
|
|
26838
26989
|
context.report({
|
|
26839
26990
|
node: property,
|
|
26840
|
-
message: `
|
|
26991
|
+
message: `Reanimated can animate "${propertyName}", but this layout-affecting style recalculates layout while the animation runs; prefer transform or opacity when the motion is only visual.`
|
|
26841
26992
|
});
|
|
26842
26993
|
}
|
|
26843
26994
|
} })
|
|
@@ -27045,9 +27196,13 @@ const LEGACY_EXPO_PACKAGE_REPLACEMENTS = new Map([
|
|
|
27045
27196
|
["expo-av", "expo-audio for audio and expo-video for video"],
|
|
27046
27197
|
["expo-permissions", "the permissions API in each module (e.g. Camera.requestPermissionsAsync())"],
|
|
27047
27198
|
["expo-app-loading", "expo-splash-screen"],
|
|
27048
|
-
["expo-linear-gradient", "the `backgroundImage` CSS gradient style prop (New Architecture) or expo-linear-gradient's successor"],
|
|
27049
27199
|
["react-native-fast-image", "expo-image (drop-in with caching, placeholders, and crossfades)"]
|
|
27050
27200
|
]);
|
|
27201
|
+
const EXPO_UI_MODULE_SOURCES = new Set([
|
|
27202
|
+
"@expo/ui",
|
|
27203
|
+
"@expo/ui/swift-ui",
|
|
27204
|
+
"@expo/ui/jetpack-compose"
|
|
27205
|
+
]);
|
|
27051
27206
|
const REACT_NATIVE_LIST_COMPONENTS = new Set([
|
|
27052
27207
|
"FlatList",
|
|
27053
27208
|
"SectionList",
|
|
@@ -27195,6 +27350,10 @@ const RECYCLABLE_LIST_PACKAGES = {
|
|
|
27195
27350
|
LegendList: ["@legendapp/list"]
|
|
27196
27351
|
};
|
|
27197
27352
|
const SIZING_HINT_ATTRIBUTE_NAMES = new Set(["estimatedItemSize", "estimatedListSize"]);
|
|
27353
|
+
const isFlashListV2OrNewer = (context) => {
|
|
27354
|
+
const flashListMajorVersion = getReactDoctorNumberSetting(context.settings, "shopifyFlashListMajorVersion");
|
|
27355
|
+
return flashListMajorVersion !== void 0 && flashListMajorVersion >= 2;
|
|
27356
|
+
};
|
|
27198
27357
|
const isEmptyArrayLiteral = (node) => {
|
|
27199
27358
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return false;
|
|
27200
27359
|
const expression = node.value.expression;
|
|
@@ -27216,6 +27375,7 @@ const rnListMissingEstimatedItemSize = defineRule({
|
|
|
27216
27375
|
break;
|
|
27217
27376
|
}
|
|
27218
27377
|
if (!canonicalRecyclerName) return;
|
|
27378
|
+
if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
|
|
27219
27379
|
let hasSizingHint = false;
|
|
27220
27380
|
let dataIsEmptyLiteral = false;
|
|
27221
27381
|
let hasDataProp = false;
|
|
@@ -27859,6 +28019,24 @@ const collectTextWrapperComponents = (programNode, isTextHandlingRoot) => {
|
|
|
27859
28019
|
return wrappers;
|
|
27860
28020
|
};
|
|
27861
28021
|
//#endregion
|
|
28022
|
+
//#region src/plugin/rules/react-native/utils/is-expo-ui-component-element.ts
|
|
28023
|
+
const isNamedImportOf = (contextNode, localName, componentName) => {
|
|
28024
|
+
for (const moduleSource of EXPO_UI_MODULE_SOURCES) if (getImportedNameFromModule(contextNode, localName, moduleSource) === componentName) return true;
|
|
28025
|
+
return false;
|
|
28026
|
+
};
|
|
28027
|
+
const isExpoUiNamespaceImport = (contextNode, localName) => {
|
|
28028
|
+
for (const moduleSource of EXPO_UI_MODULE_SOURCES) if (isNamespaceImportFromModule(contextNode, localName, moduleSource)) return true;
|
|
28029
|
+
return false;
|
|
28030
|
+
};
|
|
28031
|
+
const isExpoUiComponentElement = (openingElement, contextNode, componentName) => {
|
|
28032
|
+
if (!openingElement.name) return false;
|
|
28033
|
+
const dottedName = flattenJsxName(openingElement.name);
|
|
28034
|
+
if (!dottedName) return false;
|
|
28035
|
+
const [rootLocalName, secondName] = dottedName.split(".");
|
|
28036
|
+
if (isNamedImportOf(contextNode, rootLocalName, componentName)) return true;
|
|
28037
|
+
return secondName === componentName && isExpoUiNamespaceImport(contextNode, rootLocalName);
|
|
28038
|
+
};
|
|
28039
|
+
//#endregion
|
|
27862
28040
|
//#region src/plugin/rules/react-native/rn-no-raw-text.ts
|
|
27863
28041
|
const truncateText = (text) => text.length > 30 ? `${text.slice(0, 30)}...` : text;
|
|
27864
28042
|
const isRawTextContent = (child) => {
|
|
@@ -27918,6 +28096,7 @@ const rnNoRawText = defineRule({
|
|
|
27918
28096
|
if (isDomComponentFile) return;
|
|
27919
28097
|
const elementName = resolveTextBoundaryName(node.openingElement);
|
|
27920
28098
|
if (elementName && (isTextHandlingComponent(elementName) || autoDetectedWrappers.has(elementName))) return;
|
|
28099
|
+
if (isExpoUiComponentElement(node.openingElement, node, "ListItem")) return;
|
|
27921
28100
|
if (isInsidePlatformOsWebBranch(node)) return;
|
|
27922
28101
|
if (isTransparentTextWrapper(elementName) && isInsideTextHandlingComponent(node)) return;
|
|
27923
28102
|
for (const child of node.children ?? []) {
|
|
@@ -28081,6 +28260,7 @@ const rnNoScrollviewMappedList = defineRule({
|
|
|
28081
28260
|
create: (context) => ({ JSXElement(node) {
|
|
28082
28261
|
const elementName = resolveJsxElementName(node.openingElement);
|
|
28083
28262
|
if (!elementName || !NON_VIRTUALIZED_SCROLL_CONTAINERS.has(elementName)) return;
|
|
28263
|
+
if (isExpoUiComponentElement(node.openingElement, node, "ScrollView")) return;
|
|
28084
28264
|
for (const child of node.children ?? []) {
|
|
28085
28265
|
if (!isNodeOfType(child, "JSXExpressionContainer")) continue;
|
|
28086
28266
|
const expression = child.expression;
|
|
@@ -28138,37 +28318,22 @@ const rnNoSingleElementStyleArray = defineRule({
|
|
|
28138
28318
|
} })
|
|
28139
28319
|
});
|
|
28140
28320
|
//#endregion
|
|
28141
|
-
//#region src/plugin/
|
|
28142
|
-
const
|
|
28143
|
-
|
|
28144
|
-
|
|
28145
|
-
"
|
|
28146
|
-
|
|
28147
|
-
|
|
28148
|
-
]);
|
|
28321
|
+
//#region src/plugin/utils/define-retired-rule.ts
|
|
28322
|
+
const defineRetiredRule = (rule) => defineRule({
|
|
28323
|
+
...rule,
|
|
28324
|
+
defaultEnabled: false,
|
|
28325
|
+
lifecycle: "retired",
|
|
28326
|
+
create: () => ({})
|
|
28327
|
+
});
|
|
28149
28328
|
//#endregion
|
|
28150
28329
|
//#region src/plugin/rules/react-native/rn-prefer-content-inset-adjustment.ts
|
|
28151
|
-
const rnPreferContentInsetAdjustment =
|
|
28330
|
+
const rnPreferContentInsetAdjustment = defineRetiredRule({
|
|
28152
28331
|
id: "rn-prefer-content-inset-adjustment",
|
|
28153
|
-
title: "
|
|
28332
|
+
title: "Manual safe-area inset adjustment",
|
|
28154
28333
|
tags: ["test-noise"],
|
|
28155
28334
|
requires: ["react-native"],
|
|
28156
28335
|
severity: "warn",
|
|
28157
|
-
recommendation: "
|
|
28158
|
-
create: (context) => ({ JSXElement(node) {
|
|
28159
|
-
if (resolveJsxElementName(node.openingElement) !== "SafeAreaView") return;
|
|
28160
|
-
for (const child of node.children ?? []) {
|
|
28161
|
-
if (!isNodeOfType(child, "JSXElement")) continue;
|
|
28162
|
-
const childName = resolveJsxElementName(child.openingElement);
|
|
28163
|
-
if (!childName || !SCROLLVIEW_NAMES.has(childName)) continue;
|
|
28164
|
-
if (childName === "KeyboardAwareScrollView") continue;
|
|
28165
|
-
context.report({
|
|
28166
|
-
node,
|
|
28167
|
-
message: `Your users render an extra wrapper view from <SafeAreaView> around <${childName}>.`
|
|
28168
|
-
});
|
|
28169
|
-
return;
|
|
28170
|
-
}
|
|
28171
|
-
} })
|
|
28336
|
+
recommendation: "Prefer native content inset adjustment only when it replaces manual inset plumbing; SafeAreaView wrappers are valid and intentionally ignored."
|
|
28172
28337
|
});
|
|
28173
28338
|
//#endregion
|
|
28174
28339
|
//#region src/react-native-dependency-names.ts
|
|
@@ -28554,6 +28719,15 @@ const rnPressableSharedValueMutation = defineRule({
|
|
|
28554
28719
|
}
|
|
28555
28720
|
});
|
|
28556
28721
|
//#endregion
|
|
28722
|
+
//#region src/plugin/rules/react-native/utils/scrollview_names.ts
|
|
28723
|
+
const SCROLLVIEW_NAMES = new Set([
|
|
28724
|
+
"ScrollView",
|
|
28725
|
+
"FlatList",
|
|
28726
|
+
"SectionList",
|
|
28727
|
+
"VirtualizedList",
|
|
28728
|
+
"KeyboardAwareScrollView"
|
|
28729
|
+
]);
|
|
28730
|
+
//#endregion
|
|
28557
28731
|
//#region src/plugin/rules/react-native/rn-scrollview-dynamic-padding.ts
|
|
28558
28732
|
const rnScrollviewDynamicPadding = defineRule({
|
|
28559
28733
|
id: "rn-scrollview-dynamic-padding",
|
|
@@ -32249,7 +32423,7 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
32249
32423
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
32250
32424
|
let current = node.parent;
|
|
32251
32425
|
while (current) {
|
|
32252
|
-
if (isFunctionLike$
|
|
32426
|
+
if (isFunctionLike$3(current)) {
|
|
32253
32427
|
const resolvedName = inferFunctionName(current);
|
|
32254
32428
|
if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
|
|
32255
32429
|
}
|
|
@@ -32270,7 +32444,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
|
|
|
32270
32444
|
const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
|
|
32271
32445
|
let current = node.parent;
|
|
32272
32446
|
while (current) {
|
|
32273
|
-
if (isFunctionLike$
|
|
32447
|
+
if (isFunctionLike$3(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
|
|
32274
32448
|
current = current.parent ?? null;
|
|
32275
32449
|
}
|
|
32276
32450
|
return false;
|
|
@@ -32608,7 +32782,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
|
|
|
32608
32782
|
let foundAuthCall = false;
|
|
32609
32783
|
for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
|
|
32610
32784
|
if (foundAuthCall) return;
|
|
32611
|
-
if (isFunctionLike$
|
|
32785
|
+
if (isFunctionLike$3(child)) return false;
|
|
32612
32786
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32613
32787
|
if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
|
|
32614
32788
|
});
|
|
@@ -37222,7 +37396,7 @@ const reactDoctorRules = [
|
|
|
37222
37396
|
rule: {
|
|
37223
37397
|
...rnAnimateLayoutProperty,
|
|
37224
37398
|
framework: "react-native",
|
|
37225
|
-
category: "
|
|
37399
|
+
category: "Performance",
|
|
37226
37400
|
tags: [...new Set(["react-native", ...rnAnimateLayoutProperty.tags ?? []])]
|
|
37227
37401
|
}
|
|
37228
37402
|
},
|
|
@@ -38087,7 +38261,7 @@ const appendNode = (builder, block, node) => {
|
|
|
38087
38261
|
};
|
|
38088
38262
|
const mapDescendantsToBlock = (builder, node, block) => {
|
|
38089
38263
|
builder.nodeBlock.set(node, block);
|
|
38090
|
-
if (isFunctionLike$
|
|
38264
|
+
if (isFunctionLike$3(node)) return;
|
|
38091
38265
|
const record = node;
|
|
38092
38266
|
for (const key of Object.keys(record)) {
|
|
38093
38267
|
if (key === "parent") continue;
|
|
@@ -38425,7 +38599,7 @@ const analyzeControlFlow = (program) => {
|
|
|
38425
38599
|
body: program.body
|
|
38426
38600
|
});
|
|
38427
38601
|
const visit = (node) => {
|
|
38428
|
-
if (isFunctionLike$
|
|
38602
|
+
if (isFunctionLike$3(node)) {
|
|
38429
38603
|
const body = node.body;
|
|
38430
38604
|
if (body) buildFor(node, body);
|
|
38431
38605
|
}
|
|
@@ -38442,7 +38616,7 @@ const analyzeControlFlow = (program) => {
|
|
|
38442
38616
|
const enclosingFunction = (node) => {
|
|
38443
38617
|
let current = node;
|
|
38444
38618
|
while (current) {
|
|
38445
|
-
if (isFunctionLike$
|
|
38619
|
+
if (isFunctionLike$3(current)) return current;
|
|
38446
38620
|
if (isNodeOfType(current, "Program")) return current;
|
|
38447
38621
|
current = current.parent ?? null;
|
|
38448
38622
|
}
|