use crate::tsgo_bridge::{TsgoBridge, TsgoBridgeError};
use std::path::Path;
use std::sync::Arc;
use vize_croquis::virtual_ts::{generate_virtual_ts, VirtualTsOutput};
pub struct TypeCheckService {
bridge: Arc<TsgoBridge>,
}
#[derive(Debug, Clone, Default)]
pub struct TypeCheckServiceOptions {
pub project_root: Option<String>,
pub tsconfig_path: Option<String>,
pub check_cross_component: bool,
pub check_template: bool,
}
#[derive(Debug, Clone, Default)]
pub struct SfcTypeCheckResult {
pub diagnostics: Vec<SfcDiagnostic>,
pub error_count: usize,
pub warning_count: usize,
pub virtual_ts: Option<String>,
pub analysis_time_ms: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct SfcDiagnostic {
pub message: String,
pub severity: SfcDiagnosticSeverity,
pub start: u32,
pub end: u32,
pub code: Option<String>,
pub related: Vec<SfcRelatedInfo>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SfcDiagnosticSeverity {
Error,
Warning,
Info,
Hint,
}
#[derive(Debug, Clone)]
pub struct SfcRelatedInfo {
pub message: String,
pub filename: Option<String>,
pub start: u32,
pub end: u32,
}
impl TypeCheckService {
pub async fn new() -> Result<Self, TsgoBridgeError> {
let bridge = TsgoBridge::new();
bridge.spawn().await?;
Ok(Self {
bridge: Arc::new(bridge),
})
}
pub async fn check_sfc(
&self,
source: &str,
filename: &str,
options: &TypeCheckServiceOptions,
) -> Result<SfcTypeCheckResult, TsgoBridgeError> {
use std::time::Instant;
use vize_atelier_core::parser::parse;
use vize_atelier_sfc::{parse_sfc, SfcParseOptions};
use vize_carton::Bump;
use vize_croquis::{Analyzer, AnalyzerOptions};
let start_time = Instant::now();
let mut result = SfcTypeCheckResult::default();
let parse_opts = SfcParseOptions {
filename: filename.to_string(),
..Default::default()
};
let descriptor = match parse_sfc(source, parse_opts) {
Ok(d) => d,
Err(e) => {
result.diagnostics.push(SfcDiagnostic {
message: format!("Failed to parse SFC: {}", e.message),
severity: SfcDiagnosticSeverity::Error,
start: 0,
end: 0,
code: Some("parse-error".to_string()),
related: Vec::new(),
});
result.error_count = 1;
return Ok(result);
}
};
let script_content = descriptor
.script_setup
.as_ref()
.map(|s| s.content.as_ref())
.or_else(|| descriptor.script.as_ref().map(|s| s.content.as_ref()));
let allocator = Bump::new();
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
let script_offset: u32 = if let Some(ref script_setup) = descriptor.script_setup {
analyzer.analyze_script_setup(&script_setup.content);
script_setup.loc.start as u32
} else if let Some(ref script) = descriptor.script {
analyzer.analyze_script_plain(&script.content);
script.loc.start as u32
} else {
0
};
let (template_offset, template_ast) = if let Some(ref template) = descriptor.template {
let (root, _errors) = parse(&allocator, &template.content);
analyzer.analyze_template(&root);
(template.loc.start as u32, Some(root))
} else {
(0, None)
};
let summary = analyzer.finish();
let virtual_ts_output = generate_virtual_ts(
script_content,
template_ast.as_ref(),
&summary.bindings,
None, options.project_root.as_ref().map(Path::new),
template_offset,
);
result.virtual_ts = Some(virtual_ts_output.content.clone());
if !virtual_ts_output.content.is_empty() {
let virtual_uri = format!("vize-virtual://{}.ts", filename);
self.bridge
.open_virtual_document(&virtual_uri, &virtual_ts_output.content)
.await?;
let tsgo_result = self.bridge.get_diagnostics(&virtual_uri).await?;
for diag in tsgo_result {
let (start, end) = map_position_to_sfc(
&virtual_ts_output,
diag.range.start.line,
diag.range.start.character,
diag.range.end.line,
diag.range.end.character,
script_offset,
template_offset,
);
let severity = match diag.severity.unwrap_or(1) {
1 => SfcDiagnosticSeverity::Error,
2 => SfcDiagnosticSeverity::Warning,
3 => SfcDiagnosticSeverity::Info,
_ => SfcDiagnosticSeverity::Hint,
};
if matches!(severity, SfcDiagnosticSeverity::Error) {
result.error_count += 1;
} else if matches!(severity, SfcDiagnosticSeverity::Warning) {
result.warning_count += 1;
}
result.diagnostics.push(SfcDiagnostic {
message: diag.message,
severity,
start,
end,
code: diag.code.map(|c| format!("TS{}", c)),
related: diag
.related_information
.unwrap_or_default()
.into_iter()
.map(|r| {
let (rel_start, rel_end) = map_position_to_sfc(
&virtual_ts_output,
r.location.range.start.line,
r.location.range.start.character,
r.location.range.end.line,
r.location.range.end.character,
script_offset,
template_offset,
);
SfcRelatedInfo {
message: r.message,
filename: Some(r.location.uri),
start: rel_start,
end: rel_end,
}
})
.collect(),
});
}
self.bridge.close_virtual_document(&virtual_uri).await?;
}
result.analysis_time_ms = Some(start_time.elapsed().as_secs_f64() * 1000.0);
Ok(result)
}
pub async fn shutdown(&self) -> Result<(), TsgoBridgeError> {
self.bridge.shutdown().await
}
}
fn line_col_to_offset(content: &str, line: u32, col: u32) -> u32 {
let mut offset = 0;
let mut current_line = 0;
for (i, ch) in content.char_indices() {
if current_line == line {
return (i as u32) + col;
}
if ch == '\n' {
current_line += 1;
}
offset = i as u32 + 1;
}
offset + col
}
fn map_position_to_sfc(
virtual_ts: &VirtualTsOutput,
start_line: u32,
start_char: u32,
end_line: u32,
end_char: u32,
script_offset: u32,
_template_offset: u32,
) -> (u32, u32) {
let gen_start_offset = line_col_to_offset(&virtual_ts.content, start_line, start_char);
let gen_end_offset = line_col_to_offset(&virtual_ts.content, end_line, end_char);
if let Some(src_start) = virtual_ts.source_map.to_source(gen_start_offset) {
let src_end = virtual_ts
.source_map
.to_source(gen_end_offset)
.unwrap_or(src_start + (gen_end_offset - gen_start_offset));
return (src_start, src_end);
}
let start = script_offset + start_line * 80 + start_char;
let end = script_offset + end_line * 80 + end_char;
(start, end)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sfc_diagnostic_severity() {
assert_eq!(SfcDiagnosticSeverity::Error, SfcDiagnosticSeverity::Error);
assert_ne!(SfcDiagnosticSeverity::Error, SfcDiagnosticSeverity::Warning);
}
#[test]
fn test_type_check_service_options_default() {
let opts = TypeCheckServiceOptions::default();
assert!(opts.project_root.is_none());
assert!(opts.tsconfig_path.is_none());
assert!(!opts.check_cross_component);
assert!(!opts.check_template);
}
}