From 390de450102d2b50808e39b41c3d75eed46c5e2c Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Mon, 13 Oct 2025 23:55:21 +0900 Subject: [PATCH 1/3] feat: add isChainExpression --- lib/node-utils/is-node-of-type.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/node-utils/is-node-of-type.ts b/lib/node-utils/is-node-of-type.ts index 86e7c881..c3cd978c 100644 --- a/lib/node-utils/is-node-of-type.ts +++ b/lib/node-utils/is-node-of-type.ts @@ -21,6 +21,9 @@ export const isVariableDeclaration = ASTUtils.isNodeOfType( export const isAssignmentExpression = ASTUtils.isNodeOfType( AST_NODE_TYPES.AssignmentExpression ); +export const isChainExpression = ASTUtils.isNodeOfType( + AST_NODE_TYPES.ChainExpression +); export const isSequenceExpression = ASTUtils.isNodeOfType( AST_NODE_TYPES.SequenceExpression ); From dbbfc4a52411df36774d188895fc27a8fcb1d16a Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Tue, 14 Oct 2025 00:02:06 +0900 Subject: [PATCH 2/3] feat: support ChainExpression --- lib/node-utils/index.ts | 66 +++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/lib/node-utils/index.ts b/lib/node-utils/index.ts index 2b032a78..766b84a4 100644 --- a/lib/node-utils/index.ts +++ b/lib/node-utils/index.ts @@ -6,6 +6,7 @@ import { isArrayExpression, isArrowFunctionExpression, isAssignmentExpression, + isChainExpression, isBlockStatement, isCallExpression, isExpressionStatement, @@ -379,7 +380,7 @@ export function getPropertyIdentifierNode( return getPropertyIdentifierNode(node.callee); } - if (isExpressionStatement(node)) { + if (isExpressionStatement(node) || isChainExpression(node)) { return getPropertyIdentifierNode(node.expression); } @@ -407,6 +408,10 @@ export function getDeepestIdentifierNode( return node; } + if (isChainExpression(node)) { + return getDeepestIdentifierNode(node.expression); + } + if (isMemberExpression(node) && ASTUtils.isIdentifier(node.property)) { return node.property; } @@ -615,48 +620,45 @@ export function hasImportMatch( return importNode.local.name === identifierName; } -export function getStatementCallExpression( - statement: TSESTree.Statement -): TSESTree.CallExpression | undefined { - if (isExpressionStatement(statement)) { - const { expression } = statement; - if (isCallExpression(expression)) { - return expression; - } +function getCallExpressionFromNode( + node: TSESTree.Node | null +): TSESTree.CallExpression | null { + if (isCallExpression(node)) { + return node; + } - if ( - ASTUtils.isAwaitExpression(expression) && - isCallExpression(expression.argument) - ) { - return expression.argument; - } + if (isChainExpression(node)) { + return getCallExpressionFromNode(node.expression); + } - if (isAssignmentExpression(expression)) { - if (isCallExpression(expression.right)) { - return expression.right; - } + if (ASTUtils.isAwaitExpression(node)) { + return getCallExpressionFromNode(node.argument); + } - if ( - ASTUtils.isAwaitExpression(expression.right) && - isCallExpression(expression.right.argument) - ) { - return expression.right.argument; - } - } + if (isAssignmentExpression(node)) { + return getCallExpressionFromNode(node.right); } - if (isReturnStatement(statement) && isCallExpression(statement.argument)) { - return statement.argument; + return null; +} + +export function getStatementCallExpression( + statement: TSESTree.Statement +): TSESTree.CallExpression | null { + if (isExpressionStatement(statement)) { + return getCallExpressionFromNode(statement.expression); + } + + if (isReturnStatement(statement)) { + return getCallExpressionFromNode(statement.argument); } if (isVariableDeclaration(statement)) { for (const declaration of statement.declarations) { - if (isCallExpression(declaration.init)) { - return declaration.init; - } + return getCallExpressionFromNode(declaration.init); } } - return undefined; + return null; } /** From 5f86d33fa725518adedbc754b3d5ee91a9a7a2be Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Tue, 14 Oct 2025 00:03:54 +0900 Subject: [PATCH 3/3] test: add tests --- tests/lib/rules/no-unnecessary-act.test.ts | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/lib/rules/no-unnecessary-act.test.ts b/tests/lib/rules/no-unnecessary-act.test.ts index 352bddd6..7d02744d 100644 --- a/tests/lib/rules/no-unnecessary-act.test.ts +++ b/tests/lib/rules/no-unnecessary-act.test.ts @@ -78,6 +78,18 @@ const validNonStrictTestCases: RuleValidTestCase[] = [ }) `, }, + { + code: `// case: RTL act wrapping optional chaining call without RTL usage + import { act, render } from '@testing-library/react' + + test('valid case', async () => { + act(() => { + render(element); + callback?.(); + }); + }); + `, + }, ]; const validTestCases: RuleValidTestCase[] = [ @@ -140,6 +152,19 @@ const validTestCases: RuleValidTestCase[] = [ act(() => stuffThatDoesNotUseRTL()).then(() => {}) act(stuffThatDoesNotUseRTL().then(() => {})) }); + `, + })), + ...SUPPORTED_TESTING_FRAMEWORKS.map(([testingFramework, shortName]) => ({ + code: `// case: ${shortName} act wrapping non-${shortName} calls + import { act } from '${testingFramework}' + + let callback: undefined | (() => void); + + test('valid case', async () => { + act(() => { + callback?.(); + }); + }); `, })), {