Skip to content

Commit cb7bb23

Browse files
walkerburginlonglhoclaude
authored
fix(formatjs_cli): extract formatMessage from callbacks inside optional chains (#6460)
Fixes #6459. The Rust CLI's chain-expression visitor walked into call expression arguments, but skipped the callee. As a result, `formatMessage` calls inside callbacks passed to optional-chain methods (e.g. `getUsers()?.map(() => intl.formatMessage(...))`) were never extracted. ## Changes - Walk both the callee and arguments of a `CallExpression` chain element so nested `formatMessage` calls inside callback bodies are extracted. - Drop the `StaticMemberExpression` arm — `walk_chain_expression` covers it via the default case. - Add fixture case in `packages/ts-transformer/tests/fixtures/optionalChaining.tsx`. - Add Rust unit-test expectation in `crates/formatjs_cli/src/extractor.rs`. - Add ts-transformer assertion in `packages/ts-transformer/tests/index.test.ts`. - Add CLI conformance case `22_callback_in_optional_chain.txt` so both Rust and TS extractors are guarded. ## Test plan - [x] `bazel test //crates/formatjs_cli:formatjs_cli_test` - [x] `bazel test //packages/ts-transformer:ts-transformer_test` - [x] `bazel test //packages/cli/integration-tests:conformance_test` --------- Co-authored-by: Long Ho <[email protected]> Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
1 parent c281a36 commit cb7bb23

5 files changed

Lines changed: 64 additions & 10 deletions

File tree

MODULE.bazel.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/formatjs_cli/src/extractor.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -591,19 +591,14 @@ impl<'a> Visit<'a> for MessageExtractor<'a> {
591591
oxc_ast::ast::ChainElement::CallExpression(call) => {
592592
// Extract the message from this call expression
593593
self.extract_call_expression_message(call);
594-
// Don't walk - this prevents double extraction since walk would
595-
// visit the arguments which may contain nested chains
596-
// Instead, manually walk just the arguments to find nested messages
594+
// Walk both callee and arguments to find nested messages
595+
walk::walk_expression(self, &call.callee);
597596
for arg in &call.arguments {
598597
if let Some(expr) = arg.as_expression() {
599598
walk::walk_expression(self, expr);
600599
}
601600
}
602601
}
603-
oxc_ast::ast::ChainElement::StaticMemberExpression(_) => {
604-
// For member expressions in chains, walk normally to find nested patterns
605-
walk::walk_chain_expression(self, it);
606-
}
607602
_ => {
608603
// For other chain elements, walk normally
609604
walk::walk_chain_expression(self, it);
@@ -1379,6 +1374,14 @@ mod tests {
13791374
start: None,
13801375
end: None,
13811376
},
1377+
MessageDescriptor {
1378+
id: None,
1379+
default_message: Some("In a callback inside an optional chain".to_string()),
1380+
description: Some(Value::String("Test callbacks inside an optional chain".to_string())),
1381+
file: None,
1382+
start: None,
1383+
end: None,
1384+
},
13821385
];
13831386

13841387
assert_eq!(messages, expected);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react';
2+
3+
function getUsers(): Array<string> | undefined {
4+
return undefined;
5+
}
6+
7+
function MyComponent({intl}) {
8+
// formatMessage nested in a callback inside an optional chain.
9+
const messages = getUsers()?.map(() =>
10+
intl.formatMessage({
11+
id: 'callback.in.optional.chain',
12+
defaultMessage: 'In a callback inside an optional chain',
13+
description: 'Test callbacks inside an optional chain'
14+
})
15+
).join(', ');
16+
17+
return <div>{messages}</div>;
18+
}
19+
---
20+
{
21+
"command": "extract",
22+
"args": ["--throws"],
23+
"fileType": "tsx"
24+
}
25+
---
26+
{
27+
"callback.in.optional.chain": {
28+
"defaultMessage": "In a callback inside an optional chain",
29+
"description": "Test callbacks inside an optional chain"
30+
}
31+
}

packages/ts-transformer/tests/fixtures/optionalChaining.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
// @ts-ignore
22
const intl = {} as any
33

4+
function getUsers(): Array<string> | undefined {
5+
return undefined;
6+
}
7+
48
// GH #4471: Test optional chaining with generics
5-
export function testOptionalChainingWithGenerics() {
9+
export function testOptionalChainingWithGenerics(): void {
610
// Case 1: Normal call (baseline - should work)
711
intl.formatMessage({
812
defaultMessage: 'Normal call',
@@ -33,4 +37,14 @@ export function testOptionalChainingWithGenerics() {
3337
defaultMessage: 'Nested optional chaining',
3438
description: 'Test nested optional chaining',
3539
})
40+
41+
// Case 6: formatMessage nested in a callback inside an optional chain.
42+
getUsers()?.map(() =>
43+
intl.formatMessage(
44+
{
45+
defaultMessage: 'In a callback inside an optional chain',
46+
description: 'Test callbacks inside an optional chain',
47+
},
48+
),
49+
).join(', ')
3650
}

packages/ts-transformer/tests/index.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,11 @@ describe('emit asserts for', function () {
710710
description: 'Test nested optional chaining',
711711
id: 'DZ/55FDtO6',
712712
},
713+
{
714+
defaultMessage: 'In a callback inside an optional chain',
715+
description: 'Test callbacks inside an optional chain',
716+
id: '7SldKmH/WX',
717+
},
713718
])
714719
})
715720

0 commit comments

Comments
 (0)