Skip to content

css: do not add transition entries the declaration already lists - #38692

Open
robobun wants to merge 1 commit into
mainfrom
farm/e66ba5e1/css-transition-prefix-dedupe
Open

robobun wants to merge 1 commit into
mainfrom
farm/e66ba5e1/css-transition-prefix-dedupe

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • With CSS targets that need vendor prefixes (this includes bun build's default browser target, whose safari 14 entry still prefixes the mask-* properties), transition lists come out with duplicated prefixed entries:
    • .a{color:red} .a{transition: mask-size .3s} -> transition: -webkit-mask-size .3s, -webkit-mask-size .3s, mask-size .3s (plain bun build a.css)
    • a run of same-query @media rules merged into one gains one more copy per merged rule: three rules give -webkit-mask-size .3s, -webkit-mask-size .3s, -webkit-mask-size .3s, mask-size .3s
    • transition: margin-inline-start .2s, transform .2s (safari 8) duplicates -webkit-transform inside every generated :lang() rule, without any rule merging
    • already autoprefixed input does it in a single pass: transition: -webkit-transform .3s, transform .3s, opacity .2s (safari 6) -> -webkit-transform .3s, -webkit-transform .3s, transform .3s, opacity .2s; same for transition-property: -webkit-transform, transform
    • the same for the -webkit-mask-box-image entry inserted for mask-border: a merged rule or transition: -webkit-mask-box-image .2s, mask-border .2s ends up with it twice
  • Cause: expand_properties in src/css/properties/transition.rs expands every unprefixed entry to the full prefix set for the targets (set_prefixes_for_targets) and inserts the mask-box-image counterpart of a mask-border* entry unconditionally, without looking at what the rest of the list already says. The handler's own output lists every prefix as a separate entry, so each pass over a block adds the expansion again, and rule merging (flush_pending_style_merge and the CssRule::Media arm in src/css/rules/mod.rs) as well as add_logical_rule all run a block through the handlers more than once.
  • lightningcss 1.30.2 prints each of the inputs above with a single prefixed entry.

Fix

  • expand_properties keeps a copy of the list as written. When an entry is expanded for the targets, the prefixes the expansion added that another entry of the same property already carries are dropped again (expand_prefixes), and the -webkit-mask-box-image* entry is only inserted when the list does not contain it yet (webkit_mask_property_to_insert). Both helpers replace the previously duplicated ltr/rtl blocks.
  • Why this is the right shape:
    • Only bits the expansion added are ever removed, and no entry is removed or reordered. Each entry therefore keeps its own duration, delay and timing function (in flush those lists are paired with the property list by position), and author-written repeats such as transform 1s, transform 2s still expand both entries, since the unprefixed bit is never something the expansion added.
    • The handler's output is now a fixed point of the handler: every prefix it generated is present as an explicit entry of the same property, so a second pass adds nothing, and the output no longer depends on how many rules were merged into a block. The merged and :lang() outputs now match lightningcss.
    • Upstream handles the explicit-prefix case with merge_properties(), which folds later entries of the same property into the first one and drops them while leaving the duration/delay/timing lists alone, so every later entry is paired with the wrong values: lightningcss 1.30.2 turns transition: -webkit-transform .3s, transform .3s, opacity .2s into ..., opacity .3s, also without targets. Bun gets that input right today, so that function is deliberately not ported. Subtracting from the expansion instead gives upstream's output whenever the merged entries have the same values (every re-minify case) and keeps the author's values otherwise.
    • Upstream does not dedupe the mask-box-image insertion either (it duplicates it on a merged rule too); it is covered here because it is the same fixed-point requirement in the same function.
  • Verified:
    • test/js/bun/css/css.test.ts, transition block: 13 new prefix_tests (explicit prefixed entry before and after the unprefixed one, different durations per entry, transition-property, repeated unprefixed entries, merged same-selector rules, a run of three merged @media rules, the :lang() rules for a logical property, and mask-border: insertion for safari 15, no insertion for safari 16, explicit entry, merged rule, logical rules). 10 of them fail on the unfixed build with the duplicated entries, the other 3 pin the unchanged behavior they build on; all pass with the fix, along with the rest of the file.
    • bun bd test test/bundler/css/ test/bundler/esbuild/css.test.ts: pass.
    • bun build on the stylesheet below produces the single-entry output with the fix.

Background

  • Minifying a stylesheet runs every declaration block through the property handlers (DeclarationHandler); handlers such as TransitionHandler buffer the related declarations and write a minimal form in finalize/flush. When CssRuleList::minify merges a rule into an earlier one with the same selector, or an @media rule into the previous one with the same query, the merged block is minified again, and the :lang() rules a handler emits through add_logical_rule are minified as new rules. So a handler sees its own output as input, and its output has to be stable under that.
  • A PropertyId of a prefixable property carries a VendorPrefix bit set. Targets::prefixes replaces a set that contains the unprefixed bit with the full set the browser targets need and returns an explicitly prefixed set unchanged; that is what set_prefixes_for_targets applies to each list entry. flush then turns a multi-bit entry of a transition shorthand into one transition per bit (get_transitions), which is why the handler's own output comes back as separate single-prefix entries; a transition-property list prints a multi-bit entry as one name per bit.
  • mask-border and its longhands have no prefixed spelling; WebKit's equivalent is -webkit-mask-box-image, a different property, so masking::get_webkit_mask_property returns a separate PropertyId that the handler inserts in front of the entry instead of setting a prefix bit.
bun build repro (default targets)
.a { color: red }
.a { transition: mask-size .3s }
.b { transition: -webkit-mask-size .3s, mask-size .3s, opacity .2s }
@media (min-width: 1px) { .e { transition: mask-size .3s } }
@media (min-width: 1px) { .f { color: blue } }
@media (min-width: 1px) { .g { color: green } }

bun 1.4.0 / main:

.a { color: red; transition: -webkit-mask-size .3s, -webkit-mask-size .3s, mask-size .3s; }
.b { transition: -webkit-mask-size .3s, -webkit-mask-size .3s, mask-size .3s, opacity .2s; }
@media (min-width: 1px) {
  .e { transition: -webkit-mask-size .3s, -webkit-mask-size .3s, -webkit-mask-size .3s, mask-size .3s; }
  ...
}

with this change:

.a { color: red; transition: -webkit-mask-size .3s, mask-size .3s; }
.b { transition: -webkit-mask-size .3s, mask-size .3s, opacity .2s; }
@media (min-width: 1px) {
  .e { transition: -webkit-mask-size .3s, mask-size .3s; }
  ...
}
Comparison with lightningcss 1.30.2 (targets chrome 20, firefox 20, safari 5, ie 9, ios_saf 6, android 4 unless noted)
input bun before bun after lightningcss
.a{font-weight:bold}.a{transition: opacity 1s, transform 1s} opacity 1s,-webkit-transform 1s,-ms-transform 1s,-webkit-transform 1s,-ms-transform 1s,transform 1s opacity 1s,-webkit-transform 1s,-ms-transform 1s,transform 1s same as after
three merged @media (x:1) rules, first with transition: opacity 1s, transform 1s three copies of -webkit-transform 1s,-ms-transform 1s one same as after
.a{transition: opacity 1s, -webkit-transform 1s, transform 1s} -webkit-transform twice opacity 1s,-webkit-transform 1s,-ms-transform 1s,transform 1s same as after
.a{transition-property: opacity, -webkit-transform, transform} -webkit-transform twice opacity,-webkit-transform,-ms-transform,transform same as after
.a{transition: -webkit-transform .3s, transform .3s, opacity .2s} (safari 6) -webkit-transform twice, opacity .2s -webkit-transform .3s,transform .3s,opacity .2s -webkit-transform .3s,transform .3s,opacity .3s
.a{transition: transform 1s, -webkit-transform 1s} (safari 6) -webkit-transform 1s,transform 1s,-webkit-transform 1s transform 1s,-webkit-transform 1s -webkit-transform 1s,transform 1s
.a{font-weight:bold}.a{transition: mask-border 1s} -webkit-mask-box-image twice -webkit-mask-box-image 1s,mask-border 1s -webkit-mask-box-image twice

When the transition handler expands a property in a transition list to
the vendor prefixes the targets need, it now leaves out the prefixes that
other entries of the same property in that declaration already carry, and
it no longer inserts the -webkit-mask-box-image* counterpart of a
mask-border* entry when the declaration already contains it.

Without this, running the handler over its own output (a merged
same-selector rule, a merged @media body, or the :lang() rules generated
for a logical property) appended another copy of every prefixed entry per
pass, and already-autoprefixed input such as
"transition: -webkit-transform .3s, transform .3s" got a duplicate
-webkit-transform entry in a single pass.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ef3b5f7b-cf40-4030-9a50-4798b7e1a103

📥 Commits

Reviewing files that changed from the base of the PR and between 9805144 and 472e58c.

📒 Files selected for processing (2)
  • src/css/properties/transition.rs
  • test/js/bun/css/css.test.ts

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced and fixed, waiting on CI for 472e58c.

  • Reproduced on 1.4.0 and main with cssInternals.prefixTest / minifyTest (a merged same-selector rule, a run of merged @media rules, the :lang() rules generated for a logical property, and already-prefixed input such as transition: -webkit-transform .3s, transform .3s all get an extra prefixed entry per pass), and through plain bun build a.css, whose default browser targets still prefix the mask-* properties (transition: -webkit-mask-size .3s, -webkit-mask-size .3s, mask-size .3s for two merged .a rules).
  • test/js/bun/css/css.test.ts (transition block): 13 new tests, 10 of which fail on the unfixed build; the whole file, test/bundler/css/ and test/bundler/esbuild/css.test.ts pass with the fix.
  • Found along the way and filed separately: the transition handler pairs durations with the wrong entries after the mask-border / block logical expansions lengthen the list (same in lightningcss), and DeclarationHandler::finalize runs the transition handler before the font handler while lightningcss does the opposite (declaration order only). Neither is touched here.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — focused fixed-point fix in expand_properties so re-minifying a block (rule merge, :lang() emission, or already-prefixed input) no longer duplicates prefixed transition entries. The inline note below is pre-existing and strictly mitigated here, not introduced.

Extended reasoning...

Overview

Touches src/css/properties/transition.rs (expand_properties + two new local helpers, ~40 net lines) and adds 13 prefix_test cases to test/js/bun/css/css.test.ts. The behavioral change: before expanding prefixes, snapshot the list as written; after set_prefixes_for_targets, drop only the newly added prefix bits that another entry of the same .tag() already carries; and skip the -webkit-mask-box-image* insert when the snapshot already contains it. The two helpers also replace the previously duplicated ltr/rtl blocks.

Security risks

None. Pure CSS-minifier output shaping — no I/O, no unsafe, no external input beyond parsed CSS that was already handled identically before this change. Worst-case failure mode is suboptimal (but still valid) CSS output.

Level of scrutiny

Moderate. The prefix-bit arithmetic is subtle, so I traced it: written bits are never in added, so an entry can never lose a bit it started with (the result is never empty and author order/positions are preserved — transform 1s, transform 2s still expands both, pinned by a test). as_written is used only for containment/tag-match, not indexed access, so index shifts from earlier logical-property replace() calls don't affect it. The rtl branch reuses the ltr as_written, which is sound because rtl_properties is cloned from properties and the two lists stay index-aligned (all Inline ltr/rtl replacement slices are equal-length). PropertyId::tag() returns the prefix-less discriminant, so Transform(WEBKIT) and Transform(NONE) compare equal for the fold — verified in properties_generated.rs.

Other factors

  • The one finding is explicitly pre-existing (the properties.insert without growing durations/delays/timing_functions); this PR reduces how often that path fires and does not make it worse.
  • The deliberate divergence from upstream merge_properties() is well-argued: upstream's approach shifts durations onto the wrong entries (opacity .2s → .3s in the comparison table), which Bun avoids today and this change preserves.
  • Test coverage spans the variant matrix the PR description enumerates: explicit prefix before/after unprefixed, distinct durations, transition-property, repeated unprefixed, merged same-selector, three merged @media, :lang() rules, and the four mask-border cases.

Comment on lines +551 to +557
let targets = &context.targets;
expand_prefixes(properties.r#mut(index), &as_written, targets);
if let Some(property_id) =
masking::get_webkit_mask_property(properties.at(index))
webkit_mask_property_to_insert(properties.at(index), &as_written, targets)
{
if context
.targets
.prefixes(VendorPrefix::NONE, Feature::MaskBorder)
.contains(VendorPrefix::WEBKIT)
{
properties.insert(index, property_id);
i += 1;
}
properties.insert(index, property_id);
i += 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟣 Pre-existing (not introduced by this PR, and strictly mitigated by it): when the -webkit-mask-box-image* entry is inserted here, durations/delays/timing_functions in flush are not grown to match, so get_transitions cycles and shifts every subsequent entry — safari 15 + transition: mask-border .5s, opacity .1s comes out as -webkit-mask-box-image .5s, mask-border .1s, opacity .5s. The LogicalPropertyId::Block/Inline replace() arms (e.g. inset-block → two entries) have the same shape. Noting only because the refactored block is where a fix would land and the new tests all use single-entry or equal-duration lists that hide the cycling; not blocking.

Extended reasoning...

What the bug is

expand_properties receives only the properties list (flush() calls it as expand_properties(&mut p.0, arena, context)) and grows it in place via properties.insert(index, property_id) when a mask-border* entry needs a -webkit-mask-box-image* counterpart. The durations, delays, and timing_functions lists that flush holds alongside it are never touched, so after the insert the property list is one entry longer than the value lists it is paired with.

get_transitions then walks properties.slice() and pairs each entry with durations.at(durations_idx), bumping the index once per property via cycle_bump(&mut durations_idx, durations.len()). When properties.len() > durations.len(), that cycling shifts every entry after the insertion point onto its neighbor's value and wraps the last entry back to the first slot.

Step-by-step repro

Input (targets: safari 15, which needs the -webkit-mask-box-image insert):

.foo { transition: mask-border .5s, opacity .1s }
  1. handle_property splits the shorthand into properties = [MaskBorder, Opacity], durations = [.5s, .1s] (delays/timing similarly length-2).
  2. flush calls expand_properties. webkit_mask_property_to_insert returns WebKitMaskBoxImage (as_written does not contain it), so properties.insert(0, WebKitMaskBoxImage) runs. Now properties = [WebKitMaskBoxImage, MaskBorder, Opacity] — length 3.
  3. durations is still [.5s, .1s] — length 2; flush never grew it.
  4. get_transitions iterates:
    • WebKitMaskBoxImage → durations[0] = .5s, bump → idx 1
    • MaskBorder → durations[1] = .1s, bump → idx 0 (cycle)
    • Opacity → durations[0] = .5s
  5. Output: transition: -webkit-mask-box-image .5s, mask-border .1s, opacity .5s — mask-border lost its .5s and opacity was rewritten from .1s to .5s.

The prefix-expansion path (expand_prefixes) is not affected: a multi-bit VendorPrefix stays a single properties[] entry and get_transitions fans it out with one durations_idx increment. Only the mask insert (and the LogicalPropertyId::Block/Inline arms whose replace() calls insert_slice for multi-entry expansions like InsetBlock → [Top, Bottom]) add real list entries.

Why this is pre-existing and why the new tests don't catch it

The removed code did the identical properties.insert(index, property_id) without growing the value lists; this PR only wraps the condition in webkit_mask_property_to_insert and adds the as_written dedup. So this is not a regression — and the PR strictly reduces how often it fires, since the second-pass insert (merged rules, :lang() rules) is now skipped when the entry is already listed. The upstream lightningcss code the file was ported from has the same behavior.

The 13 new prefix_tests all use either a single mask-border entry (durations.len() == 1, so cycling is a no-op) or equal durations across every entry (.2s, .2s), both of which are invariant under the shift. A two-entry list with distinct durations, e.g. transition: mask-border .5s, opacity .1s at safari 15, would exercise it.

What a fix would look like

The value lists need the entry duplicated at the same index the property was inserted at (so the inserted -webkit-mask-box-image reuses mask-border's duration and everything after it stays paired). That means either passing the value lists into expand_properties and inserting alongside the property, or having expand_properties return the insertion indices for flush to apply to all four lists. The same treatment would cover the multi-entry Block/Inline replace() expansions. Not asking for it here — it's a separate change from the fixed-point fix this PR makes, and it exists upstream too — but flagging it since the PR description's "no entry is removed or reordered, so each entry keeps its own duration" holds for expand_prefixes but not for this insert.

Jarred-Sumner pushed a commit that referenced this pull request Sep 21, 2026
### Problem
- 49 hand-written trait impls across 12 crates have no user: no call,
and no bound that needs them.
- rustc's `dead_code` lint exempts trait impls, so the workspace's deny
lints never report them.

### Fix
- Delete the 49 impls and `hash_array_list` (`src/css/generics.rs`),
whose only caller was a removed impl. 25 files, 436 lines removed.
- Candidates come from the linker: in a relink of the debug build with
`--gc-sections`, an impl block with no kept function address is a
candidate. The compiler decides next: an impl that a bound, a
supertrait, another target, or a unit test needs does not compile away,
so it stays. That kept 128 of 233 candidates.
- 56 more stayed by choice: open PRs use them, docs describe them, or
they are std ergonomics impls on shared containers. See the Notes.
- Verified: `bun run rust:check-all` (12 targets), `cargo check
--release` (3 targets), `cargo check --tests`, `cargo clippy`, `bun bd`,
11 test files, and a scan of all open PRs for users.

### Background
- To rustc a trait impl is always reachable: generic code could call it.
Only a whole-program view shows that nothing does.
- The debug build uses opt-level 0 and one section per function, so
`--gc-sections` drops every function with no caller. Callers in generic
instantiations and macro-expanded code count.
- That link proves linux only. `cargo check` covers the other targets.
- #39795 removed 92 impls this way.

<details><summary>Notes</summary>

**Removed impls, by file**

- `src/ast/e.rs`
  - `impl From<f64> for Number`
- `src/bun_alloc/baby_vec.rs`
  - `impl<'a, 'b, T: Copy> Extend<&'b T> for BabyVec<'a, T>`
  - `impl<'a, 'b, T> IntoIterator for &'b BabyVec<'a, T>`
  - `impl<'a, 'b, T> IntoIterator for &'b mut BabyVec<'a, T>`
  - `impl<'a, T> core::borrow::Borrow<[T]> for BabyVec<'a, T>`
  - `impl<'a, T> AsRef<[T]> for BabyVec<'a, T>`
- `src/bun_alloc/lib.rs`
  - `impl<const N: usize> BSSAppendable for [&[u8]; N]`
- `src/bun_core/atomic_cell.rs`
  - `unsafe impl<U> Atom for *const U`
- `src/bun_core/fmt.rs`
  - `impl From<InvalidCharacter> for crate::CrateError`
- `src/bun_core/output.rs`
  - `impl<T: fmt::Display> FmtTuple for &[T]`
- `src/bundler/Chunk.rs`
  - `impl core::ops::Index<usize> for CompileResultSlots`
- `src/bundler/bundle_v2.rs`
  - `impl Ord for StableRef`
  - `impl PartialOrd for StableRef`
- `src/collections/array_hash_map.rs`
- `impl<K, V, C, A: MapAllocator> ArrayHashMapExt for ArrayHashMap<K, V,
C, A>`
- `src/css/generics.rs`
  - `impl<'bump, T: DeepClone<'bump>> DeepClone<'bump> for &'bump T`
  - `impl<T: CssEql, const N: usize> CssEql for [T; N]`
  - `impl CssHash for ()`
  - `impl<T: CssHash, const N: usize> CssHash for [T; N]`
  - `impl<'bump, T: CssHash> CssHash for ArrayList<'bump, T>`
  - `impl CssHash for bun_ast::Loc`
  - `impl<T: IsCompatible, const N: usize> IsCompatible for [T; N]`
  - `impl<'bump, T: ToCss> ToCss for ArrayList<'bump, T>`
  - `impl ToCss for CustomIdent`
  - `impl ToCss for DashedIdent`
  - `impl ToCss for Ident`
- `src/css/media_query.rs`
  - `impl crate::generic::ToCss for MediaList`
- `src/css/properties/mod.rs`
- `impl crate::generics::ParseWithOptions for
css_values::length::Length`
- `impl<S, const P: u8> crate::generics::Parse for GenericBorder<S, P>`
- `src/css/rules/supports.rs`
  - `impl crate::generics::CssEql for SupportsCondition`
  - `impl css::generic::ToCss for SupportsCondition`
- `src/css/selectors/parser.rs`
  - `impl<Impl: SelectorImpl> Default for GenericSelector<Impl>`
  - `impl<Impl: BunSelectorImpl> CssEql for GenericSelectorList<Impl>`
  - `impl<Impl: BunSelectorImpl> CssHash for GenericSelectorList<Impl>`
  - `impl<Impl: BunSelectorImpl> CssEql for GenericComponent<Impl>`
  - `impl<Impl: BunSelectorImpl> CssHash for GenericComponent<Impl>`
- `src/css/values/alpha.rs`
  - `impl crate::generics::CssHash for AlphaValue`
- `src/install/lockfile/lockfile_json_stringify_for_debugging.rs`
  - `impl<const N: usize> JsonScalar for &[u8; N]`
- `src/install/resolution.rs`
  - `impl Default for Tag`
- `src/install_types/resolver_hooks.rs`
  - `impl<I: VersionInt> Default for ResolutionValue<I>`
  - `impl Default for Resolution`
- `src/jsc/JSPropertyIterator.rs`
  - `impl IntoIterObject for *const JSObject`
- `src/jsc/JSValue.rs`
  - `impl FromAny for ()`
  - `impl FromAny for &str`
  - `impl<T: FromAny> FromAny for Option<T>`
- `src/jsc/host_fn.rs`
  - `impl<T> IntoHostConstructReturn for *mut T`
- `src/react_compiler/hir/mod.rs`
  - `impl From<FloatValue> for f64`
- `src/runtime/bake/DevServer.rs`
  - `impl From<OpaqueFileId> for OpaqueFileIdOrOptional`
- `src/runtime/error.rs`
  - `impl From<Error> for bun_jsc::CrateError`
- `src/tcc_sys/tcc.rs`
  - `impl<ErrCtx> Default for Config<ErrCtx>`

**Candidates that stayed (184 of 233)**
- 128: the workspace does not compile without the impl (a bound, a
supertrait, another target, or a unit test needs it).
- 8: open PRs use them. The six `SmallList` impls (`Deref`, `DerefMut`,
`IntoIterator` x2, `FromIterator`, `Extend`) for #39497, #38692 and
#36605. `Default for Wyhash` for #40372. `AsRef<[u8]> for Utf8Bytes` for
#40708. This branch merged with #39497, #38692 and #40708 passes `cargo
check --workspace`. #36605 and #40372 conflict with main on their own.
- 19: std ergonomics impls on widely shared types. This is a design
call, so it needs a maintainer decision and its own PR: `Interned`
(`Deref`, `AsRef<[u8]>`, `Borrow<[u8]>`), `ZStr` (`AsRef<ZStr>`,
`PartialEq<[u8]>`, `PartialEq<&[u8; N]>`), `StoreStr`
(`PartialEq<[u8]>`), `StoreSlice` (`AsRef<[T]>`, `From<&[T]>`,
`From<ArenaVec>`), `IntoIterator for &HashMap` and `&mut HashMap`,
`BoundedArrayAligned` (`Deref`, `DerefMut`, `fmt::Write`), `AsRef<[T]>
for RawSlice`, `Deref for ArenaString` (also the only caller of
`ArenaString::as_str`), `Display for GenericIndex`, `Display for
CowSliceZ`.
- 21: docs or comments describe the impl as part of a design.
`ArrayHashContext<Box<[u8], A>> for BoxedSliceContext<C>` (its doc
comment records it as the fix for lookups reached through `Deref`),
`Default for Ordinal` (the default is `INVALID`, not 0), `IntoLogWrite
for &mut W`, `ThrowFmtArgs for fmt::Arguments`, `PoolStorage for
UnwiredStorage`, `StringBuilder for semver_string::Builder`, `fmt::Write
for FmtAdapter`, `AsFd for &File`, and the css blanket impls that the
`css_derive` module docs and the `Parse` section of
`src/css/generics.rs` name (`Option<T>`, `SmallList`, `bool`, `str`,
`[u8]`, `[T]`, `Size2D`, `Rect`).
- 6: next to a hunk of an open dead-code PR (#40690, #41335, #43010,
#42278), left alone to avoid a conflict.
- 2: the removal leaves something else half-used. See the follow-up
list.

**Method details**
- Link: the debug link command from `build.ninja` plus
`-Wl,--gc-sections`. `-rdynamic`, `--dynamic-list` and the version
script stay, so the exported NAPI/V8/uv surface is still a root.
- Liveness: `llvm-nm` lists the 669,524 kept function symbols and
`llvm-symbolizer` maps each address to `file:line`. An impl block is a
candidate when no kept address falls inside it and a rust-analyzer SCIP
index shows that linux compiles every method in it.
- Limit of the finder: it reads impl blocks from source text. Impls that
a `macro_rules!` type list generates are not candidates, so unused arms
of such lists are not in this PR.
- Restore loop: delete all candidates, run `cargo check --workspace
--keep-going --message-format=json`, put back each impl that an error
names, repeat until clean. Targets in the loop: linux-gnu, windows-msvc,
darwin, freebsd, android, musl, then `--tests`.
- Method resolution: on linux the link proves that no call to a removed
impl remains, so no call can resolve differently. On other targets a
call that used a removed impl fails to compile.
- Open PRs: main has no merge queue, so a green PR that uses a removed
impl would break main when it merges. I fetched the heads of all 5474
open non-draft PRs, took the Rust lines each one adds against its merge
base, and searched them for the types and traits of the 49 impls. No PR
uses one.
- Base: main at 18fe86e. The liveness data is from cf71211. The
commits between them do not touch the 25 files, and every check above
ran again on 18fe86e.
- `cargo check --workspace --tests` reports one error,
`src/bundler/options.rs:2328` (`expected ContentHash, found integer`).
Clean main has the same error. This PR does not touch it.
- Three tests fail in the local debug build with and without this
change: `ffi.test.js` "FTL-compiled call site" (5 s timeout under ASAN),
and two `serve.test.ts` tests that depend on the container (root can
bind low ports, loopback check).

**Other scans in this run**
- Free functions, inherent methods, types, consts and statics: a name
scan, the linker scan, and a rust-analyzer zero-reference scan for
linux, windows and darwin. Every hit was platform code with a caller on
another target, test-only code, or a deletion that an open PR already
carries.
- Also clean or already claimed: `.rs` files outside every mod tree,
unused Cargo dependencies, extern declarations without a Rust caller,
commented-out code in C++/TS, unused `src/js` modules and internal
exports, linker-dropped C++ functions in `src/jsc/bindings`.

**Follow-up candidates, not in this diff**
- 25 hand-written `Debug` impls that nothing formats. They are a
debugging aid, so this PR keeps them.
- `Clone for JsPoster` has no caller, and it is the only reader of the
`JsPosterVTable::clone` slot. Removing both is a small refactor in
`src/event_loop` and `src/jsc/VmHandle.rs`.
- `Display for ErrorLocation` (`src/css/error.rs`) has no caller, and it
is the only reader of `ErrorLocation::filename`.
- The 19 std ergonomics impls above, if a maintainer wants shared
containers to go through inherent methods only.
- `Bun__napi_get_version` in `src/jsc/bindings/napi.cpp` has no caller.
- The class generator emits `<Type>Class__call` shims for classes that
set `call: true` and `noConstructor: true` (the seven `Expect*` matchers
in `jest.classes.ts`). Nothing references those shims.

</details>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant