use crate::context::TypeContext;
use crate::diagnostic::{TypeDiagnostic, TypeErrorCode};
use crate::types::{CompletionItem, CompletionKind, TypeInfo};
use crate::CheckResult;
#[derive(Debug, Default)]
pub struct TypeChecker {
pub strict: bool,
pub vue_checks: bool,
}
impl TypeChecker {
pub fn new() -> Self {
Self {
strict: false,
vue_checks: true,
}
}
pub fn strict() -> Self {
Self {
strict: true,
vue_checks: true,
}
}
pub fn with_strict(mut self, strict: bool) -> Self {
self.strict = strict;
self
}
pub fn with_vue_checks(mut self, vue_checks: bool) -> Self {
self.vue_checks = vue_checks;
self
}
pub fn check_template(&self, template: &str, ctx: &TypeContext) -> CheckResult {
let mut result = CheckResult::new();
self.check_interpolations(template, ctx, &mut result);
self.check_directives(template, ctx, &mut result);
self.check_event_handlers(template, ctx, &mut result);
self.check_bindings(template, ctx, &mut result);
result
}
fn check_interpolations(&self, template: &str, ctx: &TypeContext, result: &mut CheckResult) {
let mut pos = 0;
while let Some(start) = template[pos..].find("{{") {
let abs_start = pos + start;
if let Some(end) = template[abs_start..].find("}}") {
let expr_start = abs_start + 2;
let expr_end = abs_start + end;
let expr = template[expr_start..expr_end].trim();
if !expr.is_empty() {
self.check_expression(expr, expr_start as u32, expr_end as u32, ctx, result);
}
pos = abs_start + end + 2;
} else {
break;
}
}
}
fn check_directives(&self, template: &str, ctx: &TypeContext, result: &mut CheckResult) {
for directive in ["v-if", "v-else-if", "v-show"] {
self.check_directive_values(template, directive, ctx, result);
}
self.check_vfor_expressions(template, ctx, result);
}
fn check_directive_values(
&self,
template: &str,
directive: &str,
ctx: &TypeContext,
result: &mut CheckResult,
) {
let pattern = format!("{}=\"", directive);
let mut pos = 0;
while let Some(start) = template[pos..].find(&pattern) {
let abs_start = pos + start + pattern.len();
if let Some(end) = template[abs_start..].find('"') {
let expr = &template[abs_start..abs_start + end];
if !expr.is_empty() {
self.check_expression(
expr,
abs_start as u32,
(abs_start + end) as u32,
ctx,
result,
);
}
pos = abs_start + end + 1;
} else {
break;
}
}
}
fn check_vfor_expressions(&self, template: &str, ctx: &TypeContext, result: &mut CheckResult) {
let pattern = "v-for=\"";
let mut pos = 0;
while let Some(start) = template[pos..].find(pattern) {
let abs_start = pos + start + pattern.len();
if let Some(end) = template[abs_start..].find('"') {
let expr = &template[abs_start..abs_start + end];
if let Some(in_pos) = expr.find(" in ") {
let iterable = expr[in_pos + 4..].trim();
let iterable_start = abs_start + in_pos + 4;
self.check_expression(
iterable,
iterable_start as u32,
(abs_start + end) as u32,
ctx,
result,
);
}
pos = abs_start + end + 1;
} else {
break;
}
}
}
fn check_event_handlers(&self, template: &str, ctx: &TypeContext, result: &mut CheckResult) {
let patterns = ["@", "v-on:"];
for pattern in patterns {
let mut pos = 0;
while let Some(start) = template[pos..].find(pattern) {
let abs_start = pos + start + pattern.len();
if let Some(eq_pos) = template[abs_start..].find("=\"") {
let handler_start = abs_start + eq_pos + 2;
if let Some(end) = template[handler_start..].find('"') {
let handler = &template[handler_start..handler_start + end];
if Self::is_simple_identifier(handler) {
self.check_identifier(
handler,
handler_start as u32,
(handler_start + end) as u32,
ctx,
result,
);
} else if !handler.is_empty() {
self.check_expression(
handler,
handler_start as u32,
(handler_start + end) as u32,
ctx,
result,
);
}
pos = handler_start + end + 1;
} else {
break;
}
} else {
pos = abs_start + 1;
}
}
}
}
fn check_bindings(&self, template: &str, ctx: &TypeContext, result: &mut CheckResult) {
let patterns = [(":", "="), ("v-bind:", "=")];
for (prefix, suffix) in patterns {
let mut pos = 0;
while let Some(start) = template[pos..].find(prefix) {
if prefix == ":" && template[pos + start..].starts_with("::") {
pos = pos + start + 2;
continue;
}
let abs_start = pos + start + prefix.len();
if let Some(eq_pos) = template[abs_start..].find(&format!("{suffix}\"")) {
let expr_start = abs_start + eq_pos + 2;
if let Some(end) = template[expr_start..].find('"') {
let expr = &template[expr_start..expr_start + end];
if !expr.is_empty() {
self.check_expression(
expr,
expr_start as u32,
(expr_start + end) as u32,
ctx,
result,
);
}
pos = expr_start + end + 1;
} else {
break;
}
} else {
pos = abs_start + 1;
}
}
}
}
fn check_expression(
&self,
expr: &str,
start: u32,
_end: u32,
ctx: &TypeContext,
result: &mut CheckResult,
) {
for (ident, offset) in Self::extract_identifiers(expr) {
let ident_start = start + offset as u32;
let ident_end = ident_start + ident.len() as u32;
self.check_identifier(ident, ident_start, ident_end, ctx, result);
}
}
fn check_identifier(
&self,
ident: &str,
start: u32,
end: u32,
ctx: &TypeContext,
result: &mut CheckResult,
) {
if Self::is_keyword_or_literal(ident) {
return;
}
if ident.starts_with('$') {
return;
}
if !ctx.has_binding(ident) && !ctx.globals.contains_key(ident) {
result.add_diagnostic(TypeDiagnostic::error(
TypeErrorCode::UnknownIdentifier,
format!("Cannot find name '{}'", ident),
start,
end,
));
}
}
pub fn get_type_at(&self, template: &str, offset: u32, ctx: &TypeContext) -> Option<TypeInfo> {
let offset = offset as usize;
if let Some((expr, expr_start)) = self.find_expression_at(template, offset) {
let relative_offset = offset - expr_start;
return self.get_type_in_expression(&expr, relative_offset, ctx);
}
None
}
fn find_expression_at(&self, template: &str, offset: usize) -> Option<(String, usize)> {
let mut pos = 0;
while let Some(start) = template[pos..].find("{{") {
let abs_start = pos + start;
if let Some(end) = template[abs_start..].find("}}") {
let expr_start = abs_start + 2;
let expr_end = abs_start + end;
if offset >= expr_start && offset <= expr_end {
return Some((
template[expr_start..expr_end].trim().to_string(),
expr_start,
));
}
pos = abs_start + end + 2;
} else {
break;
}
}
for directive in ["v-if", "v-else-if", "v-show", "v-for"] {
if let Some((expr, start)) = self.find_directive_expr_at(template, directive, offset) {
return Some((expr, start));
}
}
None
}
fn find_directive_expr_at(
&self,
template: &str,
directive: &str,
offset: usize,
) -> Option<(String, usize)> {
let pattern = format!("{}=\"", directive);
let mut pos = 0;
while let Some(start) = template[pos..].find(&pattern) {
let abs_start = pos + start + pattern.len();
if let Some(end) = template[abs_start..].find('"') {
if offset >= abs_start && offset <= abs_start + end {
return Some((template[abs_start..abs_start + end].to_string(), abs_start));
}
pos = abs_start + end + 1;
} else {
break;
}
}
None
}
fn get_type_in_expression(
&self,
expr: &str,
offset: usize,
ctx: &TypeContext,
) -> Option<TypeInfo> {
let ident = self.find_identifier_at(expr, offset)?;
if let Some(binding) = ctx.get_binding(&ident) {
return Some(binding.type_info.clone());
}
if let Some(type_info) = ctx.globals.get(&ident) {
return Some(type_info.clone());
}
None
}
fn find_identifier_at(&self, expr: &str, offset: usize) -> Option<String> {
if offset >= expr.len() {
return None;
}
let bytes = expr.as_bytes();
if !Self::is_ident_char(bytes[offset] as char) {
return None;
}
let mut start = offset;
while start > 0 && Self::is_ident_char(bytes[start - 1] as char) {
start -= 1;
}
let mut end = offset;
while end < bytes.len() && Self::is_ident_char(bytes[end] as char) {
end += 1;
}
if !Self::is_ident_start(bytes[start] as char) {
return None;
}
Some(expr[start..end].to_string())
}
pub fn get_completions(
&self,
_template: &str,
_offset: u32,
ctx: &TypeContext,
) -> Vec<CompletionItem> {
let mut completions = Vec::new();
for (name, binding) in &ctx.bindings {
let kind = match binding.kind {
crate::context::BindingKind::Function => CompletionKind::Function,
crate::context::BindingKind::Class => CompletionKind::Class,
crate::context::BindingKind::Const
| crate::context::BindingKind::Let
| crate::context::BindingKind::Var => CompletionKind::Variable,
crate::context::BindingKind::Ref
| crate::context::BindingKind::Computed
| crate::context::BindingKind::Reactive => CompletionKind::Variable,
crate::context::BindingKind::Import => CompletionKind::Module,
crate::context::BindingKind::Prop => CompletionKind::Property,
_ => CompletionKind::Variable,
};
completions.push(
CompletionItem::new(name, kind)
.with_detail(&binding.type_info.display)
.with_priority(10),
);
}
for name in ctx.components.keys() {
completions
.push(CompletionItem::new(name, CompletionKind::Component).with_priority(20));
}
for (name, type_info) in &ctx.globals {
completions.push(
CompletionItem::new(name, CompletionKind::Variable)
.with_detail(&type_info.display)
.with_priority(30),
);
}
completions.sort_by(|a, b| {
a.sort_priority
.cmp(&b.sort_priority)
.then_with(|| a.label.cmp(&b.label))
});
completions
}
fn extract_identifiers(expr: &str) -> Vec<(&str, usize)> {
let mut identifiers = Vec::new();
let bytes = expr.as_bytes();
let mut i = 0;
while i < bytes.len() {
while i < bytes.len() && !Self::is_ident_start(bytes[i] as char) {
i += 1;
}
if i >= bytes.len() {
break;
}
let start = i;
while i < bytes.len() && Self::is_ident_char(bytes[i] as char) {
i += 1;
}
if start < i {
identifiers.push((&expr[start..i], start));
}
}
identifiers
}
fn is_simple_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
let first = chars.next().expect("non-empty string checked above");
if !Self::is_ident_start(first) {
return false;
}
chars.all(Self::is_ident_char)
}
fn is_ident_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_' || c == '$'
}
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '$'
}
fn is_keyword_or_literal(s: &str) -> bool {
matches!(
s,
"true"
| "false"
| "null"
| "undefined"
| "this"
| "if"
| "else"
| "for"
| "while"
| "do"
| "switch"
| "case"
| "default"
| "break"
| "continue"
| "return"
| "throw"
| "try"
| "catch"
| "finally"
| "new"
| "delete"
| "typeof"
| "instanceof"
| "in"
| "of"
| "void"
| "function"
| "class"
| "extends"
| "const"
| "let"
| "var"
| "import"
| "export"
| "async"
| "await"
| "yield"
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::{Binding, BindingKind};
use crate::types::TypeKind;
fn create_test_context() -> TypeContext {
let mut ctx = TypeContext::new();
ctx.add_binding(
"count",
Binding::new(
"count",
TypeInfo::new("Ref<number>", TypeKind::Ref),
BindingKind::Ref,
),
);
ctx.add_binding(
"message",
Binding::new("message", TypeInfo::string(), BindingKind::Const),
);
ctx.add_binding(
"handleClick",
Binding::new(
"handleClick",
TypeInfo::new("() => void", TypeKind::Function),
BindingKind::Function,
),
);
ctx
}
#[test]
fn test_check_interpolation() {
let checker = TypeChecker::new();
let ctx = create_test_context();
let template = r#"<div>{{ count }}</div>"#;
let result = checker.check_template(template, &ctx);
assert!(!result.has_errors());
}
#[test]
fn test_check_unknown_identifier() {
let checker = TypeChecker::new();
let ctx = create_test_context();
let template = r#"<div>{{ unknownVar }}</div>"#;
let result = checker.check_template(template, &ctx);
assert!(result.has_errors());
assert_eq!(result.error_count, 1);
assert!(result.diagnostics[0].message.contains("unknownVar"));
}
#[test]
fn test_check_directive() {
let checker = TypeChecker::new();
let ctx = create_test_context();
let template = r#"<div v-if="count > 0">visible</div>"#;
let result = checker.check_template(template, &ctx);
assert!(!result.has_errors());
}
#[test]
fn test_check_event_handler() {
let checker = TypeChecker::new();
let ctx = create_test_context();
let template = r#"<button @click="handleClick">Click</button>"#;
let result = checker.check_template(template, &ctx);
assert!(!result.has_errors());
}
#[test]
fn test_check_vbind() {
let checker = TypeChecker::new();
let ctx = create_test_context();
let template = r#"<input :value="message" />"#;
let result = checker.check_template(template, &ctx);
assert!(!result.has_errors());
}
#[test]
fn test_extract_identifiers() {
let ids = TypeChecker::extract_identifiers("count + message.length");
assert_eq!(ids.len(), 3);
assert_eq!(ids[0].0, "count");
assert_eq!(ids[1].0, "message");
assert_eq!(ids[2].0, "length");
}
#[test]
fn test_is_simple_identifier() {
assert!(TypeChecker::is_simple_identifier("foo"));
assert!(TypeChecker::is_simple_identifier("_bar"));
assert!(TypeChecker::is_simple_identifier("$baz"));
assert!(!TypeChecker::is_simple_identifier("foo.bar"));
assert!(!TypeChecker::is_simple_identifier("foo[0]"));
assert!(!TypeChecker::is_simple_identifier("123"));
}
#[test]
fn test_get_completions() {
let checker = TypeChecker::new();
let ctx = create_test_context();
let template = r#"<div>{{ }}</div>"#;
let completions = checker.get_completions(template, 8, &ctx);
assert!(!completions.is_empty());
assert!(completions.iter().any(|c| c.label == "count"));
assert!(completions.iter().any(|c| c.label == "message"));
}
#[test]
fn test_check_multiple_interpolations() {
let checker = TypeChecker::new();
let ctx = create_test_context();
let template = r#"<div>{{ count }} - {{ message }}</div>"#;
let result = checker.check_template(template, &ctx);
assert!(!result.has_errors());
}
#[test]
fn test_check_multiple_unknown_identifiers() {
let checker = TypeChecker::new();
let ctx = create_test_context();
let template = r#"<div>{{ unknownA }} {{ unknownB }}</div>"#;
let result = checker.check_template(template, &ctx);
assert!(result.has_errors());
assert_eq!(result.error_count, 2);
}
#[test]
fn test_check_keyword_not_flagged() {
let checker = TypeChecker::new();
let ctx = create_test_context();
let template = r#"<div>{{ true }}</div>"#;
let result = checker.check_template(template, &ctx);
assert!(!result.has_errors());
}
#[test]
fn test_check_ternary_expression() {
let checker = TypeChecker::new();
let ctx = create_test_context();
let template = r#"<div>{{ count > 0 ? message : count }}</div>"#;
let result = checker.check_template(template, &ctx);
assert!(!result.has_errors());
}
#[test]
fn test_extract_identifiers_with_property_access() {
let ids = TypeChecker::extract_identifiers("message.length");
assert_eq!(ids.len(), 2);
assert_eq!(ids[0].0, "message");
assert_eq!(ids[1].0, "length");
}
#[test]
fn test_extract_identifiers_complex() {
let ids = TypeChecker::extract_identifiers("a + b * c");
assert_eq!(ids.len(), 3);
assert_eq!(ids[0].0, "a");
assert_eq!(ids[1].0, "b");
assert_eq!(ids[2].0, "c");
}
#[test]
fn test_extract_identifiers_empty() {
let ids = TypeChecker::extract_identifiers("");
assert!(ids.is_empty());
}
#[test]
fn test_extract_identifiers_numeric() {
let ids = TypeChecker::extract_identifiers("42");
assert!(ids.is_empty() || ids.iter().all(|(name, _)| name.parse::<i32>().is_ok()));
}
#[test]
fn test_is_simple_identifier_edge_cases() {
assert!(!TypeChecker::is_simple_identifier(""));
assert!(TypeChecker::is_simple_identifier("a"));
assert!(TypeChecker::is_simple_identifier("$"));
assert!(TypeChecker::is_simple_identifier("_"));
assert!(!TypeChecker::is_simple_identifier("a b"));
assert!(!TypeChecker::is_simple_identifier("a.b.c"));
}
#[test]
fn test_check_v_bind_shorthand() {
let checker = TypeChecker::new();
let ctx = create_test_context();
let template = r#"<div :class="message">content</div>"#;
let result = checker.check_template(template, &ctx);
assert!(!result.has_errors());
}
#[test]
fn test_check_v_on_shorthand() {
let checker = TypeChecker::new();
let ctx = create_test_context();
let template = r#"<button @click="handleClick()">Click</button>"#;
let result = checker.check_template(template, &ctx);
assert!(!result.has_errors());
}
}