Skip to content

Comments

fix(renamer): prevent renaming symbols when there no conflicts#7936

Merged
hyf0 merged 18 commits intorolldown:mainfrom
Dunqing:prevert-rename-when-possible
Jan 20, 2026
Merged

fix(renamer): prevent renaming symbols when there no conflicts#7936
hyf0 merged 18 commits intorolldown:mainfrom
Dunqing:prevert-rename-when-possible

Conversation

@Dunqing
Copy link
Collaborator

@Dunqing Dunqing commented Jan 16, 2026

Note: waiting for oxc-project/oxc#18053 to merge to unblock this (Edit: merged, and already released)

Summary

Fixes: #6586

This PR refines the symbol renaming (deconflict) logic to correctly handle both top-level symbol conflicts and nested scope shadowing. The key insight is that these are two distinct problems requiring different solutions.

Related fixes:

This PR essentially reverts all the logic implemented in the previous fixes while preserving the performance improvements introduced in #7867 (4%).


1. Top-Level Symbol Renaming

When bundling multiple modules into a single chunk, symbols with the same name need unique identifiers.

Example:

// lib.js
export const value = 1;

// other.js
export const value = 2;

// main.js
import { value } from './lib.js';
import { value as otherValue } from './other.js';
console.log(value, otherValue);

Output:

const value = 1;      // lib.js - keeps original name (entry module priority)
const value$1 = 2;    // other.js - renamed to avoid conflict
console.log(value, value$1);

Rules:

  • Entry module symbols get naming priority
  • Subsequent conflicting symbols get suffixed: $1, $2, etc.
  • Reserved names (keywords, globals like Object, Promise) are always avoided

2. Nested Scope Symbol Renaming

This handles cases where a nested binding (function parameter, catch clause, block-scoped variable) interacts with top-level symbols.

Case A: NO renaming needed - Intentional shadowing

When a nested binding shadows an import that kept its original name, no renaming is needed because JavaScript's scoping rules handle it correctly.

Example (preserve_shadowing_param_name):

// lib.js
export const Client = [];

// main.js
import { Client } from './lib.js';

// This param shadows the imported `Client`, but should NOT be renamed
// since shadowing is intentional and doesn't cause conflicts at runtime.
const Config = (Client) => {
  console.log(Client);   // → refers to parameter
};
console.log(Client);     // → refers to import

Output:

const Client = [];
const Config = (Client) => {  // Parameter keeps its name ✓
  console.log(Client);
};
console.log(Client);

Case B: Star import member references - Renaming needed

When a namespace import member (like ns.foo) is referenced inside a function, and a nested binding would capture that reference, the nested binding must be renamed.

Example (argument-treeshaking-parameter-conflict):

// dep.js
export let value = 0;
export const mutate = () => value++;

// main.js
import * as dep from './dep';

function test(mutate) {       // Parameter named 'mutate'
  dep.mutate('hello');        // Reference to dep.mutate (top-level)
}

test();
assert.strictEqual(dep.value, 1);

Output:

let value = 0;
const mutate = () => value++;

function test(mutate$1) {     // Parameter renamed to mutate$1
  mutate("hello");            // Correctly calls top-level mutate
}
test();
assert.strictEqual(value, 1);

Why renaming is needed:

  1. The namespace import dep.mutate resolves to the top-level mutate function
  2. Inside test, the parameter mutate would shadow the top-level mutate
  3. The reference dep.mutate('hello') becomes just mutate("hello") after bundling
  4. Without renaming the parameter, this call would incorrectly invoke the parameter instead of the top-level function
  5. Solution: Rename the parameter to mutate$1 so the reference correctly resolves to the top-level mutate

Case C: Named imports - Renaming needed

When a named import is renamed due to a top-level conflict, and a nested binding has the same name as the renamed import, that nested binding must be renamed to avoid capturing references.

Example (basic_scoped):

// a.js
const a = 'a.js';
export { a };

