Skip to content

fix(es/minifier): Allow inlining functions with side-effect-free default parameters#11516

Open
kdy1 wants to merge 8 commits intomainfrom
claude/issue-11512-20260128-0423
Open

fix(es/minifier): Allow inlining functions with side-effect-free default parameters#11516
kdy1 wants to merge 8 commits intomainfrom
claude/issue-11512-20260128-0423

Conversation

@kdy1
Copy link
Member

@kdy1 kdy1 commented Jan 29, 2026

Summary

  • Allow inlining functions with default parameters when the default value has no side effects
  • Add IIFE argument inlining support for default parameters

Fixes #11512

🤖 Generated with Claude Code

@kdy1 kdy1 added this to the Planned milestone Jan 29, 2026
@kdy1 kdy1 self-assigned this Jan 29, 2026
@changeset-bot
Copy link

changeset-bot bot commented Jan 29, 2026

⚠️ No Changeset found

Latest commit: 88f0f73

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

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

@github-actions
Copy link
Contributor

github-actions bot commented Jan 29, 2026

Binary Sizes

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

Commit: 306211a

@codspeed-hq
Copy link

codspeed-hq bot commented Jan 29, 2026

CodSpeed Performance Report

Merging this PR will not alter performance

Comparing claude/issue-11512-20260128-0423 (88f0f73) with main (64be077)

Summary

✅ 184 untouched benchmarks

@kdy1
Copy link
Member Author

kdy1 commented Feb 1, 2026

@claude fix CI

@claude

This comment has been minimized.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
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 sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@kdy1
Copy link
Member Author

kdy1 commented Feb 1, 2026

@claude fix ci

@claude

This comment has been minimized.

@kdy1
Copy link
Member Author

kdy1 commented Feb 2, 2026

@claude Rebase to resolve merge conflicts

@claude

This comment has been minimized.

github-actions bot and others added 2 commits February 2, 2026 00:10
… parameters

When a function has parameters with default values, the minifier was
previously unable to inline it, even if the default value had no side effects.

This change:
1. Modifies `store_decl_for_inlining` to allow functions with default
   parameters to be inlined as "simple functions" when the default value
   has no side effects.

2. Adds IIFE argument inlining support for `Pat::Assign` (default parameters):
   - When an argument is provided at the call site, use the argument
   - When no argument is provided and the default has no side effects,
     use the default value

Fixes #11512

Co-Authored-By: Donny/강동윤 <[email protected]>
… params

When inlining functions with default parameters, we need to check
not only that the default value has no side effects, but also that
it doesn't reference any earlier parameters in the function signature.

For example, `function(a, b = a)` - the default `a` is side-effect-free
but it references the parameter `a`, which would be incorrectly inlined.

This fixes test failures:
- destructuring/default_precedence
- parameters/default_earlier_params
- parameters/assumption_ignoreFunctionLength/default_earlier_params

Co-Authored-By: Donny/강동윤 <[email protected]>
@claude claude bot force-pushed the claude/issue-11512-20260128-0423 branch from 505fef1 to eab14af Compare February 2, 2026 00:15
@kdy1
Copy link
Member Author

kdy1 commented Feb 2, 2026

@claude fix ci

@claude

This comment has been minimized.

…arams

When handling default parameters in IIFE inlining, if no usage data is
found for a parameter, be conservative and skip inlining. This prevents
incorrectly substituting the default value when the parameter might be
reassigned inside the function body.

This can happen when a function declaration is inlined as an IIFE and
the syntax context no longer matches the original analysis data.

Co-authored-by: Donny/강동윤 <[email protected]>
@kdy1
Copy link
Member Author

kdy1 commented Feb 2, 2026

@claude Fix CI

@claude

This comment has been minimized.

…lining

When deciding if a function with default parameters is simple enough
to inline, we need to also check if the parameter with the default
value is reassigned in the function body. If it is reassigned
(e.g., through destructuring like `for (var [a] of [[1]])`),
inlining the default value would be incorrect.

Test case:
```javascript
function foo(a = 2) {
  for (var [a] of [[1]]);
  expect(a).toBe(1);
}
foo();
```

Without this fix, minified output was: `expect(2).toBe(1)` - WRONG!
The parameter `a` is reassigned to `1` through the destructuring,
so the default value `2` should not be inlined.

Co-authored-by: Donny/강동윤 <[email protected]>
@kdy1
Copy link
Member Author

kdy1 commented Feb 2, 2026

@claude Add an enormous amount of execution tests to the minifier. Think about very complex cases.

@claude

This comment has been minimized.

Add ~150 new execution tests covering complex minification scenarios:

- Default parameters: closure capture, earlier param references, destructuring,
  function calls, reassignment in body, rest combinations
- Closures: loop captures with let/var, multiple captures, nested scopes,
  self-referencing, IIFE sequences, arguments object
