Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_exact_literal_coercion.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 exact JavaScript literal property keys in static lookups and generated finite class maps, independently of CSS numeric value normalization. Only fold standalone unary string coercion when its spelling is proven to agree with JavaScript Number; other values retain the existing conservative dynamic or located build-error path without a styling runtime.",
"date": "2026-10-17T00:00:00.000Z"
}
9 changes: 6 additions & 3 deletions libs/extractor/src/build_time_values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ use oxc_syntax::symbol::SymbolId;
use rustc_hash::{FxHashMap, FxHashSet};

use crate::stylex::StylexFunction;
use crate::utils::{binding_root, get_string_by_literal_expression, unwrap_syntax_only};
use crate::utils::{
binding_root, get_key_by_literal_expression, get_string_by_literal_expression,
unwrap_syntax_only,
};
use crate::{ExtractOption, ModuleResolver};

/// What code reads outside itself
Expand Down Expand Up @@ -765,7 +768,7 @@ impl<'a> Visit<'a> for Reads<'_> {
&mut self,
member: &oxc_ast::ast::ComputedMemberExpression<'a>,
) {
if let Some(key) = get_string_by_literal_expression(&member.expression) {
if let Some(key) = get_key_by_literal_expression(&member.expression) {
self.member(&member.object, &key);
if self.is_global(&member.object, "Math") {
return;
Expand All @@ -778,7 +781,7 @@ impl<'a> Visit<'a> for Reads<'_> {
match unwrap_syntax_only(&call.callee) {
// A method chosen at runtime may be any
Expression::ComputedMemberExpression(member) => {
self.impure |= get_string_by_literal_expression(&member.expression).is_none();
self.impure |= get_key_by_literal_expression(&member.expression).is_none();
}
// Exact only without a radix
Expression::StaticMemberExpression(member) if member.property.name == "toString" => {
Expand Down
6 changes: 3 additions & 3 deletions libs/extractor/src/css_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::collections::BTreeMap;
use std::fmt::Write as _;

use crate::utils::{
get_str_by_property_key, get_string_by_literal_expression, readable_code, runtime_value_error,
wrap_direct_call,
get_key_by_literal_expression, get_str_by_property_key, get_string_by_literal_expression,
readable_code, runtime_value_error, wrap_direct_call,
};
use css::{
at_rule::split_at_rule_key,
Expand Down Expand Up @@ -79,7 +79,7 @@ pub(crate) fn theme_var_reference(expr: &Expression<'_>) -> Option<String> {
}
// `theme.space[2]` and `theme.colors['brand']` read a key the build knows
Expression::ComputedMemberExpression(member) => {
path.push(get_string_by_literal_expression(&member.expression)?);
path.push(get_key_by_literal_expression(&member.expression)?);
cursor = &member.object;
}
Expression::Identifier(ident) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use crate::{
},
},
utils::{
get_number_by_literal_expression, get_str_by_property_key,
get_string_by_literal_expression, get_string_by_property_key, readable_code,
get_key_by_literal_expression, get_number_by_literal_expression, get_str_by_property_key,
get_string_by_property_key, readable_code,
},
};
use css::style_selector::StyleSelector;
Expand Down Expand Up @@ -136,7 +136,7 @@ pub(super) fn extract_style_from_member_expression<'a>(
&& !obj.properties.is_empty()
{
let mut map = BTreeMap::new();
if let Some(k) = get_string_by_literal_expression(mem_expression) {
if let Some(k) = get_key_by_literal_expression(mem_expression) {
// The last property written for the key gives it, unless a spread
// after it may replace it
let written = obj.properties.iter().rposition(|p| {
Expand Down
4 changes: 2 additions & 2 deletions libs/extractor/src/extractor/styled_record_selection.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::Preparation;
use crate::utils::{
get_str_by_property_key, get_string_by_literal_expression, js_number_string, unwrap_syntax_only,
get_key_by_literal_expression, get_str_by_property_key, js_number_string, unwrap_syntax_only,
};
use oxc_allocator::{CloneIn, FromIn, GetAllocator, Vec as ArenaVec};
use oxc_ast::ast::{
Expand Down Expand Up @@ -35,7 +35,7 @@ pub(super) fn direct_lookup(object: &Expression<'_>, index: &Expression<'_>) ->
let Some(key) = literal_key(index) else {
return false;
};
if get_string_by_literal_expression(index).as_deref() != Some(key.as_str()) {
if get_key_by_literal_expression(index).as_deref() != Some(key.as_str()) {
return false;
}
match object {
Expand Down
30 changes: 30 additions & 0 deletions libs/extractor/src/extractor/styled_record_selection_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,36 @@ use rstest::rstest;
use super::{Expression, Preparation, direct_lookup};
use crate::{extractor::styled_record_evaluation::Evaluations, utils::unwrap_syntax_only};

#[rstest]
#[case::leading_zero("({'05':'red','5':'blue'})['05'];", true)]
#[case::canonical_integer("({'05':'red','5':'blue'})['5'];", true)]
#[case::decimal("({'0.50':'red','0.5':'blue'})['0.50'];", true)]
#[case::canonical_decimal("({'0.50':'red','0.5':'blue'})['0.5'];", true)]
#[case::ts_as_index("({color:'red'})['color' as const];", false)]
fn direct_lookup_preserves_ast_when_checking_exact_index_agreement(
#[case] source: &str,
#[case] expected: bool,
) {
// Given: real parsed object/index syntax, including a TS-as index wrapper.
let allocator = Allocator::default();
let parsed = Parser::new(&allocator, source, SourceType::tsx()).parse();
assert_eq!(parsed.diagnostics.len(), 0);
let Statement::ExpressionStatement(statement) = &parsed.program.body[0] else {
panic!("test fixture must be an expression");
};
let Expression::ComputedMemberExpression(member) = &statement.expression else {
panic!("test fixture must be a computed member");
};
let original = format!("{member:?}");

// When: the caller supplies the syntax-unwrapped object as Preparation::value does.
let accepted = direct_lookup(unwrap_syntax_only(&member.object), &member.expression);

// Then: exact object keys agree, but the unsupported TS-as index stays native.
assert_eq!(accepted, expected, "{source}");
assert_eq!(format!("{member:?}"), original);
}

#[test]
fn direct_lookup_rejects_when_object_is_not_literal() {
// Given: a literal key that passes the key checks, but an unknown object.
Expand Down
Loading
Loading