use crate::options::BindingType;
use vize_croquis::builtins::is_global_allowed;
use super::super::context::CodegenContext;
use vize_carton::String;
use vize_carton::ToCompactString;
fn is_identifier_continue(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '$'
}
fn is_simple_identifier_name(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
(first.is_alphabetic() || first == '_' || first == '$')
&& chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
}
fn props_access_expression(object: &str, key: &str) -> String {
if is_simple_identifier_name(key) {
let mut out = String::with_capacity(object.len() + key.len() + 1);
out.push_str(object);
out.push('.');
out.push_str(key);
return out;
}
let mut out = String::with_capacity(object.len() + key.len() + 4);
out.push_str(object);
out.push('[');
use std::fmt::Write as _;
let _ = write!(&mut out, "{:?}", key);
out.push(']');
out
}
fn replace_prefixed_alias_access(code: String, object: &str, local: &str, key: &str) -> String {
let needle = {
let mut needle = String::with_capacity(object.len() + local.len() + 1);
needle.push_str(object);
needle.push('.');
needle.push_str(local);
needle
};
let replacement = props_access_expression(object, key);
let mut result = String::with_capacity(code.len());
let mut cursor = 0;
while let Some(rel_pos) = code[cursor..].find(needle.as_str()) {
let start = cursor + rel_pos;
let end = start + needle.len();
let after_ok = code[end..]
.chars()
.next()
.is_none_or(|c| !is_identifier_continue(c));
result.push_str(&code[cursor..start]);
if after_ok {
result.push_str(&replacement);
} else {
result.push_str(&code[start..end]);
}
cursor = end;
}
result.push_str(&code[cursor..]);
result
}
fn rewrite_props_aliases(code: String, ctx: &CodegenContext) -> String {
let Some(bindings) = &ctx.options.binding_metadata else {
return code;
};
if bindings.props_aliases.is_empty() {
return code;
}
let mut rewritten = code;
for (local, key) in &bindings.props_aliases {
rewritten = replace_prefixed_alias_access(rewritten, "$props", local, key);
}
rewritten
}
pub(crate) fn prefix_identifiers_with_context(content: &str, ctx: &CodegenContext) -> String {
use oxc_allocator::Allocator as OxcAllocator;
use oxc_ast_visit::Visit;
use oxc_ast_visit::walk::{
walk_assignment_expression, walk_object_property, walk_update_expression,
};
use oxc_parser::Parser;
use oxc_span::SourceType;
use vize_carton::FxHashSet;
struct IdentifierVisitor<'a, 'b> {
rewrites: &'a mut Vec<(usize, usize, String)>,
local_vars: &'a mut FxHashSet<String>,
assignment_targets: &'a mut FxHashSet<usize>,
ctx: &'b CodegenContext,
offset: u32,
}
impl<'a, 'b> Visit<'_> for IdentifierVisitor<'a, 'b> {
fn visit_identifier_reference(&mut self, ident: &oxc_ast::ast::IdentifierReference<'_>) {
let name = ident.name.as_str();
if self.local_vars.contains(name) {
return;
}
if is_global_allowed(name) {
return;
}
if self.ctx.is_slot_param(name) {
return;
}
let is_assignment_target = self
.assignment_targets
.contains(&(ident.span.start as usize));
let mut binding_type: Option<BindingType> = None;
let prefix = if let Some(ref metadata) = self.ctx.options.binding_metadata {
if let Some(binding) = metadata.bindings.get(name) {
binding_type = Some(*binding);
if self.ctx.options.inline {
match binding {
BindingType::Props | BindingType::PropsAliased => "$props.",
_ => "",
}
} else {
binding.non_inline_template_prefix()
}
} else {
"_ctx."
}
} else {
"_ctx."
};
if is_assignment_target {
let needs_value = self.ctx.options.inline
&& matches!(
binding_type,
Some(
BindingType::SetupLet
| BindingType::SetupMaybeRef
| BindingType::SetupRef
)
);
let replacement = if needs_value {
let mut out = String::with_capacity(prefix.len() + name.len() + 6);
out.push_str(prefix);
out.push_str(name);
out.push_str(".value");
out
} else if !prefix.is_empty() {
let mut out = String::with_capacity(prefix.len() + name.len());
out.push_str(prefix);
out.push_str(name);
out
} else {
name.to_compact_string()
};
if replacement != name {
let start = (ident.span.start - self.offset) as usize;
let end = (ident.span.end - self.offset) as usize;
self.rewrites.push((start, end, replacement));
}
return;
}
if !prefix.is_empty() {
let start = (ident.span.start - self.offset) as usize;
let end = (ident.span.end - self.offset) as usize;
let mut replacement = String::with_capacity(prefix.len() + name.len());
replacement.push_str(prefix);
replacement.push_str(name);
self.rewrites.push((start, end, replacement));
}
}
fn visit_assignment_expression(&mut self, expr: &oxc_ast::ast::AssignmentExpression<'_>) {
self.collect_assignment_targets(&expr.left);
walk_assignment_expression(self, expr);
}
fn visit_update_expression(&mut self, expr: &oxc_ast::ast::UpdateExpression<'_>) {
self.collect_simple_assignment_targets(&expr.argument);
walk_update_expression(self, expr);
}
fn visit_object_property(&mut self, prop: &oxc_ast::ast::ObjectProperty<'_>) {
if prop.shorthand
&& let oxc_ast::ast::PropertyKey::StaticIdentifier(ident) = &prop.key
{
let name = ident.name.as_str();
if self.local_vars.contains(name)
|| is_global_allowed(name)
|| self.ctx.is_slot_param(name)
{
return;
}
let mut is_ref = false;
let mut needs_unref = false;
let prefix = if let Some(ref metadata) = self.ctx.options.binding_metadata {
if let Some(binding_type) = metadata.bindings.get(name) {
is_ref = self.ctx.options.inline
&& matches!(binding_type, BindingType::SetupRef);
needs_unref = self.ctx.options.inline
&& matches!(
binding_type,
BindingType::SetupLet | BindingType::SetupMaybeRef
);
if self.ctx.options.inline {
match binding_type {
BindingType::Props | BindingType::PropsAliased => "$props.",
_ => "",
}
} else {
binding_type.non_inline_template_prefix()
}
} else {
"_ctx."
}
} else {
"_ctx."
};
if !prefix.is_empty() || is_ref || needs_unref {
let start = (prop.span.start - self.offset) as usize;
let end = (prop.span.end - self.offset) as usize;
let (value_prefix, value_suffix) = if needs_unref {
("_unref(", ")")
} else if is_ref {
("", ".value")
} else {
("", "")
};
let mut replacement = String::with_capacity(
name.len()
+ 2
+ value_prefix.len()
+ prefix.len()
+ name.len()
+ value_suffix.len(),
);
replacement.push_str(name);
replacement.push_str(": ");
replacement.push_str(value_prefix);
if !needs_unref {
replacement.push_str(prefix);
}
replacement.push_str(name);
replacement.push_str(value_suffix);
self.rewrites.push((start, end, replacement));
return;
}
}
walk_object_property(self, prop);
}
fn visit_variable_declarator(&mut self, declarator: &oxc_ast::ast::VariableDeclarator<'_>) {
if let oxc_ast::ast::BindingPattern::BindingIdentifier(ident) = &declarator.id {
self.local_vars.insert(ident.name.to_compact_string());
}
if let Some(init) = &declarator.init {
self.visit_expression(init);
}
}
fn visit_arrow_function_expression(
&mut self,
arrow: &oxc_ast::ast::ArrowFunctionExpression<'_>,
) {
for param in &arrow.params.items {
if let oxc_ast::ast::BindingPattern::BindingIdentifier(ident) = ¶m.pattern {
self.local_vars.insert(ident.name.to_compact_string());
}
}
self.visit_function_body(&arrow.body);
}
}
impl<'a, 'b> IdentifierVisitor<'a, 'b> {
fn collect_assignment_targets(&mut self, target: &oxc_ast::ast::AssignmentTarget<'_>) {
use oxc_ast::ast::{AssignmentTarget, AssignmentTargetProperty};
match target {
AssignmentTarget::AssignmentTargetIdentifier(ident) => {
self.assignment_targets.insert(ident.span.start as usize);
}
AssignmentTarget::ObjectAssignmentTarget(obj) => {
for prop in &obj.properties {
match prop {
AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(
prop_ident,
) => {
self.assignment_targets
.insert(prop_ident.binding.span.start as usize);
}
AssignmentTargetProperty::AssignmentTargetPropertyProperty(
prop_prop,
) => {
self.collect_assignment_targets_maybe_default(&prop_prop.binding);
}
}
}
if let Some(rest) = &obj.rest {
self.collect_assignment_targets(&rest.target);
}
}
AssignmentTarget::ArrayAssignmentTarget(arr) => {
for elem in arr.elements.iter().flatten() {
self.collect_assignment_targets_maybe_default(elem);
}
if let Some(rest) = &arr.rest {
self.collect_assignment_targets(&rest.target);
}
}
_ => {}
}
}
fn collect_assignment_targets_maybe_default(
&mut self,
target: &oxc_ast::ast::AssignmentTargetMaybeDefault<'_>,
) {
use oxc_ast::ast::{AssignmentTargetMaybeDefault, AssignmentTargetProperty};
match target {
AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(def) => {
self.collect_assignment_targets(&def.binding);
}
AssignmentTargetMaybeDefault::AssignmentTargetIdentifier(ident) => {
self.assignment_targets.insert(ident.span.start as usize);
}
AssignmentTargetMaybeDefault::ObjectAssignmentTarget(obj) => {
for prop in &obj.properties {
match prop {
AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(
prop_ident,
) => {
self.assignment_targets
.insert(prop_ident.binding.span.start as usize);
}
AssignmentTargetProperty::AssignmentTargetPropertyProperty(
prop_prop,
) => {
self.collect_assignment_targets_maybe_default(&prop_prop.binding);
}
}
}
if let Some(rest) = &obj.rest {
self.collect_assignment_targets(&rest.target);
}
}
AssignmentTargetMaybeDefault::ArrayAssignmentTarget(arr) => {
for elem in arr.elements.iter().flatten() {
self.collect_assignment_targets_maybe_default(elem);
}
if let Some(rest) = &arr.rest {
self.collect_assignment_targets(&rest.target);
}
}
_ => {}
}
}
fn collect_simple_assignment_targets(
&mut self,
target: &oxc_ast::ast::SimpleAssignmentTarget<'_>,
) {
use oxc_ast::ast::SimpleAssignmentTarget;
if let SimpleAssignmentTarget::AssignmentTargetIdentifier(ident) = target {
self.assignment_targets.insert(ident.span.start as usize);
}
}
}
fn apply_rewrites(content: &str, mut rewrites: Vec<(usize, usize, String)>) -> String {
if rewrites.is_empty() {
return content.to_compact_string();
}
rewrites.sort_by_key(|rewrite| std::cmp::Reverse(rewrite.0));
let mut result = content.to_compact_string();
for (start, end, replacement) in rewrites {
if start < result.len() && end <= result.len() {
result.replace_range(start..end, &replacement);
}
}
result
}
let allocator = OxcAllocator::default();
let source_type = SourceType::default().with_module(true);
let mut wrapped = String::with_capacity(content.len() + 2);
wrapped.push('(');
wrapped.push_str(content);
wrapped.push(')');
let parser = Parser::new(&allocator, &wrapped, source_type);
let parse_result = parser.parse_expression();
match parse_result {
Ok(expr) => {
let mut rewrites: Vec<(usize, usize, String)> = Vec::new();
let mut local_vars: FxHashSet<String> = FxHashSet::default();
let mut assignment_targets: FxHashSet<usize> = FxHashSet::default();
let mut visitor = IdentifierVisitor {
rewrites: &mut rewrites,
local_vars: &mut local_vars,
assignment_targets: &mut assignment_targets,
ctx,
offset: 1, };
visitor.visit_expression(&expr);
rewrite_props_aliases(apply_rewrites(content, rewrites), ctx)
}
Err(_) => {
let allocator2 = OxcAllocator::default();
let parser2 = Parser::new(&allocator2, content, source_type);
let parse_result2 = parser2.parse();
if parse_result2.errors.is_empty() {
let mut rewrites: Vec<(usize, usize, String)> = Vec::new();
let mut local_vars: FxHashSet<String> = FxHashSet::default();
let mut assignment_targets: FxHashSet<usize> = FxHashSet::default();
let mut visitor = IdentifierVisitor {
rewrites: &mut rewrites,
local_vars: &mut local_vars,
assignment_targets: &mut assignment_targets,
ctx,
offset: 0,
};
visitor.visit_program(&parse_result2.program);
rewrite_props_aliases(apply_rewrites(content, rewrites), ctx)
} else {
content.to_compact_string()
}
}
}
}
pub(crate) fn convert_line_comments_to_block(content: &str) -> String {
let mut result = String::with_capacity(content.len());
let bytes = content.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
match bytes[i] {
b'\'' | b'"' | b'`' => {
let quote = bytes[i];
result.push(quote as char);
i += 1;
while i < len {
if bytes[i] == b'\\' && i + 1 < len {
result.push(bytes[i] as char);
result.push(bytes[i + 1] as char);
i += 2;
} else if bytes[i] == quote {
result.push(quote as char);
i += 1;
break;
} else {
result.push(bytes[i] as char);
i += 1;
}
}
}
b'/' if i + 1 < len && bytes[i + 1] == b'/' => {
let comment_start = i + 2;
let mut comment_end = comment_start;
while comment_end < len && bytes[comment_end] != b'\n' {
comment_end += 1;
}
let comment_text = &content[comment_start..comment_end].trim_end();
result.push_str("/* ");
result.push_str(comment_text);
result.push_str(" */");
i = comment_end;
if i < len && bytes[i] == b'\n' {
result.push('\n');
i += 1;
}
}
b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
result.push('/');
result.push('*');
i += 2;
while i + 1 < len {
if bytes[i] == b'*' && bytes[i + 1] == b'/' {
result.push('*');
result.push('/');
i += 2;
break;
}
result.push(bytes[i] as char);
i += 1;
}
}
_ => {
result.push(bytes[i] as char);
i += 1;
}
}
}
result
}
pub(crate) fn strip_ctx_for_slot_params(ctx: &CodegenContext, content: &str) -> String {
let mut result = String::with_capacity(content.len());
let bytes = content.as_bytes();
let prefix = b"_ctx.";
let mut i = 0;
while i < bytes.len() {
if i + prefix.len() <= bytes.len() && &bytes[i..i + prefix.len()] == prefix {
let start = i + prefix.len();
let mut end = start;
while end < bytes.len()
&& (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_' || bytes[end] == b'$')
{
end += 1;
}
let ident = &content[start..end];
if !ident.is_empty() && ctx.is_slot_param(ident) {
result.push_str(ident);
i = end;
} else {
result.push_str("_ctx.");
i = start;
}
} else {
result.push(bytes[i] as char);
i += 1;
}
}
result
}