Skip to content

Comments

fix: improve variable renaming to avoid unnecessary shadowing in nested scopes#7859

Merged
graphite-app[bot] merged 1 commit intomainfrom
01-13-fix_6586
Jan 13, 2026
Merged

fix: improve variable renaming to avoid unnecessary shadowing in nested scopes#7859
graphite-app[bot] merged 1 commit intomainfrom
01-13-fix_6586

Conversation

@IWANABETHATGUY
Copy link
Member

@IWANABETHATGUY IWANABETHATGUY commented Jan 13, 2026

closed #6586

Copy link
Member Author


How to use the Graphite Merge Queue

Add the label graphite: merge-when-ready to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@netlify
Copy link

netlify bot commented Jan 13, 2026

Deploy Preview for rolldown-rs ready!

Name Link
🔨 Latest commit 10c035d
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6965f495cdf736000865799b
😎 Deploy Preview https://deploy-preview-7859--rolldown-rs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Jan 13, 2026

Deploy Preview for rolldown-rs canceled.

Name Link
🔨 Latest commit 12ae275
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/69662b6ce7f2ba000845f8d0

@IWANABETHATGUY IWANABETHATGUY force-pushed the 01-13-fix_6586 branch 4 times, most recently from 5021202 to 6767cb3 Compare January 13, 2026 08:39
@IWANABETHATGUY IWANABETHATGUY changed the title fix: 6586 refactor: improve variable renaming to avoid unnecessary shadowing in nested scopes Jan 13, 2026
@IWANABETHATGUY IWANABETHATGUY marked this pull request as ready for review January 13, 2026 09:00
Copilot AI review requested due to automatic review settings January 13, 2026 09:00
@IWANABETHATGUY IWANABETHATGUY requested review from hyf0, sapphi-red and shulaoda and removed request for Copilot January 13, 2026 09:00
@IWANABETHATGUY IWANABETHATGUY changed the title refactor: improve variable renaming to avoid unnecessary shadowing in nested scopes fix: improve variable renaming to avoid unnecessary shadowing in nested scopes Jan 13, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Jan 13, 2026

Merging this PR will not alter performance

✅ 8 untouched benchmarks


Comparing 01-13-fix_6586 (12ae275) with main (c37bd72)1

Open in CodSpeed

Footnotes

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

@github-actions
Copy link
Contributor

github-actions bot commented Jan 13, 2026

Benchmarks Rust

group                                                        pr                                     target
-----                                                        --                                     ------
bundle/bundle@multi-duplicated-top-level-symbol              1.02     73.5±2.19ms        ? ?/sec    1.00     72.0±2.56ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-sourcemap    1.02     80.8±2.82ms        ? ?/sec    1.00     79.4±3.90ms        ? ?/sec
bundle/bundle@rome_ts                                        1.03    107.3±3.90ms        ? ?/sec    1.00    104.5±2.80ms        ? ?/sec
bundle/bundle@rome_ts-sourcemap                              1.02    117.9±2.09ms        ? ?/sec    1.00    115.5±1.89ms        ? ?/sec
bundle/bundle@threejs                                        1.02     39.2±2.33ms        ? ?/sec    1.00     38.5±0.90ms        ? ?/sec
bundle/bundle@threejs-sourcemap                              1.01     43.8±1.31ms        ? ?/sec    1.00     43.2±1.01ms        ? ?/sec
bundle/bundle@threejs10x                                     1.01    387.6±6.00ms        ? ?/sec    1.00    385.0±6.90ms        ? ?/sec
bundle/bundle@threejs10x-sourcemap                           1.02    450.6±7.45ms        ? ?/sec    1.00    442.5±7.87ms        ? ?/sec
scan/scan@rome_ts                                            1.00     81.8±1.83ms        ? ?/sec    1.02     83.1±2.07ms        ? ?/sec
scan/scan@threejs                                            1.00     28.6±1.59ms        ? ?/sec    1.00     28.6±1.69ms        ? ?/sec
scan/scan@threejs10x                                         1.00    284.5±4.44ms        ? ?/sec    1.01    287.0±5.71ms        ? ?/sec