- Destructuring: nested objects/arrays, renaming, rest operators, computed
  properties, function parameters
- Spread/Rest: array concatenation, object merge, function calls, generators
- Classes: inheritance, super calls, static members, getters/setters,
  computed method names, prototype chains
- Symbols and iterators: custom iterators, Symbol.species
- Generators: delegation, two-way communication, infinite sequences
- Proxy/Reflect: get/set/has traps
- Template literals: nested expressions, tagged templates
- Optional chaining and nullish coalescing: deep access, methods
- Control flow: labeled break/continue, try/catch/finally nesting
- Map/Set/WeakMap/WeakSet operations
- Array methods: reduce, flat, flatMap, from, find
- Object methods: entries/fromEntries, defineProperty
- String/Number/Math/BigInt operations
- Expression evaluation: short-circuit, comma operator, conditionals
- Recursion: mutual recursion, closure chains
- Module patterns: revealing module pattern
- This binding: arrow vs regular, call/apply/bind
- Hoisting edge cases: var/function, TDZ
- Property descriptors: freeze/seal
- Type coercion: Symbol.toPrimitive
- IIFE inlining scenarios
- Dead code elimination
- Constant folding
- typeof/instanceof edge cases
- Unicode identifiers and strings
- Regex: groups, named groups, replace with function
- Error handling: custom errors, rethrow

Co-authored-by: kdy1 <[email protected]>
@kdy1
Copy link
Member Author

kdy1 commented Feb 2, 2026

@claude fix CI

@claude

This comment has been minimized.

When inlining default parameter values, we now check if the function body
contains any `var` declarations with the same symbol name as parameters.

Due to var hoisting, such declarations effectively reassign the parameter
even though they have different SyntaxContext values (the resolver gives
parameters and for-loop var declarations different contexts).

This fixes the incorrect transformation of:
```javascript
function foo(a = 2) {
  for (var [a] of [[1]]);
  expect(a).toBe(1);
}
foo();
```

Which was incorrectly being transformed to `expect(2).toBe(1)`.

Co-authored-by: Donny/강동윤 <[email protected]>
@kdy1
Copy link
Member Author

kdy1 commented Feb 2, 2026

@claude Fix CI

@claude

This comment has been minimized.

…ccess

This fixes a test regression where the USED_RECURSIVELY flag was being lost
between optimization passes. Added a direct check for function body property
access (e.g., f.length, f.name) to prevent incorrect parameter inlining.

Also updated fixture outputs to match the new more aggressive (but correct)
optimization behavior.

Co-authored-by: Donny/강동윤 <[email protected]>
Copy link
Member Author

@kdy1 kdy1 left a comment

Choose a reason for hiding this comment

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

@claude You should not define a new Visitor. Fix it

@claude

This comment has been minimized.

Remove ConflictingVarFinder and IdentWithPropertyAccessFinder Visitors
and replace them with simple recursive functions that iterate through
statements and expressions.

This is a refactoring that maintains the same functionality but avoids
defining new Visitor types.

Co-authored-by: Donny/강동윤 <[email protected]>
@kdy1 kdy1 marked this pull request as ready for review February 2, 2026 12:16
@kdy1 kdy1 requested a review from a team as a code owner February 2, 2026 12:16
Copilot AI review requested due to automatic review settings February 2, 2026 12:16
@kdy1 kdy1 enabled auto-merge (squash) February 2, 2026 12:16
@claude
Copy link
Contributor

claude bot commented Feb 2, 2026

PR Review: fix(es/minifier): Allow inlining functions with side-effect-free default parameters

Summary

This PR addresses issue #11512, enabling the minifier to inline functions with default parameters when those default values have no side effects. The changes are well-structured and comprehensive.


Code Quality and Best Practices ✅

Strengths:

  1. Good separation of concerns - The fix is properly split between iife.rs (IIFE argument inlining) and inline.rs (simple function inlining)
  2. Comprehensive safety checks including:
    • Side-effect detection via may_have_side_effects()
    • Detection of default values referencing earlier parameters (e.g., function(a, b = a))
    • Handling of conflicting var declarations that could reassign parameters
    • Detection of reassigned parameters via usage flags
  3. Proper use of existing utilities (contains_ident_ref, find_pat_ids, ExprExt)
  4. Clear documentation with good comments explaining the purpose of helper functions

Minor suggestions:

  1. In iife.rs:199, the cloning of assign_pat.right could potentially be avoided if the pattern itself is being taken, though this may not be significant for performance.

