feat: v2 codegen hardening — scoped local variables
Replace flat HashSet<String> local_vars with Vec<HashSet<String>> scope stack. For-in loops push/pop scopes for loop variables. Nested loops no longer clobber outer loop variable visibility. Methods: push_scope(), pop_scope(), is_local_var() 110 tests, 0 failures.
This commit is contained in:
parent
6368b798cf
commit
55ec9353ae
1 changed files with 30 additions and 17 deletions
|
|
@ -8,8 +8,8 @@ pub struct JsEmitter {
|
|||
output: String,
|
||||
indent: usize,
|
||||
node_id_counter: usize,
|
||||
/// Non-signal local variables (e.g., for-in loop vars)
|
||||
local_vars: HashSet<String>,
|
||||
/// Scoped local variables (e.g., for-in loop vars). Stack of scopes for nesting.
|
||||
local_var_scopes: Vec<HashSet<String>>,
|
||||
}
|
||||
|
||||
impl JsEmitter {
|
||||
|
|
@ -18,10 +18,26 @@ impl JsEmitter {
|
|||
output: String::new(),
|
||||
indent: 0,
|
||||
node_id_counter: 0,
|
||||
local_vars: HashSet::new(),
|
||||
local_var_scopes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new variable scope (e.g., entering a for-in loop)
|
||||
fn push_scope(&mut self, vars: &[&str]) {
|
||||
let set: HashSet<String> = vars.iter().map(|s| s.to_string()).collect();
|
||||
self.local_var_scopes.push(set);
|
||||
}
|
||||
|
||||
/// Pop the top variable scope (e.g., leaving a for-in loop)
|
||||
fn pop_scope(&mut self) {
|
||||
self.local_var_scopes.pop();
|
||||
}
|
||||
|
||||
/// Check if a name is a local variable in any active scope
|
||||
fn is_local_var(&self, name: &str) -> bool {
|
||||
self.local_var_scopes.iter().any(|scope| scope.contains(name))
|
||||
}
|
||||
|
||||
/// Generate a complete HTML page with embedded runtime and compiled app.
|
||||
pub fn emit_html(program: &Program, graph: &SignalGraph, views: &[AnalyzedView]) -> String {
|
||||
let mut emitter = Self::new();
|
||||
|
|
@ -384,7 +400,7 @@ impl JsEmitter {
|
|||
}
|
||||
}
|
||||
Expr::Ident(name) => {
|
||||
if self.local_vars.contains(name) {
|
||||
if self.is_local_var(name) {
|
||||
// Non-reactive local variable (e.g., for-in loop var)
|
||||
self.emit_line(&format!(
|
||||
"{}.textContent = {};",
|
||||
|
|
@ -533,20 +549,18 @@ impl JsEmitter {
|
|||
self.emit_line(&format!("__list.forEach(({item}, {idx_var}) => {{"));
|
||||
self.indent += 1;
|
||||
|
||||
// Mark loop vars as non-reactive so text bindings use plain access
|
||||
self.local_vars.insert(item.clone());
|
||||
// Push scope for loop variables
|
||||
let mut scope_vars: Vec<&str> = vec![item.as_str()];
|
||||
if let Some(idx) = index {
|
||||
self.local_vars.insert(idx.clone());
|
||||
scope_vars.push(idx.as_str());
|
||||
}
|
||||
self.push_scope(&scope_vars);
|
||||
|
||||
let child_var = self.emit_view_expr(body, graph);
|
||||
self.emit_line(&format!("{}.appendChild({});", container_var, child_var));
|
||||
|
||||
// Clean up
|
||||
self.local_vars.remove(item);
|
||||
if let Some(idx) = index {
|
||||
self.local_vars.remove(idx);
|
||||
}
|
||||
// Pop scope
|
||||
self.pop_scope();
|
||||
|
||||
self.indent -= 1;
|
||||
self.emit_line("});");
|
||||
|
|
@ -647,12 +661,11 @@ impl JsEmitter {
|
|||
}
|
||||
format!("`{}`", parts.join(""))
|
||||
}
|
||||
Expr::Ident(name) if self.is_local_var(name) => {
|
||||
format!("{name}")
|
||||
}
|
||||
Expr::Ident(name) => {
|
||||
if self.local_vars.contains(name) {
|
||||
format!("{name}")
|
||||
} else {
|
||||
format!("{name}.value")
|
||||
}
|
||||
format!("{name}.value")
|
||||
}
|
||||
Expr::DotAccess(base, field) => {
|
||||
let base_js = self.emit_expr(base);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue