1pub mod python;
7pub mod typescript;
8
9use std::path::{Path, PathBuf};
10
11use serde::{Deserialize, Serialize};
12
13use crate::graph::EdgeKind;
14use crate::vfs::Vfs;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
21#[repr(transparent)]
22pub struct ParseError(String);
23
24impl ParseError {
25 pub fn new(msg: impl Into<String>) -> Self {
26 Self(msg.into())
27 }
28}
29
30impl std::fmt::Display for ParseError {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.write_str(&self.0)
33 }
34}
35
36impl std::error::Error for ParseError {}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[non_exhaustive]
41pub struct RawImport {
42 pub specifier: String,
43 pub kind: EdgeKind,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[non_exhaustive]
49pub struct ParseResult {
50 pub imports: Vec<RawImport>,
51 pub unresolvable_dynamic: usize,
52}
53
54pub trait LanguageSupport: Send + Sync {
56 fn extensions(&self) -> &'static [&'static str];
57 fn parse(&self, path: &Path, source: &str) -> Result<ParseResult, ParseError>;
58 fn resolve(&self, from_dir: &Path, specifier: &str) -> Option<PathBuf>;
59 fn package_name(&self, resolved_path: &Path) -> Option<String>;
60 fn workspace_package_name(&self, file_path: &Path, project_root: &Path) -> Option<String>;
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65#[non_exhaustive]
66pub enum ProjectKind {
67 TypeScript,
68 Python,
69}
70
71pub fn detect_project(entry: &Path, vfs: &dyn Vfs) -> Option<(PathBuf, ProjectKind)> {
75 let kind = match entry.extension().and_then(|e| e.to_str()) {
76 Some("ts" | "tsx" | "js" | "jsx" | "mjs" | "cjs" | "mts" | "cts") => {
77 ProjectKind::TypeScript
78 }
79 Some("py") => ProjectKind::Python,
80 _ => return None,
81 };
82
83 let markers: &[&str] = match kind {
84 ProjectKind::TypeScript => &["package.json"],
85 ProjectKind::Python => &["pyproject.toml", "setup.py", "setup.cfg"],
86 };
87
88 let root = find_root_with_markers(entry, markers, vfs)
89 .unwrap_or_else(|| entry.parent().unwrap_or(entry).to_path_buf());
90
91 Some((root, kind))
92}
93
94fn find_root_with_markers(entry: &Path, markers: &[&str], vfs: &dyn Vfs) -> Option<PathBuf> {
95 let mut dir = entry.parent()?;
96 loop {
97 for marker in markers {
98 if vfs.exists(&dir.join(marker)) {
99 return Some(dir.to_path_buf());
100 }
101 }
102 dir = dir.parent()?;
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::vfs::OsVfs;
110 use std::fs;
111 use tempfile::tempdir;
112
113 #[test]
114 fn detect_typescript_from_ts_extension() {
115 let tmp = tempdir().unwrap();
116 let root = tmp.path().canonicalize().unwrap();
117 fs::write(root.join("package.json"), "{}").unwrap();
118 let entry = root.join("src/index.ts");
119 fs::create_dir_all(entry.parent().unwrap()).unwrap();
120 fs::write(&entry, "").unwrap();
121
122 let (detected_root, kind) = detect_project(&entry, &OsVfs).unwrap();
123 assert_eq!(kind, ProjectKind::TypeScript);
124 assert_eq!(detected_root, root);
125 }
126
127 #[test]
128 fn detect_python_from_py_extension() {
129 let tmp = tempdir().unwrap();
130 let root = tmp.path().canonicalize().unwrap();
131 fs::write(root.join("pyproject.toml"), "").unwrap();
132 let entry = root.join("src/main.py");
133 fs::create_dir_all(entry.parent().unwrap()).unwrap();
134 fs::write(&entry, "").unwrap();
135
136 let (detected_root, kind) = detect_project(&entry, &OsVfs).unwrap();
137 assert_eq!(kind, ProjectKind::Python);
138 assert_eq!(detected_root, root);
139 }
140
141 #[test]
142 fn detect_python_setup_py_fallback() {
143 let tmp = tempdir().unwrap();
144 let root = tmp.path().canonicalize().unwrap();
145 fs::write(root.join("setup.py"), "").unwrap();
147 let entry = root.join("app.py");
148 fs::write(&entry, "").unwrap();
149
150 let (detected_root, kind) = detect_project(&entry, &OsVfs).unwrap();
151 assert_eq!(kind, ProjectKind::Python);
152 assert_eq!(detected_root, root);
153 }
154}