Potential Bugs or Issues ⚠️

  1. In iife.rs:487-490: The contains_ident_ref_with_property_access_in_expr function for Expr::Member only checks the identifier's sym (name) but not the SyntaxContext. This could theoretically cause false positives if there are shadowed variables with the same name in nested scopes:

    if obj_ident.sym == ident.sym {
        return true;
    }

    Consider using obj_ident.to_id() == ident.to_id() for full identifier comparison, unless intentionally checking all scopes.

  2. Conservative behavior in inline.rs:703-706: When no usage data is found for a parameter, the code returns early (skipping inlining). The comment explains this is intentional for safety, but this could prevent valid optimizations in some edge cases. Worth documenting why this trade-off was made.

  3. In iife.rs:166-169: The check for recursive usage seems redundant since it's immediately followed by a more comprehensive check (contains_ident_ref_with_property_access). Consider whether the USED_RECURSIVELY flag check is still needed, or if both checks serve different purposes.


Performance Considerations ⚡

  1. Manual AST traversal vs Visitor pattern: The PR implements manual recursive traversal functions (has_conflicting_var_decl_in_stmt, contains_ident_ref_with_property_access_in_expr, etc.) rather than using the Visitor pattern. This is intentional per the comments (avoiding unnecessary allocations/overhead), which is appropriate for this performance-critical code path.

  2. Repeated iteration over parameters: In iife.rs, parameter identifiers are collected into a Vec and names into a FxHashSet upfront (lines 62-127), which is efficient for subsequent lookups.

  3. String cloning in param_names: Using id.sym.clone() (lines 99-102) creates new Atom instances. Since Atom is typically interned, this should be cheap, but worth noting.


Security Concerns 🔒

No security issues identified. The changes are purely about optimization correctness and don't introduce any vectors for code injection or unsafe behavior.


Test Coverage 📋

Excellent coverage:

One potential gap:

  • Consider adding a test for the property access detection (f.length, f.name cases mentioned in comments) to ensure the contains_ident_ref_with_property_access check works correctly.

Overall Assessment

This is a well-implemented fix that properly handles a variety of edge cases. The code is defensive, well-documented, and thoroughly tested. The manual AST traversal approach is justified for this performance-critical code.

Recommendation: Approve

The only actionable items are:

  1. Consider comparing full identifiers (including SyntaxContext) in contains_ident_ref_with_property_access_in_expr for Expr::Member to avoid potential false positives with shadowed variables.
  2. Consider adding explicit tests for the f.length/f.name property access detection edge case.

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 enables the SWC minifier to inline functions that have default parameters with side-effect-free values. Previously, any function with default parameters (including Pat::Assign patterns) would be skipped for inlining, even when the default values were simple literals without side effects.

