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
2 changes: 1 addition & 1 deletion crates/vize/tests/check_server_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ fn check_server_maps_dependency_patch_diagnostics_to_the_parent_sfc() {
"message": "Type 'number' is not assignable to type 'string'.",
"severity": "error",
"line": 7,
"column": 34,
"column": 27,
"code": "TS2322"
}])
);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/vize_canon/src/virtual_ts/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
mod component_props;
#[cfg(test)]
mod component_props_tests;
mod prop_sources;
mod reserved_props;
mod statements;
mod vif_chain;
Expand Down
89 changes: 26 additions & 63 deletions crates/vize_canon/src/virtual_ts/expressions/component_props.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,14 @@

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 super::prop_sources::{generated_prop_value, prop_name_source_range, prop_value_source_range};
use vize_carton::FxHashMap;
use vize_carton::FxHashSet;
use vize_carton::String;
use vize_carton::append;
use vize_carton::cstr;
use vize_carton::profile;
use vize_croquis::croquis::{ComponentUsage, PassedProp};
use vize_croquis::drawer::strip_js_comments;

fn push_ts_string_literal(out: &mut String, value: &str) {
out.push('"');
for ch in value.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(ch),
}
}
out.push('"');
}

fn generated_prop_value(
prop: &PassedProp,
template_prop_names: &FxHashSet<String>,
) -> Option<String> {
if !prop.is_dynamic {
let mut value = String::default();
if let Some(static_value) = prop.value.as_ref() {
push_ts_string_literal(&mut value, static_value.as_str());
} else {
value.push_str("true");
}
return Some(value);
}

let value = strip_js_comments(prop.value.as_ref()?.as_str());
let trimmed_value = value.as_ref().trim();
let rewritten_value = rewrite_reserved_template_prop(trimmed_value, template_prop_names);
Some(rewritten_value.as_ref().map_or_else(
|| String::from(value.as_ref()),
|s| String::from(s.as_str()),
))
}

fn has_inference_props(usage: &ComponentUsage) -> bool {
usage.props.iter().any(is_checkable_prop)
Expand All @@ -75,20 +36,6 @@ impl<'a> ComponentPropSource<'a> {
}
}

fn prop_value_source_range(
source_context: ComponentPropSource<'_>,
prop: &PassedProp,
) -> Option<std::ops::Range<usize>> {
let source = source_context.template?;
let value = prop.value.as_ref()?.as_str();
let prop_start = prop.start as usize;
let prop_end = prop.end as usize;
let raw_prop = source.get(prop_start..prop_end)?;
let relative_start = raw_prop.rfind(value)?;
let source_start = source_context.offset as usize + prop_start + relative_start;
Some(source_start..source_start + value.len())
}

fn collect_generated_class_bindings<'a>(
usage: &'a ComponentUsage,
template_prop_names: &FxHashSet<String>,
Expand Down Expand Up @@ -184,21 +131,37 @@ pub(crate) fn generate_component_prop_checks(
let check_name_end = ts.len();
append!(
*ts,
": __{component_type_name}_{idx}_prop_{safe_prop_name} = {};\n",
generated_value.as_str(),
": __{component_type_name}_{idx}_prop_{safe_prop_name} = ",
);
let value_gen_start = ts.len();
ts.push_str(generated_value.as_str());
let value_gen_end = ts.len();
ts.push_str(";\n");
let gen_stmt_end = ts.len();
append!(*ts, "{expr_indent}void {check_name};\n");

// The synthetic identifier receives the child prop-type error
// (TS2322-class), which vue-tsc anchors at the attribute name;
// the initializer keeps the exact authored expression so errors
// inside the value land on the authored bytes.
let name_src_range = prop_name_source_range(source_context, prop);
let mut sub_spans = Vec::new();
if let Some(src_range) = name_src_range.or_else(|| value_src_range.clone()) {
sub_spans.push(VizeSubSpan {
gen_range: check_name_start..check_name_end,
src_range,
});
}
if let Some(src_range) = value_src_range {
sub_spans.push(VizeSubSpan {
gen_range: value_gen_start..value_gen_end,
src_range,
});
}
mappings.push(VizeMapping {
gen_range: gen_stmt_start..gen_stmt_end,
src_range: prop_src_start..prop_src_end,
sub_spans: value_src_range
.map(|src_range| VizeSubSpan {
gen_range: check_name_start..check_name_end,
src_range,
})
.into_iter()
.collect(),
sub_spans,
});

if usage.vif_guard.is_some() {
Expand Down
160 changes: 146 additions & 14 deletions crates/vize_canon/src/virtual_ts/expressions/component_props_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const isLoading = false
}

#[test]
fn component_prop_check_maps_synthetic_name_to_bound_expression() {
fn component_prop_check_anchors_name_and_preserves_bound_expression() {
let script = r#"import Child from "./Child.vue"
const benchmarkMirror = 1
"#;
Expand All @@ -53,19 +53,37 @@ const benchmarkMirror = 1
let summary = analyzer.finish();

let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);

// The synthetic identifier — where the child prop-type error lands —
// anchors at the attribute name, matching vue-tsc.
let name_start = template.find(":code").expect("attribute present") + 1;
let name_range = name_start..name_start + "code".len();
let name_span = output
.mappings
.iter()
.flat_map(|mapping| &mapping.sub_spans)
.find(|span| span.src_range == name_range)
.expect("component prop check should anchor at the attribute name");
assert!(
output.code[name_span.gen_range.clone()].starts_with("__vize_prop_check_"),
"synthetic check identifier should map to the attribute name: {name_span:?}"
);

// The initializer keeps the exact authored expression range so errors
// inside the value land on the authored bytes.
let expression = "String(benchmarkMirror)";
let source_start = template.find(expression).expect("bound expression present");
let source_range = source_start..source_start + expression.len();
let sub_span = output
let value_span = output
.mappings
.iter()
.flat_map(|mapping| &mapping.sub_spans)
.find(|span| span.src_range == source_range)
.expect("component prop check should preserve the exact bound expression range");

assert!(
output.code[sub_span.gen_range.clone()].starts_with("__vize_prop_check_"),
"synthetic check identifier should map to the bound expression: {sub_span:?}"
assert_eq!(
&output.code[value_span.gen_range.clone()],
expression,
"initializer sub-span should cover the generated expression"
);
}

Expand Down Expand Up @@ -99,18 +117,30 @@ fn static_attribute_values_are_type_checked_like_dynamic_bindings() {
output.code
);

// The synthetic check identifier maps back to the authored attribute value.
let value_start = template.find("You did it!").expect("static value present");
let value_range = value_start..value_start + "You did it!".len();
let sub_span = output
// The synthetic check identifier anchors at the attribute name, and the
// initializer keeps the authored value range.
let name_start = template.find("msg=").expect("attribute present");
let name_range = name_start..name_start + "msg".len();
let name_span = output
.mappings
.iter()
.flat_map(|mapping| &mapping.sub_spans)
.find(|span| span.src_range == value_range)
.expect("static prop check should map to the authored attribute value");
.find(|span| span.src_range == name_range)
.expect("static prop check should anchor at the attribute name");
assert!(
output.code[sub_span.gen_range.clone()].starts_with("__vize_prop_check_"),
"synthetic check identifier should map to the static value: {sub_span:?}"
output.code[name_span.gen_range.clone()].starts_with("__vize_prop_check_"),
"synthetic check identifier should map to the attribute name: {name_span:?}"
);

let value_start = template.find("You did it!").expect("static value present");
let value_range = value_start..value_start + "You did it!".len();
assert!(
output
.mappings
.iter()
.flat_map(|mapping| &mapping.sub_spans)
.any(|span| span.src_range == value_range),
"static prop check should keep the authored value range"
);
}

Expand Down Expand Up @@ -207,3 +237,105 @@ const isLoading = false
output.code
);
}