// main.js
import { a as aJs } from './a';
const a = 'main.js';              // Local 'a' takes priority (entry module)

function foo(a$1) {               // Parameter named 'a$1'
  return [a$1, a, aJs];           // References: param, local, import
}

assert.deepEqual(foo('foo'), ['foo', 'main.js', 'a.js']);

Output:

const a$1 = "a.js";               // Import renamed (conflicts with local 'a')
const a = "main.js";              // Local keeps name (entry module priority)

function foo(a$1$1) {             // Parameter renamed to a$1$1
  return [a$1$1, a, a$1];         // Correctly resolves all three references
}

assert.deepEqual(foo("foo"), ["foo", "main.js", "a.js"]);

Why renaming is needed:

  1. a from a.js is renamed to a$1 (conflicts with local a in entry module)
  2. The alias aJs resolves to the renamed a$1
  3. Inside foo, the parameter a$1 would capture the reference to aJs
  4. Solution: Rename the parameter to a$1$1 so aJs correctly resolves to the top-level a$1

Technical Note: Cases B and C use the same underlying mechanism to detect shadowing:

  1. Get the reference_id from the reference site (where the symbol is used)
  2. Use scoping.scope_ancestors(reference.scope_id()) to walk up the scope chain
  3. Check if any ancestor scope has a binding with the same name as the renamed symbol
  4. If found, rename that nested binding to avoid capture

This detection relies on oxc-project/oxc#18053 which added scope_id to Reference, enabling us to locate the exact scope where the reference occurs.


Case D: CJS wrapper parameters - Renaming needed

For CommonJS wrapped modules, nested scopes must avoid shadowing the synthetic exports and module parameters injected by the CJS wrapper.

Example:

// cjs-module.js (detected as CommonJS)
function helper() {
  const exports = {};  // Would shadow CJS wrapper's exports parameter
  return exports;
}
module.exports = helper;

Output:

var require_cjs_module = __commonJS((exports, module) => {
  function helper() {
    const exports$1 = {};  // Renamed to avoid shadowing
    return exports$1;
  }
  module.exports = helper;
});

Implementation Details

The renaming happens in two phases:

  1. add_symbol_in_root_scope - Assigns canonical names to top-level symbols, checking:

    • Is the name already used by another top-level symbol?
    • For facade symbols (external module namespaces): would it conflict with nested scopes in the entry module?
  2. NestedScopeRenamer - Renames nested bindings that would capture references:

    • rename_bindings_shadowing_star_imports (Case B): Star import member references (ns.foo)
    • rename_bindings_shadowing_named_imports (Case C): Named imports that were renamed
    • rename_bindings_shadowing_cjs_params (Case D): CJS wrapper parameter shadowing (exports, module)

Test Cases

Test Case Description
preserve_shadowing_param_name A Parameter shadows import, keeps its name (intentional)
argument-treeshaking-parameter-conflict B Namespace member ref, parameter renamed
basic_scoped C Named import renamed, nested binding renamed
cjs D CJS wrapper parameter shadowing
Rollup: preserves-catch-argument A Catch parameter shadows import, keeps its name
Rollup: class-name-conflict-2 A Class in block scope, keeps its name

Note for reviewer: Code comments and PR description are almost generated by Claude Code, but with careful self-review.

Copilot AI review requested due to automatic review settings January 16, 2026 17:10
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 PR fixes issue #6586 to prevent renaming symbols when there are no conflicts. The main improvement is in the renamer logic to avoid unnecessary symbol renaming, using a more targeted reference-based approach for handling shadowing conflicts instead of preemptively renaming all top-level symbols.

