Skip to content

fix: unwrap non-null assertion (x!) / satisfies in placeholders (parity with babel macro) - #247

Merged
andrii-bodnar merged 2 commits into
lingui:mainfrom
pkasarda:fix/unwrap-ts-nonnull-satisfies-placeholder
Jul 21, 2026
Merged

fix: unwrap non-null assertion (x!) / satisfies in placeholders (parity with babel macro)#247
andrii-bodnar merged 2 commits into
lingui:mainfrom
pkasarda:fix/unwrap-ts-nonnull-satisfies-placeholder

Conversation

@pkasarda

@pkasarda pkasarda commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Description

Companion to lingui/js-lingui#2622.

unwrap_ts_only_expr (renamed from unwrap_ts_as_expr) only unwrapped TsAs, so a placeholder whose expression is wrapped in a non-null assertion (x!) or satisfies fell through to the numeric-index branch and got an indexed placeholder ({0}) instead of a named one ({x}), diverging from @lingui/babel-plugin-lingui-macro. Same class as #239.

This unwraps TsNonNull and TsSatisfies alongside TsAs so all TS-only wrappers let the inner expression name the placeholder.

⚠️ Important: real-world impact is limited (please read)

In practice @swc/core strips TS-only operators before this plugin runs, so consumers going through @swc/core (rspack, Next.js, Vite, rsbuild, …) already get the named placeholder today — the plugin never sees the !. Verified locally with @swc/core@1.15.43 + @lingui/swc-plugin@6.5.1:

// @swc/core alone
const y = x!;   ->   const y = x;

// @swc/core + @lingui/swc-plugin
t`no.: ${x!}`   ->   _i18n._({ id: "FPCAwc", message: "no.: {x}", values: { x: x } })   // named

So the {0} only surfaces when the plugin transforms a not-yet-stripped TS AST — e.g. this crate's own to! test harness (which feeds the parsed AST directly to the visitor). Before this change, the added test snapshots Variable {0}; after it, Variable {name}.

Why fix it anyway: it makes the plugin's placeholder naming correct independent of pipeline ordering, keeps it consistent with the babel macro (js-lingui#2622), and is what lets the regression test assert {name} (parity) rather than a misleading {0}. It does not change output for the common @swc/core pipelines.

Test

Adds js_non_null_assertion_gets_named_placeholder in js_t.rs (mirrors the js-lingui regression test). Full lingui_macro suite passes with no other snapshot changes.

🤖 Generated with Claude Code

`unwrap_ts_only_expr` (formerly `unwrap_ts_as_expr`) only unwrapped `TsAs`, so a
placeholder like `t`${x!}`` was treated as a complex expression and assigned an
indexed placeholder (`{0}`) instead of a named one (`{x}`) — diverging from
`@lingui/babel-plugin-lingui-macro` and breaking catalog lookups (mismatched
message ids) when extracting with the JS macro but running with the SWC plugin.

Unwrap `TsNonNull` and `TsSatisfies` alongside `TsAs` so all TS-only wrappers let
the inner expression name the placeholder. Companion to lingui/js-lingui#2622.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.84%. Comparing base (a7075a4) to head (ce81d7f).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #247   +/-   ##
=======================================
  Coverage   94.83%   94.84%           
=======================================
  Files           9        9           
  Lines        2325     2327    +2     
=======================================
+ Hits         2205     2207    +2     
  Misses        120      120           
Flag Coverage Δ
unittests 94.84% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
crates/lingui_macro/src/macro_utils.rs 91.77% <100.00%> (+0.05%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Lingui SWC macro plugin’s placeholder-name inference so TypeScript-only expression wrappers don’t force placeholders into the numeric-index path, aligning behavior with the Babel macro and making the direct-AST test harness reflect real-world expectations.

Changes:

  • Generalize TS-only unwrapping from just TsAs to also include TsNonNull (x!) and TsSatisfies (x satisfies T) when deriving placeholder names.
  • Add a regression test + snapshot asserting ${name!} yields a named placeholder ({name}) in t template literals.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
crates/lingui_macro/src/macro_utils.rs Extend TS-only expression unwrapping used for placeholder naming/value handling to include TsNonNull and TsSatisfies.
crates/lingui_macro/tests/js_t.rs Add a regression test covering non-null assertion in template placeholders.
crates/lingui_macro/tests/snapshots/js_t__js_non_null_assertion_gets_named_placeholder.snap New snapshot validating {name} placeholder output for ${name!}.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/lingui_macro/src/macro_utils.rs
Covers `${x satisfies T}` producing a named placeholder (`{x}`), matching the
non-null case. Addresses review feedback that `TsSatisfies` handling was
otherwise untested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@andrii-bodnar
andrii-bodnar merged commit 1aebcb5 into lingui:main Jul 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants