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