Changes:

  • Refactored renamer logic to allow entry module symbols to use original names freely, with reference-based renaming for nested bindings
  • Added reference_id tracking to MemberExprRef and MemberExprRefResolution for better conflict detection
  • Updated oxc dependency to a git branch for new semantic analysis features

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
crates/rolldown/src/utils/renamer.rs Simplified renaming logic to avoid unnecessary renames, changed entry module symbol handling
crates/rolldown/src/utils/chunk/deconflict_chunk_symbols.rs Added reference-based shadowing detection for member expressions and named imports
crates/rolldown_common/src/types/member_expr_ref*.rs Added reference_id field to track oxc reference IDs
crates/rolldown/src/ast_scanner/ Updated to capture and pass reference IDs
crates/rolldown/src/stages/link_stage/bind_imports_and_exports.rs Updated to pass reference IDs through resolution
Cargo.toml and Cargo.lock Updated oxc dependencies to unreleased git branch
Test snapshots Updated to reflect improved renaming (fewer unnecessary renames)

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

@Dunqing Dunqing force-pushed the prevert-rename-when-possible branch from 22eea64 to 436e1a2 Compare January 17, 2026 02:16
@socket-security
Copy link

socket-security bot commented Jan 17, 2026

No dependency changes detected. Learn more about Socket for GitHub.

👍 No dependency changes detected in pull request

@Dunqing Dunqing added the test: vite-tests Trigger Vite tests workflow on this PR label Jan 17, 2026
Copilot AI review requested due to automatic review settings January 17, 2026 06:39
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 30 out of 31 changed files in this pull request and generated 5 comments.


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

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI review requested due to automatic review settings January 18, 2026 14:14
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 29 out of 30 changed files in this pull request and generated 2 comments.


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

graphite-app bot pushed a commit to oxc-project/oxc that referenced this pull request Jan 19, 2026
rolldown/rolldown#7936 relies on this. You can see how Rolldown uses this at description of that PR.

Additionally, it can also simplify the use case when you want to get the scope ID where the `Reference` occurs. See follow-up PR #18059
Copilot AI review requested due to automatic review settings January 20, 2026 08:11
@Dunqing Dunqing force-pushed the prevert-rename-when-possible branch from 12d6576 to b0d70e3 Compare January 20, 2026 08:11
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 28 out of 28 changed files in this pull request and generated 2 comments.


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

Copilot AI review requested due to automatic review settings January 20, 2026 08:37
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 28 out of 28 changed files in this pull request and generated no new comments.


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

@IWANABETHATGUY
Copy link
Member

You said

Snapshot Changes
The optimization produces slightly different (but equally correct) variable names:

Avoids ugly $1$1 double-suffix patterns
Names are chosen to avoid nested scope conflicts upfront

In pr #7867, but the test case seems regressed

@Dunqing
Copy link
Collaborator Author

Dunqing commented Jan 20, 2026

You said

Snapshot Changes
The optimization produces slightly different (but equally correct) variable names:

Avoids ugly $1$1 double-suffix patterns
Names are chosen to avoid nested scope conflicts upfront

In pr #7867, but the test case seems regressed

Yes, due to the prioritized internal binding being renamed (previously, it was the top-level binding that was renamed first), this has reverted to the state before that PR. I also checked how Rollup handles this case, and it turns out it is the same. See the Rollup REPL

Additionally, avoiding ugly $1$1 is still possible, but not worthwhile, as we can try to normalize the name a$1 to a, and then re-add the suffix to check if a$1, a$2, and a$3 can be used. However, this would cause a lot of string allocation.

@hyf0 hyf0 enabled auto-merge (squash) January 20, 2026 11:18
@netlify
Copy link

netlify bot commented Jan 20, 2026

Deploy Preview for rolldown-rs canceled.

Name Link
🔨 Latest commit f503f9c
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/696f649670724e0008a097b8

@hyf0 hyf0 merged commit 62af65e into rolldown:main Jan 20, 2026
35 checks passed
@github-actions github-actions bot mentioned this pull request Jan 22, 2026
shulaoda pushed a commit that referenced this pull request Jan 22, 2026
## [1.0.0-rc.1] - 2026-01-22

### 🚀 Features

- debug_info: add facade chunk elimination reason (#7980) by @IWANABETHATGUY
- support lazy barrel optimization (#7933) by @shulaoda
- add `experimental.lazyBarrel` option (#7908) by @shulaoda
- skip unused external modules from IIFE parameter list (#7978) by @sapphi-red
- add custom panic hook for better crash reporting (#7752) by @shulaoda
- treeshake: add `invalidImportSideEffects` option (#7958) by @shulaoda
- merge allow-extension emitted chunks (#7940) by @IWANABETHATGUY
- nativeMagicString generateMap (#7944) by @IWANABETHATGUY
- Include meta.magicString in RenderChunkMeta (#7943) by @IWANABETHATGUY
- debug_info: add debug info for eliminated facade chunks (#7946) by @IWANABETHATGUY
- stablize `strictExecutionOrder` and move to `output.strictExecutionOrder` from `experimental.strictExecutionOrder` (#7901) by @sapphi-red
- add documentation link to require() error message (#7898) by @Copilot
- add `codeSplitting: boolean` and deprecate `inlineDynamicImports` (#7870) by @hyf0
- dev: change lazy module URL to `/@vite/lazy` from `/lazy` (#7884) by @sapphi-red

### 🐛 Bug Fixes

- transform JS files containing `</script>` to escape template literals (#7987) by @IWANABETHATGUY
- apply avoid-breaking-exported-api = false to clippy.toml and fix clippy errors (#7982) by @Boshen
- pass `kind` from `this.resolve` (#7981) by @sapphi-red
- rolldown_plugin_vite_resolve: ignore yarn resolution errors and fallback to other resolvers (#7968) by @sapphi-red
- renamer: prevent renaming symbols when there no conflicts (#7936) by @Dunqing
- correct minifyInterExports when emitted chunk got merged (#7941) by @IWANABETHATGUY
- deduplicate entry points when module is both emitted and dynamically imported (#7885) by @IWANABETHATGUY
- dev: add `@vite-ignore` to lazy compilation proxy module import (#7883) by @sapphi-red

### 🚜 Refactor

- rust: enable clippy nursery lint group (#8002) by @Boshen
- rust: fix inconsistent_struct_constructor clippy lint (#7999) by @Boshen
- rust: fix needless_pass_by_ref_mut clippy lint (#7994) by @Boshen
- rust: fix unnecessary_wraps clippy lint (#7993) by @Boshen
- rust: fix enum_variant_names clippy lint (#7992) by @Boshen
- fix single_match clippy lint (#7997) by @Boshen
- rust: fix redundant_clone clippy lint (#7996) by @Boshen
- rust: rename CJS to Cjs to follow upper_case_acronyms lint (#7991) by @Boshen
- rust: remove unnecessary Box wrapper around Vec in MemberExprRef (#7990) by @Boshen
- import_record: make resolved_module optional (#7907) by @shulaoda
- remove unnecessary `.parse` (#7966) by @sapphi-red
- remove unused `ImportRecordMeta::IsPlainImport` (#7948) by @shulaoda
- proper set chunk meta (#7939) by @IWANABETHATGUY
- module_loader: remove `try_spawn_with_cache` (#7920) by @shulaoda
- link_stage: simplify `ImportStatus::NoMatch` to unit variant (#7909) by @shulaoda
- improve global scope symbol reservation in chunk deconfliction (#7906) by @IWANABETHATGUY
- simplify ast unwrapping in generate stage (#7900) by @IWANABETHATGUY
- generate_stage: optimize cross-chunk imports computation (#7889) by @shulaoda
- link_stage: move runtime require logic into match branch (#7892) by @shulaoda
- link_stage: simplify runtime require reference conditions (#7891) by @shulaoda
- link_stage: inline and simplify external dynamic import check (#7890) by @shulaoda
- generate_stage: simplify external module import collection logic (#7887) by @shulaoda
- avoid redundant module lookup in TLA computation (#7886) by @shulaoda
- dev: `devEngine.compileEntry` does not return null (#7882) by @sapphi-red
- dev: fix type errors for test HMR runtime (#7881) by @sapphi-red
- dev: move `clientId` property to `DevRuntime` base class (#7880) by @sapphi-red
- dev: generate client id in browser (#7878) by @hyf0

### 📚 Documentation

- apis: organize hook filters documentation and add composable filters section (#8003) by @sapphi-red
- update `vitepress-plugin-group-icons` (#7947) by @yuyinws
- add in-depth documentation for lazy barrel optimization (#7969) by @shulaoda
- bump theme & update activeMatch for reference (#7963) by @mdong1909
- mark `build()` API as experimental (#7954) by @sapphi-red
- enhance search functionality with improved scoring and filtering logic (#7935) by @hyf0
- add minor comments to multiple types (#7930) by @sapphi-red
- refactor advanedChunks related content to adapt manual code splitting concept (#7925) by @hyf0
- apis: add content to Bundler API page (#7926) by @sapphi-red
- apis: restructure plugin API related docs (#7924) by @sapphi-red
- add plugin API docs (#7923) by @sapphi-red
- apis: add docs to important APIs (#7913) by @sapphi-red
- move the important APIs to the top of the sidebar (#7912) by @sapphi-red
- apis: add more content to CLI documentation (#7911) by @sapphi-red
- apis: generate CLI docs from --help output (#7910) by @sapphi-red
- add fathom analytics (#7896) by @mdong1909

### ⚡ Performance

- use u32 for string indices in string_wizard and rolldown to reduce memory usage (#7989) by @IWANABETHATGUY
- rust: remove all usages of `with_scope_tree_child_ids(true)` for `SemanticBuilder` (#7995) by @Dunqing
- renamer: skip unnecessary nested scope symbol processing (#7899) by @Dunqing
- module_loader: use ArcStr for importer_id to avoid string copy (#7922) by @shulaoda
- module_loader: defer `ModuleTaskOwner` construction until needed (#7921) by @shulaoda
- renamer: optimize symbol renaming by eliminating `rename_non_root_symbol` pass (#7867) by @Dunqing

### 🧪 Testing

- add lazy barrel optimization test cases (#7967) by @shulaoda

### ⚙️ Miscellaneous Tasks

- remove lazy barrel option (#8010) by @shulaoda
- mark watch API as experimental (#8004) by @sapphi-red
- deps: update dependency lodash-es to v4.17.23 [security] (#8001) by @renovate[bot]
- git ignore zed local config (#7988) by @IWANABETHATGUY
- setup publint for published packages (#7972) by @Copilot
- enable `tagged_template_transform ` uncondionally (#7975) by @IWANABETHATGUY
- deps: update oxc to v0.110.0 (#7964) by @renovate[bot]
- deps: update oxc apps (#7962) by @renovate[bot]
- ai: add upgrade-oxc Claude skill (#7957) by @Boshen
- deps: update rollup submodule for tests to v4.55.2 (#7959) by @sapphi-red
- deps: update test262 submodule for tests (#7960) by @sapphi-red
- deps: update crate-ci/typos action to v1.42.1 (#7961) by @renovate[bot]
- deps: update rust crates (#7951) by @renovate[bot]
- deps: update npm packages (#7953) by @renovate[bot]
- deps: update github-actions (#7952) by @renovate[bot]
- deps: update npm packages (#7950) by @renovate[bot]
- format magic-string test before write to disk (#7945) by @IWANABETHATGUY
- deps: update dependency rolldown-plugin-dts to ^0.21.0 (#7915) by @renovate[bot]
- deps: update dependency oxlint-tsgolint to v0.11.1 (#7914) by @renovate[bot]
- deps: update dependency diff to v8.0.3 [security] (#7904) by @renovate[bot]
- remove outdated TODO comment in `collect_depended_symbols` (#7888) by @shulaoda
- deps: update oxc resolver to v11.16.3 (#7876) by @renovate[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test: vite-tests Trigger Vite tests workflow on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Unnecessary variable renaming

3 participants