@IWANABETHATGUY IWANABETHATGUY marked this pull request as draft January 13, 2026 09:28
@IWANABETHATGUY IWANABETHATGUY marked this pull request as ready for review January 13, 2026 10:16
Copilot AI review requested due to automatic review settings January 13, 2026 10:16
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 by improving the variable renaming logic to avoid unnecessary shadowing in nested scopes. The fix distinguishes between three cases: cross-module symbols (always avoid shadowing), same-module symbols that were renamed (avoid shadowing to prevent accidental collisions), and same-module symbols that weren't renamed (allow shadowing to preserve JavaScript semantics).

Changes:

  • Enhanced the renamer to track whether symbols were renamed and their owning module
  • Implemented smarter shadowing rules for nested scopes based on symbol origin and rename status
  • Added comprehensive test cases covering the different shadowing scenarios

Reviewed changes

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

Show a summary per file
File Description
crates/rolldown/src/utils/renamer.rs Core implementation: Added CanonicalNameInfo struct and updated shadowing logic to preserve natural scoping while preventing accidental collisions
crates/rolldown/tests/rolldown/topics/deconflict/issue_6586/* Test case for natural shadowing preservation (parameter shadowing top-level variable)
crates/rolldown/tests/rolldown/topics/deconflict/issue_6586_generated_name_conflict/* Test case for preventing shadowing of generated names from cross-module imports
crates/rolldown/tests/rolldown/topics/deconflict/issue_same_module_shadowing/* Test case for preventing shadowing of same-module renamed symbols
Various snapshot files Updated test expectations reflecting improved renaming behavior across existing tests

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

@graphite-app
Copy link
Contributor

graphite-app bot commented Jan 13, 2026

Merge activity

  • Jan 13, 10:57 AM UTC: IWANABETHATGUY added this pull request to the Graphite merge queue.
  • Jan 13, 11:07 AM UTC: The Graphite merge queue couldn't merge this PR because it was not satisfying all requirements (Failed CI: 'node-test-macos / Node Test').
  • Jan 13, 11:37 AM UTC: IWANABETHATGUY added this pull request to the Graphite merge queue.
  • Jan 13, 11:38 AM UTC: Merged by the Graphite merge queue.

graphite-app bot pushed a commit that referenced this pull request Jan 13, 2026
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 63 out of 63 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.

@graphite-app graphite-app bot merged commit 12ae275 into main Jan 13, 2026
35 checks passed
@graphite-app graphite-app bot deleted the 01-13-fix_6586 branch January 13, 2026 11:38
This was referenced Jan 14, 2026
shulaoda added a commit that referenced this pull request Jan 14, 2026
## [1.0.0-beta.60] - 2026-01-14

### 💥 BREAKING CHANGES

- tsconfig: enable auto-discovery by default (#7817) by @shulaoda

### 🚀 Features

- distinguish transformer diagnostics from parse errors (#7872) by @shulaoda
- emit transformer warnings instead of ignoring them (#7850) by @shulaoda
- node: add `output.codeSplitting` option and deprecate `output.advancedChunks` (#7855) by @hyf0
- nativeMagicString reset (#7828) by @IWANABETHATGUY
- nativeMagicString lastChar (#7819) by @IWANABETHATGUY
- dev/lazy: inject lazy compilation runtime automatically (#7816) by @hyf0
- nativeMagicString snip (#7818) by @IWANABETHATGUY
- nativeMagicString construct with options (#7814) by @IWANABETHATGUY
- nativeMagicString clone (#7813) by @IWANABETHATGUY
- nativeMagicString `insert` (#7812) by @IWANABETHATGUY
- nativeMagicString `slice` (#7807) by @IWANABETHATGUY
- nativeMagicString trim methods (#7800) by @IWANABETHATGUY
- make closeBundle hook receive the last error (#7278) by @Copilot

### 🐛 Bug Fixes

- when package only contains export default, cjsDefault didn't resolve correctly (#7873) by @IWANABETHATGUY
- inline __name calls for default exports  (#7862) by @IWANABETHATGUY
- improve variable renaming to avoid unnecessary shadowing in nested scopes (#7859) by @IWANABETHATGUY
- use correct index when inserting keepNames statements during export default transformation (#7853) by @IWANABETHATGUY
- transform non-static dynamic imports when `dynamicImportInCjs` is `false` (#7823) by @shulaoda
- dev/lazy: should include imported and non-executed modules in the patch (#7815) by @hyf0
- set ExportsKind to Esm when json is none object literal  (#7808) by @IWANABETHATGUY
- nativeMagicString move api (#7796) by @IWANABETHATGUY
- remove unnecessary exports after merging into commong and user defined entry (#7789) by @IWANABETHATGUY
- use output.name instead of chunk.name in mixed export warning (#7788) by @Copilot

### 🚜 Refactor

- generalize ParseError to OxcError with dynamic EventKind (#7868) by @shulaoda
- rust: rename `advanced_chunks` to `manual_code_splitting` (#7856) by @hyf0
- string_wizard error hanlding (#7830) by @IWANABETHATGUY
- remove `experimental.disableLiveBindings` option (#7820) by @sapphi-red
- node/test: run fixture tests in concurrent (#7790) by @hyf0
- move ConfigExport and RolldownOptionsFunction types to define-config (#7799) by @shulaoda
- cli: validate config after resolving and improve error message (#7798) by @shulaoda

### 📚 Documentation

- rebrand (#7670) by @yyx990803
- fix incorrect default value for propertyReadSideEffects (#7847) by @Copilot
- remove options pages and redirect to reference pages (#7834) by @sapphi-red
- options: inline types to option property pages (#7831) by @sapphi-red
- options: port checks.pluginTimings content from options page to reference page (#7832) by @sapphi-red
- options: use `@linkcode` where possible (#7824) by @sapphi-red
- options: port content from options page to reference page (#7822) by @sapphi-red
- options: add descriptions for output options (#7821) by @sapphi-red
- options: add description for input options (#7802) by @sapphi-red
- options: add description for `checks.*` (#7801) by @sapphi-red
- apis: add hook graph (#7671) by @sapphi-red

### 🧪 Testing

- add all valid combination of chunk exports related test (#7851) by @IWANABETHATGUY
- enable MagicString test after api return type alignment (#7797) by @IWANABETHATGUY
- init magic-string test (#7794) by @IWANABETHATGUY

### ⚙️ Miscellaneous Tasks

- vite-tests: configure git user for rebase operation (#7875) by @shulaoda
- rolldown_binding: remove v3 native plugins (#7837) by @shulaoda
- rolldown_binding: allow crate-type as lib (#7866) by @Brooooooklyn
- README.md: adjust position and size of rolldown logo (#7861) by @hyf0
- deps: update test262 submodule for tests (#7857) by @sapphi-red
- deps: update oxc to v0.108.0 (#7845) by @renovate[bot]
- deps: update dependency oxlint to v1.39.0 (#7849) by @renovate[bot]
- deps: update dependency oxfmt to ^0.24.0 (#7844) by @renovate[bot]
- deps: update npm packages (#7841) by @renovate[bot]
- deps: update rust crates (#7839) by @renovate[bot]
- deps: update github-actions (#7840) by @renovate[bot]
- use workspace edition for all crates (#7829) by @IWANABETHATGUY
- deps: update dependency oxlint-tsgolint to v0.11.0 (#7827) by @renovate[bot]
- deps: update napi to v3.8.2 (#7810) by @renovate[bot]
- remove outdated snapshot files (#7806) by @shulaoda
- deps: update crate-ci/typos action to v1.42.0 (#7792) by @renovate[bot]

Co-authored-by: shulaoda <[email protected]>
shulaoda pushed a commit that referenced this pull request Jan 14, 2026
…ot_symbol` pass (#7867)

## Summary

This PR optimizes the symbol renamer by eliminating the expensive
`rename_non_root_symbol` pass and fixes a bug that could generate
duplicate declarations.

### Background
This is a rework of #4014, split into atomic commits for easier review.
The original PR was rejected due to too many changes at once. Issue
#6586 has since been fixed by #7859, but the performance optimization
approach from #4014 still provides significant gains.

### Key Changes

**1. Eliminate `rename_non_root_symbol` pass**
- During root scope renaming, check against nested scopes upfront via
`is_name_available()`
- Most nested scope symbols keep their original names
- Only handle edge cases (cross-module shadowing, CJS params) in
lightweight `register_nested_scope_symbols()`

**2. Skip storing original-named nested symbols**
- Add `canonical_name_for_or_original()` method that falls back to
original name
- Only store symbols in `canonical_names` when actually renamed
- Reduces HashMap insertions for ~80-90% of nested symbols

**3. Fix duplicate declaration bug**
- When renaming nested symbols (e.g., `a` → `a$1`), check if the
candidate name already exists in the same module
- Prevents generating invalid code like `function test(a$1, a$1)` when
both `a` and `a$1` exist as parameters

### How It Works

**Old algorithm:**
1. Rename all root scope symbols
2. Iterate through ALL nested scope symbols to check for conflicts
3. Rename nested symbols that conflict with renamed root symbols

**New algorithm:**
1. During root scope renaming, pick names that don't conflict with
nested scopes upfront
2. Most nested scope symbols keep original names (no storage needed)
3. Only rename nested symbols that shadow cross-module or renamed
top-level symbols
4. Check for duplicate declarations when generating renamed names

```text
 ┌─────────────────────────────────────────────────────────────────┐
 │ Phase 1: Root Scope Renaming                                    │
 ├─────────────────────────────────────────────────────────────────┤
 │  for each top-level symbol:                                     │
 │    → Check: used_canonical_names (top-level conflicts)          │
 │    → Check: entry module nested scopes (avoid capture)          │
 │    → Check: own module nested scopes (avoid capture)            │
 │    → Pick name that avoids ALL conflicts upfront                │
 └─────────────────────────────────────────────────────────────────┘
                               ↓
 ┌─────────────────────────────────────────────────────────────────┐
 │ Phase 2: Nested Scope Renaming (lightweight)                    │
 ├─────────────────────────────────────────────────────────────────┤
 │  for each nested symbol:                                        │
 │    ├─ shadows cross-module top-level? ──→ rename                │
 │    ├─ shadows renamed top-level?      ──→ rename                │
 │    ├─ is CJS `exports`/`module`?      ──→ rename                │
 │    └─ otherwise ──→ keep original (no storage needed)           │
 │                                                                 │
 │  When renaming, check candidate doesn't exist in same module    │
 │  to prevent duplicate declarations:                             │
 │                                                                 │
 │    Input: function test(a, a$1) { ... }                         │
 │           where `a` shadows top-level from another module       │
 │                                                                 │
 │    Rename `a`:                                                  │
 │      try `a$1` → exists in module scope → skip                  │
 │      try `a$2` → available → use it ✓                           │
 │                                                                 │
 │    Result: function test(a$2, a$1) { ... }                      │
 └─────────────────────────────────────────────────────────────────┘
```

### Test Case

Added `nested_scope_rename_counter` test to verify the duplicate
declaration fix:

```js
// other.js
export const a = 'from-other';

// module_with_nested.js - has both `a` and `a$1` as parameters
export function test(a, a$1) { return [a, a$1]; }

// main.js
import { a } from './other.js';
import { test } from './module_with_nested.js';
```
**Before fix**: `function test(a$1, a$1)` - duplicate declarations!

**After fix**: `function test(a$2, a$1)` - correctly skips `a$1` since
it already exists

### 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

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
Co-authored-by: Yunfei He <[email protected]>
hyf0 pushed a commit that referenced this pull request Jan 20, 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:

- #7859
- #7867 
- #7899 

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:**
```js
// 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:**
```js
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`):**

```js
// 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:**
```js
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`):**

```js
// 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:**
```js
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`):**

```js
// 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:**
```js
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:**
```js
// cjs-module.js (detected as CommonJS)
function helper() {
  const exports = {};  // Would shadow CJS wrapper's exports parameter
  return exports;
}
module.exports = helper;
```

**Output:**
```js
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.
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.

[Bug]: Unnecessary variable renaming

2 participants