Skip to content

fix(linter): add export {} when noUnusedImports removes last import in TypeScript#8977

Merged
dyc3 merged 1 commit intobiomejs:mainfrom
FrankFMY:fix/no-unused-imports-ambient-module
Feb 10, 2026
Merged

fix(linter): add export {} when noUnusedImports removes last import in TypeScript#8977
dyc3 merged 1 commit intobiomejs:mainfrom
FrankFMY:fix/no-unused-imports-ambient-module

Conversation

@FrankFMY
Copy link
Copy Markdown
Contributor

@FrankFMY FrankFMY commented Feb 5, 2026

AI disclosure: This PR was authored with the assistance of Claude (Anthropic). Claude helped explore the codebase, analyze the issue, implement the fix, write tests, and prepare the PR.

Summary

Fixes #4888. When noUnusedImports removes the last import statement in a TypeScript file that has no other exports, the file becomes an ambient module (global scope). This fix replaces the removed import with export {} to preserve module context.

This does not apply to embedded scripts in Vue, Svelte, or Astro files, which are already in a module context.

Test Plan

  • Added issue_4888.ts test case with a TS file containing only one unused import and a declare module block
  • Verified the fix generates export {} instead of just removing the import
  • All 23 existing noUnusedImports tests continue to pass

Docs

No documentation changes needed — the rule's existing documentation covers the behavior.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 5, 2026

🦋 Changeset detected

Latest commit: 09109f8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added A-Linter Area: linter L-JavaScript Language: JavaScript and super languages labels Feb 5, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 5, 2026

Walkthrough

The noUnusedImports lint rule now preserves TypeScript module boundaries when removing imports. If removing an unused import would leave a TypeScript file with no imports or exports (and the file is not an embedded script in Vue/Svelte/Astro), the fixer replaces the removed import with an empty export clause (export {}) instead of deleting it. The change adds AST handling for module items and export syntax and includes a test covering interface-merging scenarios that would otherwise become ambient.

Possibly related PRs

Suggested reviewers

  • ematipico
  • dyc3
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main fix: adding export {} when noUnusedImports removes the last import in TypeScript files.
Linked Issues check ✅ Passed The PR fully addresses issue #4888 by implementing the fix to add export {} when removing the last import in TS files, preventing ambient module conversion.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the noUnusedImports rule fix: rule implementation, test case, and changelog entry—no extraneous modifications present.
Description check ✅ Passed The PR description clearly relates to the changeset, explaining the fix for issue #4888 regarding noUnusedImports behaviour in TypeScript files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/biome_js_analyze/src/lint/correctness/no_unused_imports.rs (1)

431-493: ⚠️ Potential issue | 🟠 Major

Double mutation conflict on parent in TypeScript imports.

When a file has a single import with leading blank lines and no siblings (triggering the blank-line-stripping path at line 423), the code removes the parent import statement and replaces the entire module with an empty one. However, the needs_export_empty logic that follows (line 452) then attempts to mutate parent again at line 478 or 483. The mutation system's "last write wins" semantics means the second operation overwrites the first, defeating the blank-line handling.

Either guard the needs_export_empty block to skip it after the blank-line path handles removal, or reorganise to evaluate needs_export_empty before the blank-line logic to decide the strategy upfront.

Copy link
Copy Markdown
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

Needs a changeset

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Feb 5, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 95 skipped benchmarks1


Comparing FrankFMY:fix/no-unused-imports-ambient-module (09109f8) with main (51c158e)2

Open in CodSpeed

Footnotes

  1. 95 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (5a341b2) during the generation of this report, so 51c158e was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

…n TS

When noUnusedImports removes the last import in a TypeScript file that
has no other exports, the file becomes an ambient module (global scope).
This replaces the removed import with export {} to preserve module
context.

Skip this behavior for embedded scripts in Vue, Svelte, and Astro files,
which are already in a module context by nature of being inside <script>
tags.
@FrankFMY FrankFMY force-pushed the fix/no-unused-imports-ambient-module branch from 9abad98 to 09109f8 Compare February 5, 2026 20:12
@FrankFMY
Copy link
Copy Markdown
Contributor Author

FrankFMY commented Feb 5, 2026

Thanks for the review! I've pushed an update that:

  1. Added a changeset as requested.
  2. Fixed the failing Vue/Svelte tests — the export {} insertion now skips embedded scripts (Vue, Svelte, Astro) by checking EmbeddingKind. These script blocks are already in a module context, so they don't need export {} to preserve module boundaries.

Regarding the double mutation concern from CodeRabbit: the blank-line-stripping path (line ~423) only triggers when there's a prev_sibling, which means there are other items in the module — in that case needs_export_empty will be false because there are sibling items. The two paths are mutually exclusive in practice.

@dyc3 dyc3 merged commit bbe0e0c into biomejs:main Feb 10, 2026
19 checks passed
@github-actions github-actions Bot mentioned this pull request Feb 10, 2026
mongolyy added a commit to mongolyy/reviewdog-action-biome that referenced this pull request Feb 14, 2026
Update test expected output files to match Biome 2.3.15 behavior.

Changes in Biome 2.3.15:
- noUnusedImports now adds "export {}" when removing the last import
  in a TypeScript file to prevent it from becoming an ambient module
  (biomejs/biome#8977)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Linter Area: linter L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

💅 noUnusedImports fix can change TS modules into ambient contexts

2 participants