diff --git a/crates/vize_canon/src/virtual_ts/expressions/component_props.rs b/crates/vize_canon/src/virtual_ts/expressions/component_props.rs index 543330687..25f2ff621 100644 --- a/crates/vize_canon/src/virtual_ts/expressions/component_props.rs +++ b/crates/vize_canon/src/virtual_ts/expressions/component_props.rs @@ -8,6 +8,7 @@ use super::super::helpers::{to_camel_case, to_safe_identifier_fragment}; use super::super::types::{VizeMapping, VizeSubSpan}; use super::reserved_props::rewrite_reserved_template_prop; +use vize_carton::FxHashMap; use vize_carton::FxHashSet; use vize_carton::String; use vize_carton::append; @@ -132,6 +133,7 @@ pub(crate) fn generate_component_prop_checks( indent: &str, ) { let component_type_name = to_safe_identifier_fragment(usage.name.as_str()); + let mut name_occurrences: FxHashMap = FxHashMap::default(); for prop in &usage.props { if !is_checkable_prop(prop) { continue; @@ -163,7 +165,18 @@ pub(crate) fn generate_component_prop_checks( append!(*ts, "{indent}if ({guard}) {{\n"); } - let check_name = cstr!("__vize_prop_check_{idx}_{safe_prop_name}"); + // A repeated attribute name (static class next to :class) still + // checks every authored value, but each check constant needs a + // unique name or the virtual TS redeclares it (TS2451). + let occurrence = name_occurrences + .entry(String::from(safe_prop_name.as_str())) + .and_modify(|count| *count += 1) + .or_insert(1); + let check_name = if *occurrence == 1 { + cstr!("__vize_prop_check_{idx}_{safe_prop_name}") + } else { + cstr!("__vize_prop_check_{idx}_{safe_prop_name}_{occurrence}") + }; let gen_stmt_start = ts.len(); append!(*ts, "{expr_indent}const "); let check_name_start = ts.len(); diff --git a/crates/vize_canon/src/virtual_ts/expressions/component_props_tests.rs b/crates/vize_canon/src/virtual_ts/expressions/component_props_tests.rs index 7d169cd90..a476438f1 100644 --- a/crates/vize_canon/src/virtual_ts/expressions/component_props_tests.rs +++ b/crates/vize_canon/src/virtual_ts/expressions/component_props_tests.rs @@ -157,3 +157,53 @@ fn valueless_static_attributes_stay_out_of_per_prop_checks() { output.code ); } + +#[test] +fn repeated_prop_names_keep_unique_checks_and_one_type_alias() { + let script = r#"import Child from "./Child.vue" +const isLoading = false +"#; + let template = r#""#; + + let allocator = vize_carton::Bump::new(); + let (root, _) = vize_armature::parse(&allocator, template); + let mut analyzer = Analyzer::with_options(AnalyzerOptions::full()); + analyzer.analyze_script_setup(script); + analyzer.analyze_template(&root); + let summary = analyzer.finish(); + + let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0); + + assert_eq!( + output + .code + .matches("type __Child_0_prop_class = __VizePropValue<__Child_Props_0, 'class'>;") + .count(), + 1, + "the child prop type alias must be declared exactly once:\n{}", + output.code + ); + assert!( + output + .code + .contains("const __vize_prop_check_0_class: __Child_0_prop_class = \"static-card\";"), + "the static class value keeps the base check name:\n{}", + output.code + ); + assert!( + output.code.contains( + "const __vize_prop_check_0_class_2: __Child_0_prop_class = { loading: isLoading };" + ), + "the bound class value gets a unique check name:\n{}", + output.code + ); + assert_eq!( + output + .code + .matches("const __vize_prop_check_0_class") + .count(), + 2, + "both authored values stay checked:\n{}", + output.code + ); +} diff --git a/crates/vize_canon/src/virtual_ts/scope/component_props.rs b/crates/vize_canon/src/virtual_ts/scope/component_props.rs index c0631b29f..131cb254f 100644 --- a/crates/vize_canon/src/virtual_ts/scope/component_props.rs +++ b/crates/vize_canon/src/virtual_ts/scope/component_props.rs @@ -104,6 +104,10 @@ pub(super) fn generate_component_props( " type __{component_type_name}_Props_{idx} = typeof {component_ref} extends {{ __vizeCheck: any }} ? Record : (typeof {component_ref} extends {{ new (): {{ $props: infer __P }} }} ? __P : (typeof {component_ref} extends (props: infer __P) => any ? __P : {{}}));\n", ); + // One alias per distinct prop name: a repeated attribute (for example + // a static class next to a bound :class) reuses the same child prop + // type, and a duplicate alias would be a TS2300 in the virtual TS. + let mut declared_aliases = FxHashSet::default(); for prop in &usage.props { if prop.name_is_dynamic || prop.name.as_str() == "key" || prop.name.as_str() == "ref" { continue; @@ -111,6 +115,9 @@ pub(super) fn generate_component_props( if prop.value.is_some() { let camel_prop_name = to_camel_case(prop.name.as_str()); let safe_prop_name = to_safe_identifier_fragment(prop.name.as_str()); + if !declared_aliases.insert(safe_prop_name.clone()) { + continue; + } append!( *ts, " type __{component_type_name}_{idx}_prop_{safe_prop_name} = __VizePropValue<__{component_type_name}_Props_{idx}, '{camel_prop_name}'>;\n", @@ -306,24 +313,7 @@ fn generate_closure_component_props_recursive( } // Recursively handle child closure scopes (v-for and v-slot) - if let Some(child_ids) = ctx.children_map.get(&scope_id) { - for &child_id in child_ids { - if let Some(child_scope) = ctx.summary.scopes.get_scope(child_id) - && matches!(child_scope.kind, ScopeKind::VFor | ScopeKind::VSlot) - { - profile!( - "canon.virtual_ts.closure_component_props", - generate_closure_component_props_recursive( - ts, - mappings, - ctx, - child_scope, - &vfor_inner_indent, - ) - ); - } - } - } + recurse_child_closure_scopes(ts, mappings, ctx, scope_id, &vfor_inner_indent); ts.push_str(&loop_indent); ts.push_str("});\n"); @@ -380,24 +370,7 @@ fn generate_closure_component_props_recursive( } // Recursively handle child closure scopes (v-for and v-slot) - if let Some(child_ids) = ctx.children_map.get(&scope_id) { - for &child_id in child_ids { - if let Some(child_scope) = ctx.summary.scopes.get_scope(child_id) - && matches!(child_scope.kind, ScopeKind::VFor | ScopeKind::VSlot) - { - profile!( - "canon.virtual_ts.closure_component_props", - generate_closure_component_props_recursive( - ts, - mappings, - ctx, - child_scope, - &inner_indent, - ) - ); - } - } - } + recurse_child_closure_scopes(ts, mappings, ctx, scope_id, &inner_indent); ts.push_str(indent); ts.push_str("};\n"); @@ -405,3 +378,27 @@ fn generate_closure_component_props_recursive( _ => {} } } + +/// Recurse into a scope's direct v-for/v-slot child scopes, emitting their +/// component prop checks at the given indent. +fn recurse_child_closure_scopes( + ts: &mut String, + mappings: &mut Vec, + ctx: &VForPropsContext<'_>, + scope_id: u32, + indent: &str, +) { + let Some(child_ids) = ctx.children_map.get(&scope_id) else { + return; + }; + for &child_id in child_ids { + if let Some(child_scope) = ctx.summary.scopes.get_scope(child_id) + && matches!(child_scope.kind, ScopeKind::VFor | ScopeKind::VSlot) + { + profile!( + "canon.virtual_ts.closure_component_props", + generate_closure_component_props_recursive(ts, mappings, ctx, child_scope, indent) + ); + } + } +}