Skip to content

Commit ac7669d

Browse files
fix(formatjs): scope static evaluation to formatjs descriptor keys only (#591)
Two bugs caused valid JSX outside formatjs calls to trigger the "must be statically evaluate-able" error: 1. `visit_mut_jsx_opening_element` only guarded against `JSXElementName::Ident` names. Member expressions like `React.Suspense` fell through the guard and had their attributes (e.g. `fallback={<Loading />}`) evaluated, causing spurious errors. 2. `JSXAttrOrSpread::get_key_value_with_visitor` called `evaluate_expr` for ALL `JSXExprContainer` attribute values regardless of the attribute key. Attributes like `values` or `fallback` that contain JSX expressions would trigger the error even inside legitimate `FormattedMessage` components. Fixes #588 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Donny/강동윤 <kdy1@users.noreply.github.com>
1 parent b91178e commit ac7669d

2 files changed

Lines changed: 79 additions & 5 deletions

File tree

packages/formatjs/__tests__/wasm.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,4 +609,63 @@ describe("formatjs swc plugin", () => {
609609
expect(code2).toMatch(/id: "Ae\/S0P"/);
610610
expect(code3).toMatch(/id: "Ae\/S0P"/);
611611
});
612+
613+
it("should not error on valid JSX outside formatjs calls (issue #588)", async () => {
614+
// Member expression JSX names like React.Suspense with JSX fallback props
615+
// should not trigger the static evaluation error.
616+
const input = `
617+
import React from 'react';
618+
619+
const Loading = () => <div>Loading...</div>;
620+
621+
function App() {
622+
return (
623+
<React.Suspense fallback={<Loading />}>
624+
<div>Content</div>
625+
</React.Suspense>
626+
);
627+
}
628+
`;
629+
630+
// Should succeed without throwing "must be statically evaluate-able" error
631+
await expect(transformCode(input)).resolves.toBeDefined();
632+
});
633+
634+
it("should not error on conditional JSX outside formatjs calls", async () => {
635+
// Conditional JSX expressions unrelated to formatjs should not be evaluated
636+
const input = `
637+
import React from 'react';
638+
639+
function App({ isLoading }: { isLoading: boolean }) {
640+
return (
641+
<div>
642+
{isLoading ? <span>Loading...</span> : <span>Done</span>}
643+
</div>
644+
);
645+
}
646+
`;
647+
648+
await expect(transformCode(input)).resolves.toBeDefined();
649+
});
650+
651+
it("should not error on FormattedMessage with JSX values prop", async () => {
652+
// FormattedMessage with a `values` prop containing JSX should not error,
653+
// since `values` is not a known formatjs descriptor key.
654+
const input = `
655+
import React from 'react';
656+
import { FormattedMessage } from 'react-intl';
657+
658+
function App() {
659+
return (
660+
<FormattedMessage
661+
defaultMessage="Hello <b>{name}</b>"
662+
values={{ b: (chunks) => <b>{chunks}</b>, name: "World" }}
663+
/>
664+
);
665+
}
666+
`;
667+
668+
const output = await transformCode(input);
669+
expect(output).toMatch(/defaultMessage/);
670+
});
612671
});

packages/formatjs/transform/src/lib.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,17 @@ impl MessageDescriptorExtractor for JSXAttrOrSpread {
9191
let key = match name {
9292
JSXAttrName::Ident(name)
9393
| JSXAttrName::JSXNamespacedName(JSXNamespacedName { name, .. }) => {
94-
Some(name.sym.to_string())
94+
name.sym.to_string()
9595
}
9696
};
97+
98+
// Only evaluate values for known formatjs message descriptor keys.
99+
// Attributes like `values`, `fallback`, etc. may contain JSX
100+
// expressions that cannot (and should not) be statically evaluated.
101+
if !matches!(key.as_str(), "id" | "defaultMessage" | "description") {
102+
return None;
103+
}
104+
97105
let value = match value {
98106
JSXAttrValue::Str(s) => Some(MessageDescriptionValue::Str(
99107
s.value.as_str().expect("non-utf8 string").to_string(),
@@ -126,7 +134,7 @@ impl MessageDescriptorExtractor for JSXAttrOrSpread {
126134
_ => None,
127135
};
128136

129-
if let (Some(key), Some(value)) = (key, value) {
137+
if let Some(value) = value {
130138
Some((key, value))
131139
} else {
132140
None
@@ -978,10 +986,17 @@ impl<'a, C: Clone + Comments, S: SourceMapper> VisitMut for FormatJSVisitor<'a,
978986

979987
let name = &jsx_opening_elem.name;
980988

981-
if let JSXElementName::Ident(ident) = name {
982-
if !self.component_names.contains(&*ident.sym) {
983-
return;
989+
// Only process known formatjs component names (simple identifiers).
990+
// Member expressions (e.g. `React.Suspense`) and namespaced names are
991+
// never formatjs components, so return early to avoid evaluating their
992+
// attributes.
993+
match name {
994+
JSXElementName::Ident(ident) => {
995+
if !self.component_names.contains(&*ident.sym) {
996+
return;
997+
}
984998
}
999+
_ => return,
9851000
}
9861001

9871002
let mut descriptor = self.create_message_descriptor_from_extractor(&jsx_opening_elem.attrs);

0 commit comments

Comments
 (0)