Skip to content

fix: improve detection of comment directives#226

Merged
timofei-iatsenko merged 11 commits into
lingui:mainfrom
mogelbrod:disappearing-directives
Jun 9, 2026
Merged

fix: improve detection of comment directives#226
timofei-iatsenko merged 11 commits into
lingui:mainfrom
mogelbrod:disappearing-directives

Conversation

@mogelbrod

Copy link
Copy Markdown
Contributor

Fix lingui-set directives disappearing in the SWC plugin when they appear before TypeScript-only declarations, such as export type.

lingui-set context was sometimes lost during SWC transform. This resulted in message IDs were generated without the directive context, so the transformed app code used different IDs from the extracted catalogs.

In practice, a file like this could produce the wrong ID:

// lingui-set context="navigation"
export type SomeType = string

const table = {
  home: msg`Home`,
}

Instead of using the navigation context and generating the context-aware ID, the transform fell back to the unscoped ID.

Root cause

The plugin’s existing directive lookup depended on SWC comment attachment by node position. That works when the directive comment is attached to a visited runtime node, but it breaks for comments that sit before TypeScript-only declarations or before the first import.

There were two related gaps:

  1. The positional comment collector only sees comments that SWC exposes through nearby AST spans.
  2. The source-snippet fallback was anchored to program.span(), which starts at the first AST token, not necessarily at the beginning of the file. That means top-of-file lingui-set comments could be excluded from the fallback source text entirely.

So the plugin could miss a valid directive even though the source file clearly contained it.

Solution

This change adds a source-text-based directive fallback and wires it into directive lookup:

  • Parse lingui-set / lingui-reset directives directly from source lines.
  • Store those parsed directives in MacroCtx.
  • Resolve directives by byte position via a line lookup when positional SWC comments are missing.
  • Expand the source snippet used for fallback collection to start at the containing file start position instead of program.span().lo, so top-of-file directives are included.

Why this approach

SWC doesn't appear to expose a global “all comments” API in plugin mode.

This solution was selected because it fixes the actual failure mode without depending on such an API.

Other options were weaker:

  • Relying only on positional comment attachment is exactly what caused the bug.
  • Requiring users to move directives or duplicate context in code would be a workaround, not a fix.
  • Reconstructing behavior from specific AST shapes would be brittle and miss other valid directive placements.

Using source text as a fallback is more robust because directives are fundamentally lexical comments, and this approach preserves the existing fast positional path while covering the cases SWC comment attachment misses.

Validation

Added/updated E2E coverage for a TypeScript case where lingui-set appears before export type, and verified that the transform now preserves the expected context-aware ID instead of the unscoped one.

@codecov

codecov Bot commented May 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.14325% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.03%. Comparing base (88dc19a) to head (62d5ece).

Files with missing lines Patch % Lines
src/lib.rs 72.72% 9 Missing ⚠️
src/comment_directive/mod.rs 95.09% 5 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #226      +/-   ##
==========================================
+ Coverage   94.74%   95.03%   +0.28%     
==========================================
  Files           9       10       +1     
  Lines        2265     2495     +230     
==========================================
+ Hits         2146     2371     +225     
- Misses        119      124       +5     
Flag Coverage Δ
unittests 95.03% <96.14%> (+0.28%) ⬆️

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

Files with missing lines Coverage Δ
src/comment_directive/source_scanner.rs 100.00% <100.00%> (ø)
src/macro_utils.rs 89.21% <100.00%> (ø)
src/comment_directive/mod.rs 95.01% <95.09%> (ø)
src/lib.rs 91.00% <72.72%> (-1.21%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@timofei-iatsenko

Copy link
Copy Markdown
Collaborator

BTW, i think this could be solved by enabling jsc.experimental.runPluginFirst

  /**
   * Run Wasm plugins before stripping TypeScript or decorators.
   *
   * See https://github.com/swc-project/swc/issues/9132 for more details.
   */
  runPluginFirst?: boolean;

This option is poorely documented, i've found it in the PR

@timofei-iatsenko

Copy link
Copy Markdown
Collaborator

Here is few issues with this implementation:

  1. Accessing the metadata.source_map from wasm plugin whould invoke a costly transfer between plugin host and plugin itself. So this will slow down compilation.
  2. Now it's 2 ways of accessing comments, old-way used by traversing AST and looking for associated comment in SWC prepared struct and new way using a flat array of comments extracted manually from the source code.

I don't see any other solution rather to do what it's already done here - parse comments manually. Seems SWC doesn't give us any other way. But some changes should be done here:

  1. Parsing comments from directives should be guarded. There are two options i see here:
  • Pass a sourcode to the LinguiMacroFolder, and move comments parsing from process_transform to fold_module_items after if !self.has_lingui_macro_imports { guard.
  • Implement second, quick check on the source code str in the process_transform, by simply checking of presence of substring what is defined in options.macro_packages (eq @lingui/core/macro)
  1. Eliminate collect_lingui_directives, DirectiveCollector and all glue code, and just use a flat array of already prepared directives. This would match js implementation.

@mogelbrod

Copy link
Copy Markdown
Contributor Author

@timofei-iatsenko Thanks for reviewing! I pushed a commit that should address everything. Ended up with a minimal JS parser that only looks for comments, to avoid false positives like lingui-set within JS strings.

@timofei-iatsenko

Copy link
Copy Markdown
Collaborator

I added a benchmark suite for lingui cli/transforms

i'm going to validate this solution using this benchmark first.

@timofei-iatsenko timofei-iatsenko force-pushed the disappearing-directives branch from 4db6e2f to 885b7d9 Compare June 8, 2026 16:16
@timofei-iatsenko

Copy link
Copy Markdown
Collaborator

I checked implementation with benchmark and it doesn't add any significant peformance overhead, so we good to go.

I refactored/cleaned up implementation:

  • Created a convenient struct LinguiCommentDirectives with related methods
  • Changed a source code parser to be a slightly simpler, and less complicated
  • Split into files and extracted, directives related code into it's own module (subfolder)

@mogelbrod mogelbrod left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Looks great, thanks @timofei-iatsenko!

@timofei-iatsenko

Copy link
Copy Markdown
Collaborator

I added few more changes, this ready to be merged.

@timofei-iatsenko timofei-iatsenko merged commit 4e259bd into lingui:main Jun 9, 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.

2 participants