Conversation
…print time The parser already decides whether a `composes` declaration counts (it has to sit directly in a style rule with a single class selector) and records the accepted ones in `StyleSheet::composes` for the bundler. It warned about rejected ones but still pushed them, and the printer then validated the property a second time and failed the whole stylesheet with an opaque PrintError. The print-time check was also stricter than the parser: it refused any rule printed at a nested indent, so `composes` in a style rule inside a top-level @media block, or in a module pulled in through an `@import` with layer/media/supports conditions (the bundler wraps those rules itself), failed without any warning at all. Rejected declarations are now dropped after the warning, and the import record of a rejected `composes: x from "./file.css"` is marked unused so the file is neither resolved nor bundled. The printer only omits the property, as every `composes` left in a style rule was accepted. This is what esbuild does. handle_composes, Printer::is_nested and the two printer error kinds had no other users and are removed. The cli.test.ts print-failure test used a rejected composes as its trigger; it now uses the nesting expansion limit.
|
Warning Review limit reached
Next review available in: 6 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (10)
Comment |
|
Status
|
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it removes print-time validation on the argument that the parser is now the sole source of truth for composes acceptance — a behavior change that turns previously-failing builds into warnings — a human look at that reasoning would still be worthwhile.
Checked: the removed handle_composes / is_nested / cssparser_loc / invalid_composes_* have no remaining users; mark_import_record_unused's .expect() is safe because Specifier::ImportRecordIndex can only be produced when add_import_record had a records list; IS_UNUSED is honored by resolve_import_records (bundle_v2.rs) and linker.rs; the DisallowEntirely arm's unchanged fall-through only reaches non-tracking contexts (NoComposesCtx). The replacement PrintError trigger in cli.test.ts hits the real maximum_nesting_expansion bound.
Extended reasoning...
Overview
This PR fixes CSS modules composes handling: rejected declarations (nested rule, non-single-class selector, at-rule inside a style rule) previously warned and then failed the whole build with a generic PrintError because the printer re-validated them; accepted declarations inside a top-level @media/@supports/@layer (or a module reached via a conditional @import) failed with no warning at all because is_nested() was "indent > 2". The fix drops rejected declarations at parse time (after warning), marks any from "<file>" import record IS_UNUSED so the target isn't resolved/bundled, and reduces the printer to a plain "skip composes" — deleting handle_composes, Printer::is_nested, two PrinterErrorKind variants, and the cssparser_loc field. 6 new tests cover the three rejected shapes, top-level at-rule acceptance, conditional-@import wrapping, and rejected-from not pulling in files; cli.test.ts's CSS PrintError test is repointed at the nesting-expansion limit.
Security risks
None. No untrusted-input parsing changes, no auth/crypto/permissions surface. The one new unsafe block (mark_import_record_unused) dereferences Parser.import_records under the same documented Stacked-Borrows invariant as add_import_record immediately above it, and the .expect() is guarded by the invariant that Specifier::ImportRecordIndex only exists when add_import_record succeeded (which requires import_records.is_some()).
Level of scrutiny
Medium-high. This is a user-visible behavior change to bun build: builds that previously exited 1 now exit 0 with a warning, and files referenced by rejected composes: x from "..." are no longer resolved or bundled. The correctness argument — that the parser's ComposesState verdict is the only one the bundler ever consumed (via StyleSheet::composes), so the printer's re-check was strictly redundant-or-wrong — is well-argued and matches what I traced through css_parser.rs, but it removes a check rather than adding one, so a maintainer familiar with the CSS pipeline should confirm the analysis.
Other factors
- Verified no remaining references to the removed symbols (
is_nestedhits incss_parser.rsare an unrelated local parameter). IS_UNUSEDis respected byresolve_import_records(bundle_v2.rs:5950) and the linker loop (linker.rs:403), matching the PR's claim.- The
DisallowEntirelymatch arm still falls through and pushes the property; that path is only reached fromNoComposesCtx(style attribute /@page/ keyframes), which don't print viaStyleRule::to_css_base, so behavior there is unchanged. - Tests are thorough, use inline snapshots with exact exports/CSS, and the PR states they were run alongside the broader CSS suites plus
USE_SYSTEM_BUNverification. - The PR notes textual overlap with #38520 in the same files.
…ngs in itBundled Declaration blocks that are not style rules (@page, @counter-style, @Viewport, keyframes) reach parse_declaration_impl with DisallowEntirely. Warn about a composes there too and drop it like the other rejected shapes, so its `from` file is not bundled either. expectBundled's warning parser assumed every warning location is an absolute path under the test directory; CSS parser warnings only carry the file's basename, which made it throw. Such warnings are now keyed as "/<basename>", so the composes tests can be written as itBundled cases with the exact list of expected warnings, and rules that hold nothing but a rejected composes are asserted to disappear from the output.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes user-facing bundler behavior (builds that previously failed now succeed), removes the printer's composes validation layer, and adjusts the shared expectBundled.ts warning parser, a human look would still be worthwhile.
What was reviewed:
- Confirmed
handle_composes,Printer::is_nested,invalid_composes_*error kinds, andComposes::cssparser_lochave no remaining references after removal. - Checked
mark_import_record_unused: the.expect()onimport_recordsis safe because aSpecifier::ImportRecordIndexcan only exist ifadd_import_recordsucceeded, which requiresimport_recordsto beSome. - Traced the
errorOrWarnParserslice-offset change ("\n at ".length→" at ".length): the absolute-path branch still works because its regex searches rather than anchors, and the new bare-basename branch needs the corrected offset for its^-anchored match.
Extended reasoning...
Overview
This PR fixes CSS modules composes handling so that a rejected declaration (nested rule, non-single-class selector, or outside a style rule) is dropped at parse time with a warning instead of failing the whole stylesheet at print time. It touches 10 files: six in src/css/ (parser, printer, declaration handling, error kinds, css_modules helpers, style rule printing) and three test files plus the expectBundled.ts harness. The printer's redundant validation layer is removed entirely, along with the now-dead handle_composes, is_nested, two PrinterErrorKind variants, and the cssparser_loc field. A new Composes::discard marks the from "..." import record IS_UNUSED so the bundler neither resolves nor bundles it.
Security risks
None identified. This is CSS-modules build-time behavior with no auth, crypto, or untrusted-input parsing changes beyond what already existed. The unsafe in mark_import_record_unused follows the same pattern and SAFETY justification as the adjacent add_import_record.
Level of scrutiny
Medium-high. While the mechanism is well-explained and well-tested (seven new itBundled cases covering nested rules, non-single-class selectors, at-rules-in-rules, @keyframes/@page, rejected from imports, top-level at-rule acceptance, and @import with conditions), this is a deliberate design change: the printer no longer validates composes at all, relying on the parser having dropped every rejected declaration. The PR description argues this is correct because Bun (unlike lightningcss) records composes at parse time and the printer never emitted the property anyway — but confirming that no other code path can inject a Property::Composes into a rule the parser didn't validate is worth a human's eye. The harness change to errorOrWarnParser also affects every bundler test that asserts warnings.
Other factors
- The comment-cop bot flagged verbose comments across five files; the author trimmed them in follow-up commits (3d3908d, ca67465) and left a justified three-line note in
style.rsexplaining why the lightningcss nesting check must not be re-added. - The
cli.test.tsPrintError trigger was replaced (nesting expansion limit instead of rejectedcomposes) since the old trigger no longer errors — the test still validates the same contract. - The PR notes it overlaps textually with #38520 in
declaration.rsand the test file. - Test coverage is thorough and the PR description states each case fails on the unfixed build, plus the broader bundler warning-asserting suites were run.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes user-facing bundler behavior (builds that previously failed now succeed), removes the printer-side composes validation entirely, and touches the shared expectBundled.ts warning parser, a human look would still be worthwhile.
What was reviewed:
mark_import_record_unused's.expect()is safe —add_import_recorderrors whenimport_recordsisNone, so anImportRecordIndeximplies the pointer is set;resolve_import_records(bundle_v2.rs:6002) skipsIS_UNUSEDrecords so the droppedfromfile is neither resolved nor bundled.- The
errorOrWarnParserslice-length change ("\n at "→" at ") leaves 4 extra leading chars insourcefor absolute-path warnings, but the downstream/bun-build-tests[\/\\].../regex is unanchored so existing warning assertions parse the same; the PR ran everybundleWarnings-using suite green. NoComposesCtxreturnsDisallowEntirely, so@page/@keyframesbodies (which use plainparse_declaration) now hit the new warn-and-drop arm; the removedcssparser_locfield andis_nested()/handle_composeshad no other callers.
Extended reasoning...
Overview
This PR fixes CSS modules composes handling: rejected composes declarations (nested, non-single-class selector, or outside a style rule) are now dropped at parse time with a warning instead of being pushed into the AST and then failing the whole stylesheet at print time with a generic PrintError. It touches 6 Rust files in src/css/ (parser, printer, declaration handling, error kinds, css_modules struct) plus 3 test files. The change removes ~50 lines of print-time validation (handle_composes, Printer::is_nested, two PrinterErrorKind variants, the cssparser_loc field) and adds ~20 lines of parse-time drop logic plus a new mark_import_record_unused helper. Seven new itBundled cases with inline snapshots cover every rejected shape and the previously-broken accepted shapes (@media/@layer wrapping).
Security risks
None identified. The change operates on already-parsed CSS AST nodes; no new untrusted-input parsing paths, no filesystem or network surface. The one unsafe block added (mark_import_record_unused) follows the exact same SharedRW-provenance pattern as the adjacent add_import_record, with the same SAFETY comment referencing the field doc. The .expect() on import_records is invariant-guarded: a Specifier::ImportRecordIndex can only exist when add_import_record succeeded, which requires import_records.is_some().
Level of scrutiny
Medium-high. This is a user-facing behavior change to bun build for CSS modules — inputs that previously exited 1 now exit 0 with a warning, matching esbuild. The PR removes a validation layer entirely (print-time composes checks), which is a design decision the author justified thoroughly (the parser already decides everything the bundler exports; the printer never emitted the property; the print-time check was both redundant with the parser and stricter in ways that broke valid inputs). The reasoning is sound and matches the esbuild reference behavior, but removing validation and changing exit codes is the kind of thing a maintainer should sign off on. The expectBundled.ts change is small but affects every bundler test that asserts warnings.
Other factors
- All comment-cop bot feedback has been addressed (comments trimmed in ca67465 and 3d3908d; all threads resolved).
- Test coverage is thorough: 7 new cases covering nested rules, non-single-class selectors, at-rules-in-rules,
@keyframes/@page, rejectedfromimports (both existing and missing files), top-level@media/@supports/@layeracceptance, and@importwith conditions. Each rejected-shape test includes both a declaration sharing a rule with a real property and one alone (so the empty-rule-removal path is exercised). The author verified all seven fail on the unfixed build. - The rewritten
cli.test.tscase now uses the nesting-expansion limit as itsPrintErrortrigger (themaximum_nesting_expansionvariant), which is orthogonal to this fix and passes before and after. - The PR notes textual overlap with #38520; whichever lands second rebases.
- I verified
IS_UNUSEDis honored byresolve_import_recordsand that theerrorOrWarnParseroffset change is harmless for existing absolute-path warnings (unanchored regex).
Problem
composesdeclaration the CSS modules parser rejects (inside a nested rule, inside an at-rule nested in a rule, or on a selector that is not a single class) gets its warning, and thenbun buildstill exits 1 witherror: Failed to generate CSS for this file (PrintError).parse_declaration_impl(src/css/declaration.rs) warns but still pushes the declaration, andStyleRule::to_css_base(src/css/rules/style.rs) validatedcomposesa second time at print time (Printer::is_nested,CssModule::handle_composes) and failed the whole stylesheet.is_nested()is only "indent > 2", so an acceptedcomposesfailed with no warning at all when its rule is printed inside any block:@media (min-width: 1px) { .b { composes: c } }(accepted by the parser, exported by the bundler, then PrintError).@import "./x.module.css" layer(foo);(or a media / supports condition): the bundler wraps the file's rules in the matching@layer/@media/@supportsblock itself (prepareCssAstsForChunk.rs), so the file can never print.composes: x from "./other.module.css"kept its import record, soother.module.csswas bundled (or failed resolution) on behalf of a declaration that has no effect. The same happened, without any warning, for acomposeswritten outside a style rule of a module (@keyframes,@page, ...), which was also printed as if it were a real property.composesin a rule inside a top-level at-rule works.Fix
parse_declaration_impldrops every rejectedcomposesafter warning (the previously silent "outside a style rule" case now warns"composes" is not valid here, esbuild's wording), andComposes::discardmarks the import record of itsfrom "<file>"specifierIS_UNUSED(the existing flag for "the parser created this record and then decided it is not needed"; the bundler neither resolves nor orders such records).StyleRule::to_css_baseonly omitscomposesfrom the output.handle_composes,Printer::is_nested, the two printer error kinds andComposes::cssparser_lochad no other users and are removed.StyleSheet::composesfeedsgenerateCodeForLazyExport/scanImportsAndExports; the printer never emitted the property), so the print-time validation only ever turned an already reported warning into a build failure, or, in the wrapped cases, rejected rules the parser had correctly accepted. With rejected declarations dropped at parse time, the onlycomposesleft in the AST are accepted ones in style rules, so there is nothing left for the printer to check. This matches esbuild for.module.cssfiles, including not bundling thefromfile of a rejected declaration. (composesin plain, non-module.cssfiles is still parsed as a property and bundles itsfromfile; that is a separate pre-existing divergence, tracked separately, and untouched here.)test/bundler/expectBundled.ts: the warning parser assumed every warning location is an absolute path inside the test directory and threw on CSS parser warnings, which only carry the file's basename (at styles.module.css:1:6). Those are now keyed as/<basename>, sobundleWarningscan assert the exact list of CSS warnings. Warnings that did parse before parse exactly as before.css-module/Composes*cases intest/bundler/css/css-modules.test.ts: the three rejected shapes (each with one declaration that shares a rule with a real property and one that is alone, so the rule holding only the rejectedcomposeshas to disappear from the output),@keyframes/@page, a rejectedfromthat must neither bundle an existing file nor fail on a missing one,@media/@supports+@layeracceptance, and a module imported with conditions. All seven fail on the unfixed build (the builds exit 1, or in the@keyframes/@pagecase bundleother.module.csswithout a warning) and pass with the fix.test/bundler/cli.test.ts"fails instead of emitting a truncated stylesheet" used a rejectedcomposesas its PrintError trigger, which no longer exists; it now uses the nesting expansion limit (passes before and after, it is not the proof for this change).cargo clippy -p bun_css,cargo fmt.declaration.rsand the test file; whichever lands second rebases trivially.Background
composes:.a { composes: b }makes the JS export forathe string"b_<hash> a_<hash>". Bun implements it in the parser: when a style rule is opened,NestedRuleParser::parse_block(css_parser.rs) computes aComposesStatefrom the selector shape and nesting depth (declaration blocks that are not style rules getDisallowEntirely);parse_declaration_implthen either records the declaration intoStyleSheet::composes(Allow) or warns. The bundler builds the exports object from that map. The property itself is never printed.composeswhile printing, which is why its printer validated the selector and nesting. In Bun those checks duplicated the parser's, and the failure surfaced as a genericPrintErrorbecause the bundler discards printer error details (generateCompileResultForCssChunk.rs).ImportRecordFlags::IS_UNUSED: a record the parser created and later decided is unnecessary (TypeScript type-only imports use it).resolve_import_recordsskips such records, so they keep an invalidsource_index, andfindImportedFilesInCSSOrderonly followscomposesrecords with a valid one.@keyframesfixture only produces the@pagewarning: keyframe bodies are parsed with a freshParserOptionsthat has no logger (rules/keyframes.rs), so warnings raised there are dropped today. The declaration is still removed, which the test's stylesheet snapshot shows; making those warnings visible is tracked separately.Repro and esbuild comparison
entry.jsdoesimport styles from "./w.module.css";bun build entry.js --outdir out(bun 1.4.0 and main before this change):entry.csswith@import "./w.module.css" layer(foo);where w.module.css is just.c { color: red } .b { composes: c }-> PrintError, exit 1.esbuild 0.25 on the same inputs: the rejected shapes warn and export
{c, a, z}/{c, a, b}/{c, a}with no composition,@mediaexportsb: "w_c w_b", files named by rejected declarations are never resolved, and every build succeeds. With this change Bun produces the same exports and exit codes for all of them.Earlier revision of this PR: the first push left the "outside a style rule" case untouched and tested the rest by spawning
bun builddirectly, because the harness could not parse CSS warnings; the self-review pointed out both, which the second commit addresses.no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/cli.test.ts