Skip to content

Commit 0c9bd8a

Browse files
committed
Fix mutable variable tracking, throw in exceptional case, fix keyframes, fix css builders
1 parent 320d2cd commit 0c9bd8a

15 files changed

Lines changed: 794 additions & 309 deletions

packages/swc-plugin/src/lib.rs

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,72 @@ impl Default for CompiledOptions {
118118
}
119119
}
120120

121+
122+
123+
/// Visitor for collecting variable information only (no CSS processing)
124+
struct VariableInfoCollector<'a> {
125+
state: &'a mut TransformState,
126+
current_var_decl_kind: Option<VarDeclKind>,
127+
}
128+
129+
impl<'a> VariableInfoCollector<'a> {
130+
fn new(state: &'a mut TransformState) -> Self {
131+
Self {
132+
state,
133+
current_var_decl_kind: None,
134+
}
135+
}
136+
}
137+
138+
impl<'a> VisitMut for VariableInfoCollector<'a> {
139+
fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
140+
// Set the current variable declaration kind
141+
self.current_var_decl_kind = Some(n.kind);
142+
143+
n.visit_mut_children_with(self);
144+
145+
// Clear the current variable declaration kind
146+
self.current_var_decl_kind = None;
147+
}
148+
149+
fn visit_mut_var_declarator(&mut self, n: &mut VarDeclarator) {
150+
// Track variable declarations
151+
if let Pat::Ident(ident) = &n.name {
152+
let var_name = ident.id.sym.to_string();
153+
154+
// Track the declaration kind
155+
if let Some(decl_kind) = self.current_var_decl_kind {
156+
self.state.variable_declaration_kinds.insert(var_name.clone(), decl_kind);
157+
}
158+
}
159+
160+
n.visit_mut_children_with(self);
161+
}
162+
163+
fn visit_mut_assign_expr(&mut self, n: &mut AssignExpr) {
164+
// Track mutations
165+
match &n.left {
166+
AssignTarget::Simple(SimpleAssignTarget::Ident(ident)) => {
167+
let var_name = ident.id.sym.to_string();
168+
self.state.mutated_variables.insert(var_name);
169+
}
170+
_ => {}
171+
}
172+
173+
n.visit_mut_children_with(self);
174+
}
175+
176+
fn visit_mut_update_expr(&mut self, n: &mut UpdateExpr) {
177+
// Track mutations
178+
if let Expr::Ident(ident) = &*n.arg {
179+
let var_name = ident.sym.to_string();
180+
self.state.mutated_variables.insert(var_name);
181+
}
182+
183+
n.visit_mut_children_with(self);
184+
}
185+
}
186+
121187
/// Main transformation context for the Compiled SWC plugin.
122188
///
123189
/// This struct maintains the state throughout the transformation process,
@@ -133,6 +199,8 @@ pub struct CompiledTransform {
133199
pub state: TransformState,
134200
/// Whether any transformations were applied
135201
pub had_transformations: bool,
202+
/// Current variable declaration kind (let, const, var) being processed
203+
pub current_var_decl_kind: Option<VarDeclKind>,
136204
}
137205

138206
impl CompiledTransform {
@@ -165,6 +233,12 @@ impl CompiledTransform {
165233
// For now, we rely on the string-based approach in the test infrastructure
166234
}
167235

236+
/// Collect variable declarations and mutations without processing CSS
237+
fn collect_variable_info(&mut self, module: &mut Module) {
238+
let mut collector = VariableInfoCollector::new(&mut self.state);
239+
module.visit_mut_with(&mut collector);
240+
}
241+
168242
/// Process imports and pragmas from the module
169243
fn process_imports_and_pragmas(&mut self, module: &mut Module) {
170244
let mut imports_to_remove = Vec::new();
@@ -903,10 +977,16 @@ impl VisitMut for CompiledTransform {
903977
}
904978

905979
fn visit_mut_module(&mut self, n: &mut Module) {
906-
// First pass: Check for JSX pragmas and compiled imports
980+
// Check for JSX pragmas and compiled imports
907981
self.process_imports_and_pragmas(n);
908982

909-
// Second pass: Process child nodes for transformations
983+
// First pass: collect variable declarations and mutations without processing CSS
984+
self.collect_variable_info(n);
985+
986+
// Rebuild the variable context excluding mutated variables
987+
self.state.variable_context = crate::utils::variable_context::build_variable_context_from_module_with_mutations(n, &self.state.mutated_variables);
988+
989+
// Second pass: process child nodes for transformations with correct variable context
910990
n.visit_mut_children_with(self);
911991

912992
// Finalize the module
@@ -932,7 +1012,7 @@ impl VisitMut for CompiledTransform {
9321012
// Handle different types of tagged templates
9331013
if visitors::styled::visit_styled_tagged_template(n, &mut self.state, &mut self.collected_css_sheets) {
9341014
self.had_transformations = true;
935-
} else if visitors::keyframes::visit_keyframes_tagged_template(n, &mut self.state) {
1015+
} else if visitors::keyframes::visit_keyframes_tagged_template(n, &mut self.state, &mut self.collected_css_sheets) {
9361016
self.had_transformations = true;
9371017
}
9381018

@@ -952,11 +1032,30 @@ impl VisitMut for CompiledTransform {
9521032
n.visit_mut_children_with(self);
9531033
}
9541034

1035+
fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
1036+
// Store the current variable declaration kind
1037+
self.current_var_decl_kind = Some(n.kind);
1038+
1039+
// Visit children (including declarators)
1040+
n.visit_mut_children_with(self);
1041+
1042+
// Clear the current variable declaration kind
1043+
self.current_var_decl_kind = None;
1044+
}
1045+
9551046
fn visit_mut_var_declarator(&mut self, n: &mut VarDeclarator) {
9561047
// Track local variables before visiting children
9571048
if let Pat::Ident(ident) = &n.name {
9581049
if let Some(init_expr) = &n.init {
9591050
let var_name = ident.id.sym.to_string();
1051+
1052+
// Track the declaration kind
1053+
if let Some(decl_kind) = self.current_var_decl_kind {
1054+
self.state.variable_declaration_kinds.insert(var_name.clone(), decl_kind);
1055+
}
1056+
1057+
// Track all variables for static analysis initially
1058+
// We'll filter out actually mutated ones during evaluation
9601059
self.track_local_variable(&var_name, init_expr);
9611060
}
9621061
}
@@ -970,6 +1069,34 @@ impl VisitMut for CompiledTransform {
9701069
}
9711070
}
9721071

1072+
/// Track assignments to variables (mutations)
1073+
fn visit_mut_assign_expr(&mut self, n: &mut AssignExpr) {
1074+
// Check if this is an assignment to a variable
1075+
match &n.left {
1076+
AssignTarget::Simple(SimpleAssignTarget::Ident(ident)) => {
1077+
let var_name = ident.id.sym.to_string();
1078+
// Mark this variable as mutated
1079+
self.state.mutated_variables.insert(var_name);
1080+
}
1081+
_ => {}
1082+
}
1083+
1084+
n.visit_mut_children_with(self);
1085+
}
1086+
1087+
/// Track update expressions (mutations like var++, ++var, var--, --var)
1088+
fn visit_mut_update_expr(&mut self, n: &mut UpdateExpr) {
1089+
if let Expr::Ident(ident) = &*n.arg {
1090+
let var_name = ident.sym.to_string();
1091+
// Mark this variable as mutated
1092+
self.state.mutated_variables.insert(var_name);
1093+
}
1094+
1095+
n.visit_mut_children_with(self);
1096+
}
1097+
1098+
1099+
9731100

9741101
}
9751102

@@ -989,6 +1116,7 @@ pub fn process_transform(program: Program, metadata: TransformPluginProgramMetad
9891116
css_content_to_var: HashMap::new(),
9901117
state,
9911118
had_transformations: false,
1119+
current_var_decl_kind: None,
9921120
};
9931121

9941122
program.fold_with(&mut as_folder(&mut transform))

packages/swc-plugin/src/test_utils.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,14 @@ pub fn transform_with_compiled(code: &str, mut options: TestTransformOptions) ->
125125
state.compiled_imports = Some(crate::types::CompiledImports::new());
126126
}
127127

128-
// Build variable context from the module
129-
state.variable_context = crate::utils::variable_context::build_variable_context_from_module(&module);
130-
131128
// Create our Compiled transformation instance
132129
let mut compiled_transform = CompiledTransform {
133130
options: compiled_opts,
134131
collected_css_sheets: Vec::new(),
135132
css_content_to_var: HashMap::new(),
136133
state,
137134
had_transformations: false,
135+
current_var_decl_kind: None,
138136
};
139137

140138
// Apply our Compiled transformation

packages/swc-plugin/src/types.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
use std::collections::HashMap;
2+
use std::collections::{HashMap, HashSet};
33
use crate::utils::module_resolver::{ModuleResolver, ExportValue};
44
use crate::utils::variable_context::VariableContext;
55
use swc_core::ecma::ast::*;
@@ -57,6 +57,12 @@ pub struct TransformState {
5757
/// Local variables in the current scope (for static analysis)
5858
pub local_variables: HashMap<String, ExportValue>,
5959

60+
/// Variable declaration kinds (let, const, var) for let variable detection
61+
pub variable_declaration_kinds: HashMap<String, VarDeclKind>,
62+
63+
/// Variables that have been actually mutated (assigned to after declaration)
64+
pub mutated_variables: HashSet<String>,
65+
6066
/// Variable context for expression evaluation
6167
pub variable_context: VariableContext,
6268

@@ -162,6 +168,8 @@ impl Default for TransformState {
162168
resolved_expressions: HashMap::new(),
163169
css_expressions_to_resolve: Vec::new(),
164170
local_variables: HashMap::new(),
171+
variable_declaration_kinds: HashMap::new(),
172+
mutated_variables: HashSet::new(),
165173
variable_context: VariableContext::new(),
166174
debug_messages: Vec::new(),
167175
}

packages/swc-plugin/src/utils/css_builder.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,15 @@ fn generate_css_variable_name(expr: &Expr) -> String {
271271
format!("--_{}", hash.chars().take(8).collect::<String>())
272272
}
273273

274+
/// Converts a CSS object expression to CSS string with variable support and state (includes mutated let variable check)
275+
pub fn object_expression_to_css_with_state(obj_expr: &ObjectLit, state: &TransformState) -> (String, Vec<Variable>) {
276+
// Check for actually mutated let variables first
277+
crate::utils::expression_evaluator::check_for_mutated_let_variables(&Expr::Object(obj_expr.clone()), &state.variable_declaration_kinds, &state.mutated_variables);
278+
279+
// If no mutated let variables found, proceed with normal processing
280+
object_expression_to_css_with_variables_and_context(obj_expr, &state.variable_context)
281+
}
282+
274283
/// Converts a CSS object expression to CSS string with variable support and context
275284
pub fn object_expression_to_css_with_variables_and_context(obj_expr: &ObjectLit, context: &VariableContext) -> (String, Vec<Variable>) {
276285
let mut css = String::new();
@@ -572,6 +581,15 @@ pub fn build_atomic_css_from_object(obj_expr: &ObjectLit) -> Option<AtomicCSSOut
572581
})
573582
}
574583

584+
/// Builds CSS output from an expression with state (includes mutated let variable check)
585+
pub fn build_css_from_expression_with_state(expr: &Expr, state: &TransformState) -> Option<CSSOutput> {
586+
// Check for actually mutated let variables first
587+
crate::utils::expression_evaluator::check_for_mutated_let_variables(expr, &state.variable_declaration_kinds, &state.mutated_variables);
588+
589+
// If no mutated let variables found, proceed with normal processing
590+
build_css_from_expression_with_context(expr, &state.variable_context)
591+
}
592+
575593
/// Builds CSS output from an expression with variable support (with context)
576594
pub fn build_css_from_expression_with_context(expr: &Expr, context: &VariableContext) -> Option<CSSOutput> {
577595
match expr {

packages/swc-plugin/src/utils/expression_evaluator.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,103 @@
11
use swc_core::ecma::ast::*;
22
use crate::utils::variable_context::VariableContext;
3+
use crate::types::TransformState;
4+
use std::collections::HashMap;
5+
6+
/// Check if an expression contains any actually mutated let variables
7+
/// Panics with a helpful error message if a mutated let variable is found
8+
pub fn check_for_mutated_let_variables(expr: &Expr, variable_declaration_kinds: &HashMap<String, VarDeclKind>, mutated_variables: &std::collections::HashSet<String>) {
9+
match expr {
10+
Expr::Ident(ident) => {
11+
let var_name = ident.sym.to_string();
12+
// Only panic if it's a let variable AND it has been mutated
13+
if let Some(decl_kind) = variable_declaration_kinds.get(&var_name) {
14+
if matches!(decl_kind, VarDeclKind::Let) && mutated_variables.contains(&var_name) {
15+
panic!("Mutable variable '{}' cannot be used in CSS expressions. Compiled only supports constant values in CSS at compile time. Consider using CSS variables or move the dynamic logic to a style prop.", var_name);
16+
}
17+
}
18+
}
19+
Expr::Bin(bin_expr) => {
20+
check_for_mutated_let_variables(&bin_expr.left, variable_declaration_kinds, mutated_variables);
21+
check_for_mutated_let_variables(&bin_expr.right, variable_declaration_kinds, mutated_variables);
22+
}
23+
Expr::Unary(unary_expr) => {
24+
check_for_mutated_let_variables(&unary_expr.arg, variable_declaration_kinds, mutated_variables);
25+
}
26+
Expr::Cond(cond_expr) => {
27+
check_for_mutated_let_variables(&cond_expr.test, variable_declaration_kinds, mutated_variables);
28+
check_for_mutated_let_variables(&cond_expr.cons, variable_declaration_kinds, mutated_variables);
29+
check_for_mutated_let_variables(&cond_expr.alt, variable_declaration_kinds, mutated_variables);
30+
}
31+
Expr::Member(member_expr) => {
32+
check_for_mutated_let_variables(&member_expr.obj, variable_declaration_kinds, mutated_variables);
33+
if let MemberProp::Computed(computed) = &member_expr.prop {
34+
check_for_mutated_let_variables(&computed.expr, variable_declaration_kinds, mutated_variables);
35+
}
36+
}
37+
Expr::Tpl(tpl) => {
38+
for expr in &tpl.exprs {
39+
check_for_mutated_let_variables(expr, variable_declaration_kinds, mutated_variables);
40+
}
41+
}
42+
Expr::Call(call_expr) => {
43+
if let Callee::Expr(expr) = &call_expr.callee {
44+
check_for_mutated_let_variables(expr, variable_declaration_kinds, mutated_variables);
45+
}
46+
for arg in &call_expr.args {
47+
check_for_mutated_let_variables(&arg.expr, variable_declaration_kinds, mutated_variables);
48+
}
49+
}
50+
Expr::Object(obj_lit) => {
51+
for prop in &obj_lit.props {
52+
if let PropOrSpread::Prop(prop_box) = prop {
53+
match &**prop_box {
54+
Prop::KeyValue(kv) => {
55+
if let PropName::Computed(computed) = &kv.key {
56+
check_for_mutated_let_variables(&computed.expr, variable_declaration_kinds, mutated_variables);
57+
}
58+
check_for_mutated_let_variables(&kv.value, variable_declaration_kinds, mutated_variables);
59+
}
60+
Prop::Method(method) => {
61+
if let PropName::Computed(computed) = &method.key {
62+
check_for_mutated_let_variables(&computed.expr, variable_declaration_kinds, mutated_variables);
63+
}
64+
}
65+
_ => {}
66+
}
67+
}
68+
}
69+
}
70+
Expr::Array(arr_lit) => {
71+
for elem in &arr_lit.elems {
72+
if let Some(expr_or_spread) = elem {
73+
check_for_mutated_let_variables(&expr_or_spread.expr, variable_declaration_kinds, mutated_variables);
74+
}
75+
}
76+
}
77+
// Literal values are safe
78+
Expr::Lit(_) => {}
79+
// Other expressions we don't need to check deeply
80+
_ => {}
81+
}
82+
}
383

484
/// Attempts to statically evaluate an expression to a literal value
585
/// Returns Some(Expr) if evaluation was successful, None otherwise
686
pub fn evaluate_expression(expr: &Expr) -> Option<Expr> {
787
evaluate_expression_with_context(expr, &VariableContext::new())
888
}
989

90+
/// Attempts to statically evaluate an expression with a transform state
91+
/// Checks for mutated let variables first and panics if found
92+
/// Returns Some(Expr) if evaluation was successful, None otherwise
93+
pub fn evaluate_expression_with_state(expr: &Expr, state: &TransformState) -> Option<Expr> {
94+
// Check for actually mutated let variables first and panic if found
95+
check_for_mutated_let_variables(expr, &state.variable_declaration_kinds, &state.mutated_variables);
96+
97+
// If no mutated let variables, proceed with normal evaluation
98+
evaluate_expression_with_context(expr, &state.variable_context)
99+
}
100+
10101
/// Attempts to statically evaluate an expression with a variable context
11102
/// Returns Some(Expr) if evaluation was successful, None otherwise
12103
pub fn evaluate_expression_with_context(expr: &Expr, context: &VariableContext) -> Option<Expr> {

0 commit comments

Comments
 (0)