use std::path::{Path, PathBuf};
use std::process::Command;
use super::error::{TsgoNotFoundError, TsgoResult};
use super::type_checker::TypeCheckResult;
use super::virtual_project::VirtualProject;
use super::Diagnostic;
pub struct TsgoExecutor {
tsgo_path: PathBuf,
}
impl TsgoExecutor {
pub fn new(project_root: &Path) -> Result<Self, TsgoNotFoundError> {
let local_tsgo = project_root.join("node_modules/.bin/tsgo");
if local_tsgo.exists() {
return Ok(Self {
tsgo_path: local_tsgo,
});
}
if let Ok(global_tsgo) = which::which("tsgo") {
return Ok(Self {
tsgo_path: global_tsgo,
});
}
if let Some(mise_tsgo) = Self::find_mise_tsgo() {
return Ok(Self {
tsgo_path: mise_tsgo,
});
}
Err(TsgoNotFoundError::new(project_root))
}
fn find_mise_tsgo() -> Option<PathBuf> {
let mise_data_dir = std::env::var("MISE_DATA_DIR")
.map(PathBuf::from)
.ok()
.or_else(|| {
dirs::data_local_dir().map(|d| d.join("mise"))
})?;
let shims_tsgo = mise_data_dir.join("shims").join("tsgo");
if shims_tsgo.exists() {
return Some(shims_tsgo);
}
if let Some(xdg_data) = std::env::var("XDG_DATA_HOME").ok().map(PathBuf::from) {
let xdg_tsgo = xdg_data.join("mise").join("shims").join("tsgo");
if xdg_tsgo.exists() {
return Some(xdg_tsgo);
}
}
if let Some(home) = dirs::home_dir() {
let home_tsgo = home.join(".local/share/mise/shims/tsgo");
if home_tsgo.exists() {
return Some(home_tsgo);
}
}
None
}
pub fn tsgo_path(&self) -> &Path {
&self.tsgo_path
}
pub fn check(&self, project: &VirtualProject) -> TsgoResult<TypeCheckResult> {
project.materialize()?;
let output = Command::new(&self.tsgo_path)
.current_dir(project.virtual_root())
.args([
"--project",
"tsconfig.json",
"--noEmit",
"--pretty",
"false",
])
.output()?;
let stderr = String::from_utf8_lossy(&output.stderr);
let diagnostics = self.parse_tsgo_output(&stderr, project);
let exit_code = output.status.code().unwrap_or(-1);
Ok(TypeCheckResult {
diagnostics,
exit_code,
success: output.status.success(),
})
}
fn parse_tsgo_output(&self, output: &str, project: &VirtualProject) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for line in output.lines() {
if let Some(diag) = self.parse_diagnostic_line(line, project) {
diagnostics.push(diag);
}
}
diagnostics
}
fn parse_diagnostic_line(&self, line: &str, project: &VirtualProject) -> Option<Diagnostic> {
let paren_pos = line.find('(')?;
let colon_pos = line.find("): ")?;
let file_path = &line[..paren_pos];
let location = &line[paren_pos + 1..colon_pos];
let rest = &line[colon_pos + 3..];
let (line_num, col_num) = self.parse_location(location)?;
let (severity, code, message) = self.parse_message(rest)?;
let virtual_path = project.virtual_root().join(file_path);
let original = project.map_to_original(&virtual_path, line_num - 1, col_num - 1);
if let Some(orig) = original {
Some(Diagnostic {
file: orig.path,
line: orig.line,
column: orig.column,
message,
code,
severity,
block_type: orig.block_type,
})
} else {
Some(Diagnostic {
file: PathBuf::from(file_path),
line: line_num - 1,
column: col_num - 1,
message,
code,
severity,
block_type: None,
})
}
}
fn parse_location(&self, s: &str) -> Option<(u32, u32)> {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() != 2 {
return None;
}
let line = parts[0].parse().ok()?;
let col = parts[1].parse().ok()?;
Some((line, col))
}
fn parse_message(&self, s: &str) -> Option<(u8, Option<u32>, String)> {
let severity = if s.starts_with("error") {
1
} else if s.starts_with("warning") {
2
} else {
1 };
let code = if let Some(ts_start) = s.find("TS") {
let code_end = s[ts_start..]
.find(':')
.map(|i| ts_start + i)
.unwrap_or(s.len());
s[ts_start + 2..code_end].parse().ok()
} else {
None
};
let message = if let Some(msg_start) = s.find(": ") {
s[msg_start + 2..].to_string()
} else {
s.to_string()
};
Some((severity, code, message))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_location() {
let executor = TsgoExecutor {
tsgo_path: PathBuf::from("tsgo"),
};
assert_eq!(executor.parse_location("10,5"), Some((10, 5)));
assert_eq!(executor.parse_location("1,1"), Some((1, 1)));
assert_eq!(executor.parse_location("invalid"), None);
}
#[test]
fn test_parse_message() {
let executor = TsgoExecutor {
tsgo_path: PathBuf::from("tsgo"),
};
let result = executor.parse_message("error TS2304: Cannot find name 'foo'.");
assert!(result.is_some());
let (severity, code, message) = result.unwrap();
assert_eq!(severity, 1);
assert_eq!(code, Some(2304));
assert_eq!(message, "Cannot find name 'foo'.");
let result = executor.parse_message("warning TS2551: Did you mean 'bar'?");
assert!(result.is_some());
let (severity, code, message) = result.unwrap();
assert_eq!(severity, 2);
assert_eq!(code, Some(2551));
assert_eq!(message, "Did you mean 'bar'?");
}
}