Changes:

  • Enhanced function inlining logic to support default parameters with side-effect-free values
  • Added IIFE (Immediately Invoked Function Expression) argument inlining support for default parameters
  • Implemented safety checks for parameter references, side effects, and var declaration conflicts

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
crates/swc_ecma_minifier/src/compress/optimize/inline.rs Added logic to check and allow inlining of functions with side-effect-free default parameters, including checks for parameter interdependencies and conflicting var declarations
crates/swc_ecma_minifier/src/compress/optimize/iife.rs Extended IIFE optimization to handle default parameters, with comprehensive safety checks and helper functions for detecting property access and var conflicts
crates/swc_ecma_minifier/tests/fixture/issues/11512/* Test cases demonstrating the fix for the reported issue
crates/swc_ecma_minifier/tests/fixture/issues/11512-simple/* Simplified test case for default parameter inlining
crates/swc_ecma_minifier/tests/fixture/pr/11446/* Test case for default parameters referencing earlier parameters
crates/swc_ecma_minifier/tests/terser/compress/evaluate/issue_2926_1/output.js Updated expected output after optimization improvement
crates/swc_ecma_minifier/tests/fixture/issues/firebase-firestore/1/output.js Updated expected output showing function inlining
crates/swc_ecma_minifier/tests/exec.rs Added 3000+ lines of comprehensive execution tests covering edge cases

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

Comment on lines +385 to +394
let references_earlier_param =
param_idents.iter().take(idx).any(|maybe_ident| {
match maybe_ident {
Some(ident) => {
contains_ident_ref(&*assign_pat.right, ident)
}
// Complex pattern - be conservative
None => true,
}
});
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The logic for checking if a default parameter references earlier parameters appears correct. However, there's a subtle issue: when checking if a default value references earlier parameters in the IIFE code (lines 385-394), the code returns true if maybe_ident is None (complex pattern). This is conservative but might be overly restrictive. For simple cases where the default doesn't reference anything, this would still prevent inlining even though it's safe. Consider checking if the default value references ANY identifier before being conservative about complex patterns.

Copilot uses AI. Check for mistakes.
Comment on lines +821 to +845
for earlier_param in
f.function.params.iter().take(param_idx)
{
let earlier_ident = match &earlier_param.pat {
Pat::Ident(id) => Some(&id.id),
Pat::Assign(a) => {
if let Pat::Ident(id) = &*a.left {
Some(&id.id)
} else {
// Complex pattern - be conservative
return;
}
}
_ => {
// Complex pattern - be conservative
return;
}
};

if let Some(ident) = earlier_ident {
if contains_ident_ref(&*assign.right, ident) {
return;
}
}
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

When checking for references to earlier parameters in inline.rs (lines 821-845), the code returns early if any earlier parameter has a complex pattern (lines 830-831, 834-836). While this is conservative and safe, it might be overly restrictive. Consider: if the default value doesn't reference any identifiers at all (e.g., it's just a literal like 42), it would still skip inlining due to a complex earlier parameter. This might prevent some valid optimizations. However, given the complexity of handling all edge cases, the current conservative approach is reasonable.

Copilot uses AI. Check for mistakes.
Comment on lines +790 to +793
if let Pat::Ident(param_id) = &*assign.left {
if has_conflicting_vars
&& param_names.contains(&param_id.sym)
{
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The condition has_conflicting_vars && param_names.contains(&param_id.sym) has a logical issue. The function has_conflicting_var_decl returns true if ANY var declaration in the body conflicts with ANY parameter name. The subsequent check param_names.contains(&param_id.sym) will always be true for a parameter (since we're iterating over parameters), making it redundant. The code should instead check if THIS specific parameter has a conflicting var declaration. Consider refactoring has_conflicting_var_decl to return FxHashSet<Atom> containing only the parameter names that have conflicts, then check conflicting_names.contains(&param_id.sym).

Suggested change
if let Pat::Ident(param_id) = &*assign.left {
if has_conflicting_vars
&& param_names.contains(&param_id.sym)
{
if let Pat::Ident(_param_id) = &*assign.left {
if has_conflicting_vars {

Copilot uses AI. Check for mistakes.
// body that has the same name as this parameter. Due to var
// hoisting, such declarations effectively reassign the parameter,
// but they may have different SyntaxContext values.
if has_conflicting_vars && param_names.contains(&param_id.sym) {
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The condition has_conflicting_vars && param_names.contains(&param_id.sym) has a logical issue. The function has_conflicting_var_decl returns true if ANY var declaration in the body conflicts with ANY parameter name. The subsequent check param_names.contains(&param_id.sym) will always be true for a parameter (since we're iterating over parameters), making it redundant. The code should instead check if THIS specific parameter has a conflicting var declaration. Consider refactoring has_conflicting_var_decl to return FxHashSet<Atom> containing only the parameter names that have conflicts, then check conflicting_names.contains(&param_id.sym).

Suggested change
if has_conflicting_vars && param_names.contains(&param_id.sym) {
if has_conflicting_vars {

Copilot uses AI. Check for mistakes.
// Check if this is a member expression with the target ident as object
Expr::Member(member) => {
if let Expr::Ident(obj_ident) = &*member.obj {
if obj_ident.sym == ident.sym {
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The property access check only compares symbol names (sym) without checking the SyntaxContext (ctxt). This could lead to false positives when different identifiers with the same name but different contexts are used. Consider using obj_ident.sym == ident.sym && obj_ident.ctxt == ident.ctxt to ensure proper identifier comparison, similar to how contains_ident_ref works in swc_ecma_utils.

Suggested change
if obj_ident.sym == ident.sym {
if obj_ident.sym == ident.sym && obj_ident.span.ctxt == ident.span.ctxt {

Copilot uses AI. Check for mistakes.
Expr::OptChain(opt) => match &*opt.base {
OptChainBase::Member(m) => {
if let Expr::Ident(obj_ident) = &*m.obj {
if obj_ident.sym == ident.sym {
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The property access check only compares symbol names (sym) without checking the SyntaxContext (ctxt). This could lead to false positives when different identifiers with the same name but different contexts are used. Consider using obj_ident.sym == ident.sym && obj_ident.ctxt == ident.ctxt to ensure proper identifier comparison, similar to how contains_ident_ref works in swc_ecma_utils.

Suggested change
if obj_ident.sym == ident.sym {
if obj_ident.sym == ident.sym && obj_ident.span.ctxt == ident.span.ctxt {

Copilot uses AI. Check for mistakes.
Comment on lines +1942 to +1946
if let Expr::Ident(obj_ident) = &*m.obj {
if obj_ident.sym == ident.sym {
return true;
}
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The property access check only compares symbol names (sym) without checking the SyntaxContext (ctxt). This could lead to false positives when different identifiers with the same name but different contexts are used. Consider using obj_ident.sym == ident.sym && obj_ident.ctxt == ident.ctxt to ensure proper identifier comparison, similar to how contains_ident_ref works in swc_ecma_utils.

Copilot uses AI. Check for mistakes.
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.

When adding a default value to a function, compress / minify can't optimize the function.

2 participants