From d1a0294c828567ed98f886eeac940d38aadfb054 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:40:50 +0000 Subject: [PATCH] fix(formatjs): scope static evaluation to formatjs descriptor keys only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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={}`) 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. Fix: return early for non-Ident JSX element names, and only call `evaluate_expr` for known formatjs descriptor keys (`id`, `defaultMessage`, `description`). Fixes #588 Co-authored-by: Donny/강동윤 --- packages/formatjs/__tests__/wasm.test.ts | 59 ++++++++++++++++++++++++ packages/formatjs/transform/src/lib.rs | 25 ++++++++-- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/packages/formatjs/__tests__/wasm.test.ts b/packages/formatjs/__tests__/wasm.test.ts index 29780795a..87cca20a7 100644 --- a/packages/formatjs/__tests__/wasm.test.ts +++ b/packages/formatjs/__tests__/wasm.test.ts @@ -609,4 +609,63 @@ describe("formatjs swc plugin", () => { expect(code2).toMatch(/id: "Ae\/S0P"/); expect(code3).toMatch(/id: "Ae\/S0P"/); }); + + it("should not error on valid JSX outside formatjs calls (issue #588)", async () => { + // Member expression JSX names like React.Suspense with JSX fallback props + // should not trigger the static evaluation error. + const input = ` + import React from 'react'; + + const Loading = () =>
Loading...
; + + function App() { + return ( + }> +
Content
+
+ ); + } + `; + + // Should succeed without throwing "must be statically evaluate-able" error + await expect(transformCode(input)).resolves.toBeDefined(); + }); + + it("should not error on conditional JSX outside formatjs calls", async () => { + // Conditional JSX expressions unrelated to formatjs should not be evaluated + const input = ` + import React from 'react'; + + function App({ isLoading }: { isLoading: boolean }) { + return ( +
+ {isLoading ? Loading... : Done} +
+ ); + } + `; + + await expect(transformCode(input)).resolves.toBeDefined(); + }); + + it("should not error on FormattedMessage with JSX values prop", async () => { + // FormattedMessage with a `values` prop containing JSX should not error, + // since `values` is not a known formatjs descriptor key. + const input = ` + import React from 'react'; + import { FormattedMessage } from 'react-intl'; + + function App() { + return ( + {chunks}, name: "World" }} + /> + ); + } + `; + + const output = await transformCode(input); + expect(output).toMatch(/defaultMessage/); + }); }); diff --git a/packages/formatjs/transform/src/lib.rs b/packages/formatjs/transform/src/lib.rs index 42424290c..4450c2453 100644 --- a/packages/formatjs/transform/src/lib.rs +++ b/packages/formatjs/transform/src/lib.rs @@ -91,9 +91,17 @@ impl MessageDescriptorExtractor for JSXAttrOrSpread { let key = match name { JSXAttrName::Ident(name) | JSXAttrName::JSXNamespacedName(JSXNamespacedName { name, .. }) => { - Some(name.sym.to_string()) + name.sym.to_string() } }; + + // Only evaluate values for known formatjs message descriptor keys. + // Attributes like `values`, `fallback`, etc. may contain JSX + // expressions that cannot (and should not) be statically evaluated. + if !matches!(key.as_str(), "id" | "defaultMessage" | "description") { + return None; + } + let value = match value { JSXAttrValue::Str(s) => Some(MessageDescriptionValue::Str( s.value.as_str().expect("non-utf8 string").to_string(), @@ -126,7 +134,7 @@ impl MessageDescriptorExtractor for JSXAttrOrSpread { _ => None, }; - if let (Some(key), Some(value)) = (key, value) { + if let Some(value) = value { Some((key, value)) } else { None @@ -978,10 +986,17 @@ impl<'a, C: Clone + Comments, S: SourceMapper> VisitMut for FormatJSVisitor<'a, let name = &jsx_opening_elem.name; - if let JSXElementName::Ident(ident) = name { - if !self.component_names.contains(&*ident.sym) { - return; + // Only process known formatjs component names (simple identifiers). + // Member expressions (e.g. `React.Suspense`) and namespaced names are + // never formatjs components, so return early to avoid evaluating their + // attributes. + match name { + JSXElementName::Ident(ident) => { + if !self.component_names.contains(&*ident.sym) { + return; + } } + _ => return, } let mut descriptor = self.create_message_descriptor_from_extractor(&jsx_opening_elem.attrs);