Skip to content
Open
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_local_style_objects.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "A style object declared with const inside a function and passed only to styling APIs now compiles when its entries are written out, including Emotion, styled-components and vanilla-extract aliases. Successful member, primitive and constant-condition folding accounts for erased semantic references, so mutation or extra-use paths keep the existing located refusal while safe style-only member reads compile. css() and styled() compose known conditional rule arrays with the controller read once. Whole-rule props callbacks preserve their original invocation scope, destructuring, defaults, rest and authored argument evaluation order, including covered reads and empty/static-only results; fixed dynamic values become CSS variables without a runtime rule-object interpreter. Nonzero numbers remain px lengths except for unitless properties, and proven theme paths remain static CSS variables. Implicit actual-props theme reads through own arguments or a first rest formal retain located errors with correct catch, loop, switch and var binding ownership. Named whole-rule callbacks that refer to their own binding, including metadata reads, now give a located error naming the callback and explaining that recursion is not compiled; a same-named type parameter cannot hide that value self-reference, while type-only names and actual value shadows still compile. Repeated preparation checks, traversal and error buffering are consolidated without changing output or existing diagnostics. Opaque calls in Emotion ClassNames cx remain located errors until their result is proven unable to carry Devup style classes; only that allowance is deferred. Other located errors remain: attrs chained after styles, callbacks without one immediate explicit rule object, and local style objects also read elsewhere or holding unprovable style structure. No styling runtime or helper is added.",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_w62m_745_repair.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Preserve extractor callback, theme-read and rule-array behavior while removing redundant preparation and unreachable analysis paths. Add Linux-coverable regression cases without coverage exclusions. No new build errors.",
"date": "2026-10-21T00:00:00.000Z"
}
81 changes: 81 additions & 0 deletions libs/extractor/src/consumed_references.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//! Semantic references removed by successful constant inlining.

use oxc_ast::ast::{Expression, IdentifierReference};
use oxc_ast_visit::Visit;
use oxc_semantic::Scoping;
use oxc_syntax::{reference::ReferenceId, symbol::SymbolId};
use rustc_hash::{FxHashMap, FxHashSet};

/// Counts erased references once, even when a retained AST node is revisited.
pub(super) struct ConsumedReferences<'s> {
scoping: &'s Scoping,
seen: FxHashSet<ReferenceId>,
by_symbol: FxHashMap<SymbolId, usize>,
}

impl<'s> ConsumedReferences<'s> {
pub(super) fn new(scoping: &'s Scoping) -> Self {
Self {
scoping,
seen: FxHashSet::default(),
by_symbol: FxHashMap::default(),
}
}

pub(super) fn count(&self, symbol: SymbolId) -> usize {
self.by_symbol.get(&symbol).copied().unwrap_or(0)
}

/// Call only for a subtree about to be erased, never for a folding probe.
pub(super) fn consume(&mut self, expression: &Expression<'_>) {
self.visit_expression(expression);
}
}

