Skip to content

refactor(es/typescript): Run typescript transform in two passes#11532

Merged
kdy1 merged 6 commits intomainfrom
kdy1/merge-ts
Feb 6, 2026
Merged

refactor(es/typescript): Run typescript transform in two passes#11532
kdy1 merged 6 commits intomainfrom
kdy1/merge-ts

Conversation

@kdy1
Copy link
Member

@kdy1 kdy1 commented Feb 6, 2026

Summary

  • reworked swc_ecma_transforms_typescript to run in two passes: one semantic analysis pass and one transform pass
  • added semantic.rs to collect usage/type-value/export/enum information in a single analysis traversal
  • updated the transform pass to consume semantic info and perform strip/runtime transformations in one traversal
  • rewired TypeScript entrypoints to use the new pipeline

Validation

  • cargo fmt --all
  • cargo test -p swc_ecma_transforms_typescript -p swc_ecma_transforms_react -p swc_ecma_transforms
  • cargo test -p swc --test projects --test tsc

@kdy1 kdy1 requested a review from a team as a code owner February 6, 2026 06:11
Copilot AI review requested due to automatic review settings February 6, 2026 06:11
@changeset-bot
Copy link

changeset-bot bot commented Feb 6, 2026

🦋 Changeset detected

Latest commit: b96fab7

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

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

@kdy1 kdy1 changed the title refactor(ts): run typescript transform in two passes refactor(es/typescript): Run typescript transform in two passes Feb 6, 2026
@kdy1 kdy1 added this to the Planned milestone Feb 6, 2026
@claude

This comment has been minimized.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request refactors the TypeScript transform to use a two-pass architecture, significantly improving code organization and maintainability. The PR replaces the previous interleaved approach with:

Pass 1 (Semantic Analysis): A new semantic.rs module traverses the AST once to collect:

  • Identifier usage information
  • Type vs value declarations
  • Export bindings
  • Enum records and constant enum tracking

Pass 2 (Transform): The existing transform.rs now consumes the semantic information and performs all stripping and runtime transformations in a single traversal, removing the need for the old strip_import_export module.

Changes:

  • Introduced semantic.rs with analyze_program() for semantic analysis
  • Removed strip_import_export.rs module (replaced by semantic analysis)
  • Refactored transform.rs to consume semantic info and consolidate transform logic
  • Updated typescript.rs entrypoint to orchestrate the two-pass pipeline

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
crates/swc_ecma_transforms_typescript/src/semantic.rs New file implementing semantic analysis pass that collects usage, type/value distinctions, export bindings, and enum information
crates/swc_ecma_transforms_typescript/src/transform.rs Refactored to consume semantic info, removed Visit impl, added stripping logic with semantic info, duplicated helper traits/functions from strip_type.rs
crates/swc_ecma_transforms_typescript/src/typescript.rs Updated entrypoint to call semantic analysis before transform, passing semantic info through the pipeline
crates/swc_ecma_transforms_typescript/src/lib.rs Module declaration changed from strip_import_export to semantic

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

Comment on lines 1868 to 1956
trait IsConcrete {
fn is_concrete(&self) -> bool;
}

impl IsConcrete for TsModuleDecl {
fn is_concrete(&self) -> bool {
self.body
.as_ref()
.map(|body| body.is_concrete())
.unwrap_or_default()
}
}

impl IsConcrete for TsNamespaceBody {
fn is_concrete(&self) -> bool {
match self {
Self::TsModuleBlock(ts_module_block) => {
ts_module_block.body.iter().any(|item| item.is_concrete())
}
Self::TsNamespaceDecl(ts_namespace_decl) => ts_namespace_decl.body.is_concrete(),
#[cfg(swc_ast_unknown)]
_ => panic!("unable to access unknown nodes"),
}
}
}

impl IsConcrete for ModuleItem {
fn is_concrete(&self) -> bool {
match self {
Self::ModuleDecl(module_decl) => module_decl.is_concrete(),
Self::Stmt(stmt) => stmt.is_concrete(),
#[cfg(swc_ast_unknown)]
_ => panic!("unable to access unknown nodes"),
}
}
}