#[test]
fn attribute_name_anchor_survives_prefixes_and_modifiers() {
let script = r#"import Child from "./Child.vue"
const bind = 1
const sync = 2
const fooBar = 3
"#;
let template = r#"<Child v-bind:bind="bind" :sync.camel="sync" :foo-bar="fooBar" />"#;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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);
let sub_spans: Vec<_> = output
.mappings
.iter()
.flat_map(|mapping| &mapping.sub_spans)
.collect();

let bind_name = template.find("v-bind:bind").expect("v-bind attr") + "v-bind:".len();
assert!(
sub_spans.iter().any(
|span| span.src_range == (bind_name..bind_name + "bind".len())
&& output.code[span.gen_range.clone()].starts_with("__vize_prop_check_")
),
"v-bind: prefixed name should anchor after the prefix"
);

let sync_name = template.find(":sync.camel").expect("modifier attr") + 1;
assert!(
sub_spans.iter().any(
|span| span.src_range == (sync_name..sync_name + "sync".len())
&& output.code[span.gen_range.clone()].starts_with("__vize_prop_check_")
),
"modifier attributes should anchor at the name, not the modifier"
);

let kebab_name = template.find(":foo-bar").expect("kebab attr") + 1;
assert!(
sub_spans.iter().any(
|span| span.src_range == (kebab_name..kebab_name + "foo-bar".len())
&& output.code[span.gen_range.clone()].starts_with("__vize_prop_check_")
),
"kebab-case names should anchor across the whole hyphenated name, not a leading segment"
);
}

#[test]
fn attribute_name_anchor_uses_utf8_byte_offsets_after_multibyte_text() {
let script = r#"import Child from "./Child.vue"
const total = 1
"#;
// The static label holds multibyte characters (😀 is 4 UTF-8 bytes / 2 UTF-16
// units, ハ is 3 UTF-8 bytes) so a byte/char confusion in the source mapping
// would shift every following range.
let template = r#"<Child label="😀ハ" :count="total" />"#;

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);
let sub_spans: Vec<_> = output
.mappings
.iter()
.flat_map(|mapping| &mapping.sub_spans)
.collect();

// `str::find` returns UTF-8 byte offsets; the multibyte prefix makes the byte
// offset strictly larger than the char count, so these ranges only match when
// the source mapping is byte-indexed.
let name_start = template.find(":count").expect("attribute present") + 1;
assert!(
name_start > template[..name_start].chars().count(),
"fixture must place the prop after multibyte text so byte and char offsets diverge"
);
let name_range = name_start..name_start + "count".len();
let name_span = sub_spans
.iter()
.find(|span| span.src_range == name_range)
.expect("prop check should anchor at the attribute name after multibyte text");
assert!(
output.code[name_span.gen_range.clone()].starts_with("__vize_prop_check_"),
"synthetic check identifier should map to the attribute name: {name_span:?}"
);

// The initializer keeps the exact authored expression bytes.
let value_start = template.find("total").expect("bound value present");
let value_range = value_start..value_start + "total".len();
assert!(
sub_spans.iter().any(|span| span.src_range == value_range),
"initializer sub-span should keep the authored value bytes after multibyte text"
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading
Loading