impl<'a> Visit<'a> for ConsumedReferences<'_> {
fn visit_identifier_reference(&mut self, identifier: &IdentifierReference<'a>) {
if let Some(reference) = identifier.reference_id.get()
&& let Some(symbol) = self.scoping.get_reference(reference).symbol_id()
&& self.seen.insert(reference)
{
*self.by_symbol.entry(symbol).or_default() += 1;
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use oxc_ast::ast::Statement;

#[test]
fn references_count_once_when_a_computed_subtree_is_revisited() {
// Given: two distinct references to one binding in a computed member.
let allocator = oxc_allocator::Allocator::default();
let program = oxc_parser::Parser::new(
&allocator,
"const holder = {}; holder[holder.key];",
oxc_span::SourceType::ts(),
)
.parse()
.program;
let scoping = oxc_semantic::SemanticBuilder::new()
.build(&program)
.semantic
.into_scoping();
let symbol = scoping
.symbol_ids()
.find(|symbol| scoping.symbol_name(*symbol) == "holder")
.unwrap_or_else(|| panic!("fixture must declare holder"));
let Some(Statement::ExpressionStatement(statement)) = program.body.last() else {
panic!("fixture must end with the computed read");
};
let mut references = ConsumedReferences::new(&scoping);
// When: the same semantic subtree is accounted more than once.
for _ in 0..2 {
references.consume(&statement.expression);
}
// Then: distinct operands count, repeated visits do not.
assert_eq!(references.count(symbol), 2);
}
}
144 changes: 103 additions & 41 deletions libs/extractor/src/css_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ use oxc_ast::ast::Expression;
use oxc_ast::ast::{TemplateElement, TemplateLiteral};
use oxc_ast::builder::AstBuilder;

#[path = "implicit_props_theme.rs"]
mod implicit_props_theme;

#[path = "theme_callback.rs"]
mod theme_callback;

pub(crate) use implicit_props_theme::reads as reads_implicit_props_theme;

use crate::extract_style::{
extract_dynamic_style::ExtractDynamicStyle, extract_static_style::ExtractStaticStyle,
extract_style_value::ExtractStyleValue,
Expand Down Expand Up @@ -57,24 +65,43 @@ pub fn rm_last_semi_colon(code: &str) -> &str {
/// style static; left alone the arrow would be called with the element's props,
/// which carry no `theme`, and silently evaluate to `undefined`.
pub(crate) fn theme_var_reference(expr: &Expression<'_>) -> Option<String> {
let Expression::ArrowFunctionExpression(arrow) = expr else {
return None;
use oxc_ast_visit::Visit;
struct Defaults(bool);
impl<'a> Visit<'a> for Defaults {
fn visit_assignment_pattern(&mut self, _: &oxc_ast::ast::AssignmentPattern<'a>) {
self.0 = true;
}
}
let (params, body) = match expr {
Expression::ArrowFunctionExpression(arrow) if !arrow.r#async => {
(&arrow.params, arrow.body.as_expression()?)
}
Expression::FunctionExpression(function) if !function.r#async && !function.generator => (
&function.params,
crate::css_prop::returned(function.body.as_deref()?)?,
),
_ => return None,
};
let [param] = arrow.params.items.as_slice() else {
let [param] = params.items.as_slice() else {
return None;
};
let root = match &param.pattern {
BindingPattern::BindingIdentifier(ident) => ThemeRoot::Props(ident.name.as_str()),
BindingPattern::ObjectPattern(pattern) => pattern
.properties
.iter()
.any(|p| get_str_by_property_key(&p.key).is_some_and(|key| key == "theme"))
.then_some(ThemeRoot::Theme)?,
_ => return None,
};
let mut defaults = Defaults(false);
defaults.visit_binding_pattern(&param.pattern);
if params.rest.is_some() || param.initializer.is_some() || defaults.0 {
return None;
}
theme_leaf_reference(params, body)
}

/// Map a leaf without erasing its enclosing callback's parameter evaluation.
pub(crate) fn theme_leaf_reference(
params: &oxc_ast::ast::FormalParameters<'_>,
body: &Expression<'_>,
) -> Option<String> {
let root = theme_root(&params.items.first()?.pattern)?;

let mut path: Vec<Cow<'_, str>> = Vec::new();
let mut cursor = arrow.body.as_expression()?;
let mut cursor = body;
loop {
match cursor {
Expression::StaticMemberExpression(member) => {
Expand All @@ -88,8 +115,7 @@ pub(crate) fn theme_var_reference(expr: &Expression<'_>) -> Option<String> {
}
Expression::Identifier(ident) => {
let matches_root = match root {
ThemeRoot::Props(name) => ident.name.as_str() == name,
ThemeRoot::Theme => ident.name.as_str() == "theme",
ThemeRoot::Props(name) | ThemeRoot::Theme(name) => ident.name.as_str() == name,
};
if !matches_root {
return None;
Expand All @@ -116,7 +142,7 @@ pub(crate) fn theme_var_reference(expr: &Expression<'_>) -> Option<String> {
pub(crate) fn reads_unmapped_theme(expr: &Expression<'_>) -> bool {
use oxc_ast_visit::{Visit, walk};
struct ThemeReads<'n> {
props: Option<&'n str>,
root: ThemeRoot<'n>,
found: bool,
}
impl<'a> Visit<'a> for ThemeReads<'_> {
Expand All @@ -125,48 +151,61 @@ pub(crate) fn reads_unmapped_theme(expr: &Expression<'_>) -> bool {
it: &oxc_ast::ast::StaticMemberExpression<'a>,
) {
if it.property.name == "theme"
&& matches!(&it.object, Expression::Identifier(object) if Some(object.name.as_str()) == self.props)
&& matches!(self.root, ThemeRoot::Props(name)
if matches!(&it.object, Expression::Identifier(object) if object.name == name))
{
self.found = true;
}
walk::walk_static_member_expression(self, it);
}
fn visit_computed_member_expression(
&mut self,
it: &oxc_ast::ast::ComputedMemberExpression<'a>,
) {
if get_string_by_literal_expression(&it.expression).is_some_and(|key| key == "theme")
&& matches!(self.root, ThemeRoot::Props(name)
if matches!(&it.object, Expression::Identifier(object) if object.name == name))
{
self.found = true;
}
walk::walk_computed_member_expression(self, it);
}
fn visit_identifier_reference(&mut self, it: &oxc_ast::ast::IdentifierReference<'a>) {
if self.props.is_none() && it.name == "theme" {
if matches!(self.root, ThemeRoot::Theme(name) if it.name == name) {
self.found = true;
}
}
}
let Expression::ArrowFunctionExpression(arrow) = expr else {
if reads_implicit_props_theme(expr) {
return true;
}
let Some(theme_callback::Callback { params, body }) = theme_callback::Callback::read(expr)
else {
return false;
};
let [param] = arrow.params.items.as_slice() else {
let Some(param) = params.items.first() else {
return false;
};
let props = match &param.pattern {
BindingPattern::BindingIdentifier(ident) => Some(ident.name.as_str()),
BindingPattern::ObjectPattern(pattern)
if pattern
.properties
.iter()
.any(|p| get_str_by_property_key(&p.key).is_some_and(|key| key == "theme")) =>
{
None
}
_ => return false,
let Some(root) = theme_root(&param.pattern) else {
return false;
};
if theme_var_reference(expr).is_some() {
return false;
}
let mut reads = ThemeReads {
props,
found: false,
};
match &arrow.body {
oxc_ast::ast::ArrowFunctionBody::FunctionBody(body) => reads.visit_function_body(body),
body => {
if let Some(expression) = body.as_expression() {
reads.visit_expression(expression);
let mut reads = ThemeReads { root, found: false };
reads.visit_formal_parameters(params);
match body {
theme_callback::Body::Arrow(body) => match body {
oxc_ast::ast::ArrowFunctionBody::FunctionBody(body) => reads.visit_function_body(body),
body => {
if let Some(expression) = body.as_expression() {
reads.visit_expression(expression);
}
}
},
theme_callback::Body::Function(body) => {
if let Some(body) = body {
reads.visit_function_body(body);
}
}
}
Expand All @@ -185,9 +224,32 @@ pub(crate) fn unmapped_theme_error(api: &str, expr: &Expression<'_>) -> (u32, St
)
}

#[derive(Clone, Copy)]
enum ThemeRoot<'a> {
Props(&'a str),
Theme,
Theme(&'a str),
}

/// The props parameter or the local binding destructured from its theme.
fn theme_root<'p>(pattern: &'p BindingPattern<'_>) -> Option<ThemeRoot<'p>> {
match pattern {
BindingPattern::BindingIdentifier(identifier) => {
Some(ThemeRoot::Props(identifier.name.as_str()))
}
BindingPattern::ObjectPattern(object) => object.properties.iter().find_map(|property| {
let mut value = &property.value;
while let BindingPattern::AssignmentPattern(default) = value {
value = &default.left;
}
let BindingPattern::BindingIdentifier(identifier) = value else {
return None;
};
get_str_by_property_key(&property.key)
.is_some_and(|key| key == "theme")
.then_some(ThemeRoot::Theme(identifier.name.as_str()))
}),
_ => None,
}
}

/// Convert a dynamic template-literal expression into identifier code,
Expand Down
36 changes: 16 additions & 20 deletions libs/extractor/src/extractor/extract_style_from_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -639,20 +639,21 @@ pub(crate) fn extract_with_policy<'a, const LOCATE: bool>(
}
// `styled.div({ color: (p) => p.theme.brand })` — the object spelling of
// the template interpolation, resolved to the same build-time `var()`.
Expression::ArrowFunctionExpression(_) => match (name, theme_var_reference(expression))
{
(Some(name), Some(reference)) => ExtractResult {
styles: create_static_styles(
name,
&reference,
&[level],
selector,
ThemeTokenResolution::default(),
),
..ExtractResult::default()
},
_ => unreadable(expression),
},
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => {
match (name, theme_var_reference(expression)) {
(Some(name), Some(reference)) => ExtractResult {
styles: create_static_styles(
name,
&reference,
&[level],
selector,
ThemeTokenResolution::default(),
),
..ExtractResult::default()
},
_ => unreadable(expression),
}
}
// Each variant is kept on its own line so per-line coverage
// tools (tarpaulin on CI) can attribute the hit to the exact
// pattern being exercised.
Expand Down Expand Up @@ -1117,12 +1118,7 @@ pub(crate) fn extract_with_policy<'a, const LOCATE: bool>(
Some(_) if matches!(expression, Expression::NullLiteral(_)) => {
ExtractResult::default()
}
Some(_)
if matches!(
expression,
Expression::FunctionExpression(_) | Expression::ClassExpression(_)
) =>
{
Some(_) if matches!(expression, Expression::ClassExpression(_)) => {
unreadable(expression)
}
// A sequence keeps its parentheses, as its code is written into
Expand Down
Loading
Loading