impl IsConcrete for ModuleDecl {
fn is_concrete(&self) -> bool {
match self {
Self::Import(import_decl) => !import_decl.type_only,
Self::ExportDecl(export_decl) => export_decl.decl.is_concrete(),
Self::ExportNamed(named_export) => !named_export.type_only,
Self::ExportDefaultDecl(export_default_decl) => export_default_decl.decl.is_concrete(),
Self::ExportDefaultExpr(..) => true,
Self::ExportAll(export_all) => !export_all.type_only,
Self::TsImportEquals(ts_import_equals) => !ts_import_equals.is_type_only,
Self::TsExportAssignment(..) => true,
Self::TsNamespaceExport(..) => false,
#[cfg(swc_ast_unknown)]
_ => panic!("unable to access unknown nodes"),
}
}
}

impl IsConcrete for Decl {
fn is_concrete(&self) -> bool {
match self {
Self::TsInterface(..) | Self::TsTypeAlias(..) => false,
Self::Fn(function_decl) => function_decl.function.body.is_some(),
Self::Class(..) | Self::Var(..) | Self::Using(..) | Self::TsEnum(..) => true,
Self::TsModule(ts_module) => ts_module.is_concrete(),
#[cfg(swc_ast_unknown)]
_ => panic!("unable to access unknown nodes"),
}
}
}

impl IsConcrete for DefaultDecl {
fn is_concrete(&self) -> bool {
match self {
Self::Class(..) => true,
Self::Fn(function_expr) => function_expr.function.body.is_some(),
Self::TsInterfaceDecl(..) => false,
#[cfg(swc_ast_unknown)]
_ => panic!("unable to access unknown nodes"),
}
}
}

impl IsConcrete for Stmt {
fn is_concrete(&self) -> bool {
match self {
Self::Empty(..) => false,
Self::Decl(decl) => decl.is_concrete(),
_ => true,
}
}
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The IsConcrete trait and its implementations are duplicated from strip_type.rs. This creates maintenance burden as changes to one need to be synchronized with the other. Consider importing IsConcrete from strip_type module instead, or moving it to a shared location if both modules need it.

Copilot uses AI. Check for mistakes.
Comment on lines 1958 to 1976
trait IsDeclare {
fn is_declare(&self) -> bool;
}

impl IsDeclare for Decl {
fn is_declare(&self) -> bool {
match self {
Decl::Class(class_decl) => class_decl.declare,
Decl::Fn(function_decl) => function_decl.declare,
Decl::Var(var_decl) => var_decl.declare,
Decl::Using(..) => false,
Decl::TsInterface(..) | Decl::TsTypeAlias(..) => true,
Decl::TsEnum(ts_enum_decl) => ts_enum_decl.declare,
Decl::TsModule(ts_module_decl) => ts_module_decl.declare || ts_module_decl.global,
#[cfg(swc_ast_unknown)]
_ => panic!("unable to access unknown nodes"),
}
}
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The IsDeclare trait and its implementation are duplicated from strip_type.rs. This creates maintenance burden as changes to one need to be synchronized with the other. Consider importing IsDeclare from strip_type module instead, or moving it to a shared location if both modules need it.

Copilot uses AI. Check for mistakes.
Comment on lines 1827 to 1836
fn get_module_ident(ts_entity_name: &TsEntityName) -> &Ident {
match ts_entity_name {
TsEntityName::TsQualifiedName(ts_qualified_name) => {
get_module_ident(&ts_qualified_name.left)
}
TsEntityName::Ident(ident) => ident,
#[cfg(swc_ast_unknown)]
_ => panic!("unable to access unknown nodes"),
}
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The get_module_ident helper function is duplicated in semantic.rs (line 505). This creates maintenance burden. Consider moving this function to a shared utility module to avoid duplication.

Copilot uses AI. Check for mistakes.
Comment on lines 1838 to 1866
fn should_retain_module_item(module_item: &ModuleItem, in_namespace: bool) -> bool {
match module_item {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => {
// Keep `export declare var` in namespace blocks for downstream transforms.
if in_namespace && export_decl.decl.is_var() {
return true;
}

should_retain_decl(&export_decl.decl)
}
ModuleItem::Stmt(stmt) => should_retain_stmt(stmt),
_ => module_item.is_concrete(),
}
}

fn should_retain_stmt(stmt: &Stmt) -> bool {
match stmt {
Stmt::Decl(decl) => should_retain_decl(decl),
_ => stmt.is_concrete(),
}
}

fn should_retain_decl(decl: &Decl) -> bool {
if decl.is_declare() {
return false;
}

decl.is_concrete()
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The should_retain_module_item, should_retain_stmt, and should_retain_decl helper functions are duplicated from strip_type.rs with nearly identical implementations. This creates maintenance burden as changes to one need to be synchronized with the other. Consider importing these functions from strip_type module or consolidating them in a shared location.

Copilot uses AI. Check for mistakes.
}
}

impl Visit for NamespaceUsageCollect {
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The NamespaceUsageCollect Visit implementation is missing the noop_visit_type macro. This means it will visit TypeScript type annotations when it should skip them, potentially collecting identifiers from type positions that shouldn't be considered as value usages. Add noop_visit_type!(); as the first line in the Visit implementation for consistency with other visitors in this module.

Suggested change
impl Visit for NamespaceUsageCollect {
impl Visit for NamespaceUsageCollect {
noop_visit_type!();

Copilot uses AI. Check for mistakes.
kodiakhq[bot]
kodiakhq bot previously approved these changes Feb 6, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

Binary Sizes

File Size
swc.linux-x64-gnu.node 28M (28595912 bytes)

Commit: 56b2024

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 6, 2026

CodSpeed Performance Report

Merging this PR will improve performance by 10.49%

Comparing kdy1/merge-ts (b96fab7) with main (aa5a9ac)

Summary

⚡ 6 improved benchmarks
✅ 178 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
es/oxc/benches/assets/table.tsx/sourceMap=false/reactDev=false 5.1 ms 5 ms +2%
es/oxc/benches/assets/parser.ts/sourceMap=false/reactDev=true 52.5 ms 51.2 ms +2.46%
es/oxc/benches/assets/renderer.ts/sourceMap=false/reactDev=true 9.1 ms 8.9 ms +2.3%
es/oxc/benches/assets/renderer.ts/sourceMap=false/reactDev=false 9.1 ms 8.9 ms +2.29%
es/oxc/benches/assets/parser.ts/sourceMap=false/reactDev=false 52.5 ms 51.3 ms +2.3%
es/transform/baseline/common_typescript 388.6 µs 351.8 µs +10.49%

kodiakhq[bot]
kodiakhq bot previously approved these changes Feb 6, 2026
@claude

This comment has been minimized.

Copilot AI review requested due to automatic review settings February 6, 2026 06:58
@kdy1 kdy1 requested a review from a team as a code owner February 6, 2026 06:58
@kdy1 kdy1 enabled auto-merge (squash) February 6, 2026 06:59
@socket-security
Copy link

socket-security bot commented Feb 6, 2026

@socket-security
Copy link

socket-security bot commented Feb 6, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn Critical
Critical CVE: npm cipher-base is missing type checks, leading to hash rewind and passing on crafted data

CVE: GHSA-cpq7-6gpm-g9rc cipher-base is missing type checks, leading to hash rewind and passing on crafted data (CRITICAL)

Affected versions: < 1.0.5

Patched version: 1.0.5

From: ?npm/cipher-base@1.0.4

ℹ Read more on: This package | This alert | What is a critical CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known critical CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/cipher-base@1.0.4. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm buffer is 96.0% likely obfuscated

Confidence: 0.96

Location: Package overview

From: ?npm/buffer@4.9.2

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/buffer@4.9.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.


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

Comment on lines +1818 to +1932
#[derive(Debug, Default)]
struct NamespaceUsageCollect {
id_usage: FxHashSet<Id>,
import_chain: FxHashMap<Id, Id>,
}

impl NamespaceUsageCollect {
fn has_usage(&self, id: &Id) -> bool {
self.id_usage.contains(id)
}

fn analyze_import_chain(&mut self) {
if self.import_chain.is_empty() {
return;
}

let mut new_usage = FxHashSet::default();
for id in &self.id_usage {
let mut next = self.import_chain.remove(id);
while let Some(id) = next {
next = self.import_chain.remove(&id);
new_usage.insert(id);
}

if self.import_chain.is_empty() {
break;
}
}

self.id_usage.extend(new_usage);
}
}

impl Visit for NamespaceUsageCollect {
fn visit_ident(&mut self, node: &Ident) {
self.id_usage.insert(node.to_id());
}

fn visit_expr(&mut self, node: &Expr) {
maybe_grow_default(|| node.visit_children_with(self));
}

fn visit_binding_ident(&mut self, _: &BindingIdent) {
// skip
}

fn visit_fn_decl(&mut self, node: &FnDecl) {
// skip function identifier
node.function.visit_with(self);
}

fn visit_fn_expr(&mut self, node: &FnExpr) {
// skip function identifier
node.function.visit_with(self);
}

fn visit_class_decl(&mut self, node: &ClassDecl) {
// skip class identifier
node.class.visit_with(self);
}

fn visit_class_expr(&mut self, node: &ClassExpr) {
// skip class identifier
node.class.visit_with(self);
}

fn visit_import_decl(&mut self, _: &ImportDecl) {
// skip
}

fn visit_ts_import_equals_decl(&mut self, node: &TsImportEqualsDecl) {
if node.is_type_only {
return;
}

let TsModuleRef::TsEntityName(ts_entity_name) = &node.module_ref else {
return;
};

let id = get_module_ident(ts_entity_name);

if node.is_export {
id.visit_with(self);
node.id.visit_with(self);
return;
}

self.import_chain.insert(node.id.to_id(), id.to_id());
}

fn visit_export_named_specifier(&mut self, node: &ExportNamedSpecifier) {
if node.is_type_only {
return;
}

node.orig.visit_with(self);
}

fn visit_named_export(&mut self, node: &NamedExport) {
if node.type_only || node.src.is_some() {
return;
}

node.visit_children_with(self);
}

fn visit_jsx_element_name(&mut self, node: &JSXElementName) {
if matches!(node, JSXElementName::Ident(i) if i.sym.starts_with(|c: char| c.is_ascii_lowercase()))
{
return;
}

node.visit_children_with(self);
}
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

There is significant code duplication between the NamespaceUsageCollect struct (lines 1818-1932) and the SemanticAnalyzer in semantic.rs. Both implement nearly identical visit methods for tracking identifier usage (visit_ident, visit_binding_ident, visit_fn_decl, visit_fn_expr, visit_class_decl, visit_class_expr, visit_import_decl, visit_ts_import_equals_decl, visit_export_named_specifier, visit_named_export, visit_jsx_element_name) and both have the same analyze_import_chain logic. Consider extracting a shared usage collection trait or struct to reduce this duplication and improve maintainability.

Copilot uses AI. Check for mistakes.
@@ -2,11 +2,12 @@
#![allow(clippy::vec_box)]
#![allow(clippy::mutable_key_type)]

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The public function strip_type() has been removed from the API by removing it from the re-exports in lib.rs. This is a breaking change for any external code that was calling strip_type() directly. The changeset marks this as a "patch" version change, but removing a public API typically requires at least a "minor" version bump in semantic versioning. Consider whether this API removal is intentional and documented, or if the strip_type function should still be exposed (possibly as a thin wrapper around the new implementation).

Suggested change
pub use self::typescript::strip_type;

Copilot uses AI. Check for mistakes.
}
}

/// ## This Module will transform all TypeScript specific synatx
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Spelling error: "synatx" should be "syntax".

Suggested change
/// ## This Module will transform all TypeScript specific synatx
/// ## This Module will transform all TypeScript specific syntax

Copilot uses AI. Check for mistakes.
kodiakhq[bot]
kodiakhq bot previously approved these changes Feb 6, 2026
@kdy1
Copy link
Member Author

kdy1 commented Feb 6, 2026

@claude Fix cargo clippy. Run cargo fmt --all after then

@claude

This comment has been minimized.

Co-authored-by: Donny/강동윤 <kdy1@users.noreply.github.com>
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ kdy1
❌ github-actions[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

1 similar comment
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ kdy1
❌ github-actions[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@claude
Copy link
Contributor

claude bot commented Feb 6, 2026

PR Review: refactor(es/typescript): Run typescript transform in two passes

Summary

This PR consolidates three separate AST passes (StripImportExport + StripType + Transform's Visit impl) into a clean two-pass architecture: a read-only semantic analysis pass (semantic.rs) followed by a single mutating transform pass (transform.rs). The shared utilities are properly extracted into retain.rs (type-stripping retention logic) and shared.rs (helper functions).

Architecture

The two-pass design is a solid improvement:

  1. Pass 1 (semantic.rs): Read-only Visit that collects usage info, type-vs-value classifications, exported bindings, enum records, and const-enum tracking into a single SemanticInfo struct.
  2. Pass 2 (transform.rs): VisitMut that consumes SemanticInfo and performs all type stripping, import/export filtering, and runtime transformations in one traversal.

This reduces the number of full-tree traversals and improves cache locality. The SemanticInfo struct provides a clean interface between the two passes.

Code Quality

Positives:

  • Good separation of concerns — retain.rs for retention predicates, shared.rs for shared helpers, semantic.rs for analysis, transform.rs for mutation.
  • The IsConcrete / IsDeclare traits are properly extracted into retain.rs rather than being duplicated.
  • enum_member_id_atom() and get_module_ident() are correctly shared via shared.rs.
  • The SemanticInfo struct has clean, well-named accessor methods (has_usage, has_value, has_pure_type).

Minor concerns:

  1. NamespaceUsageCollect duplication (transform.rs:1815-1933): This struct's Visit implementation is nearly identical to SemanticAnalyzer's usage collection logic in semantic.rs. The same visit_ident, visit_binding_ident, visit_fn_decl, visit_fn_expr, visit_class_decl, visit_class_expr, visit_import_decl, visit_ts_import_equals_decl, visit_jsx_element_name, and analyze_import_chain methods are duplicated. This is understandable if the namespace-level analysis must happen lazily during transform (since namespace bodies may be encountered dynamically), but it would be worth adding a comment explaining why pre-computation during the semantic pass isn't feasible, or exploring whether a shared visitor trait/macro could reduce the duplication.

  2. member.clone() in visit_ts_enum_decl (semantic.rs:698): Self::transform_ts_enum_member(member.clone(), ...) clones each TsEnumMember to pass by value. In the old code (transform.rs), this was also done via .clone(), so it's not a regression, but it would be more performant to take a reference-based approach if EnumValueComputer::compute could accept a borrowed Box<Expr> instead.

  3. #[allow(clippy::too_many_arguments)] (transform.rs:77): The transform() function now takes 7 parameters. While the suppression works, a builder pattern or config struct grouping import_export_assign_config, ts_enum_is_mutable, verbatim_module_syntax, native_class_properties would be cleaner. Not a blocker.

  4. lib.rs re-export change: The old pub use self::{strip_type::*, typescript::*}; is replaced with pub use self::typescript::*;. This removes the public re-export of strip_type() and IsConcrete. If any downstream crates depend on swc_ecma_transforms_typescript::strip_type() or swc_ecma_transforms_typescript::IsConcrete, this is a breaking change. Since the changeset marks this as patch, verify no public API was removed. The IsConcrete trait was pub(crate) so that's fine, but strip_type() was pub fn strip_type() -> impl VisitMut — was this used externally?

Potential Issues

  1. Double-retain on visit_mut_module_items (transform.rs:159): The method now calls node.retain(|item| should_retain_module_item(...)) immediately before node.retain_mut(...). Both closures iterate the full items list sequentially. While this is correct (the first filters type-only items, the second handles visit-and-retain logic), a single retain_mut pass could do both checks together for better performance. This is minor since module item counts are typically small.

  2. visit_mut_stmts is missing: The old StripType had visit_mut_stmts that cleaned up dummy empty statements: n.retain(|s| !matches!(s, Stmt::Empty(e) if e.span.is_dummy())). The new Transform adds this to visit_mut_constructor (via node.retain(...) at the end), but doesn't have a general visit_mut_stmts. If any non-constructor statement block contains statements that get replaced with dummy Stmt::Empty by visit_mut_stmt's node.take(), those won't be cleaned up. The old code cleaned them up via visit_mut_stmts on any Vec<Stmt>. Verify this doesn't cause regressions where empty statements leak into output.

  3. Ordering of strip vs transform in visit_mut_module (transform.rs:130-133): strip_module_items_with_semantic runs before visit_mut_for_ts_import_export and before visit_mut_children_with. This means import/export stripping happens before the transform walk. In the old code, StripImportExport ran as a separate pass before StripType, so the ordering is preserved. Just want to confirm this is intentional and correct.

Test Coverage

The PR description lists running:

  • cargo test -p swc_ecma_transforms_typescript -p swc_ecma_transforms_react -p swc_ecma_transforms
  • cargo test -p swc --test projects --test tsc

This covers the relevant test suites. No test files are modified, which indicates this is a behavior-preserving refactor — good. CI checks are still pending at time of review.

Security

No security concerns. This is a pure refactoring of AST transformation logic with no I/O, network access, or user-facing input handling changes.

Verdict

This is a clean architectural improvement. The main actionable items are:

  1. Verify visit_mut_stmts removal doesn't cause empty statement leaks in non-constructor blocks.
  2. Verify the strip_type() public API removal doesn't break downstream consumers (or bump to minor version if it does).
  3. Consider adding a comment on NamespaceUsageCollect explaining why it can't reuse SemanticAnalyzer logic.

@kdy1 kdy1 merged commit b069558 into main Feb 6, 2026
187 of 188 checks passed
@kdy1 kdy1 deleted the kdy1/merge-ts branch February 6, 2026 07:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants