1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::path::PathBuf;
4
5use crate::error::ParseEnumError;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum Language {
11 Rust,
12 TypeScript,
13 JavaScript,
14 Python,
15}
16
17impl Language {
18 pub fn as_str(&self) -> &'static str {
20 match self {
21 Self::Rust => "rust",
22 Self::TypeScript => "typescript",
23 Self::JavaScript => "javascript",
24 Self::Python => "python",
25 }
26 }
27}
28
29impl fmt::Display for Language {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 Self::Rust => write!(f, "Rust"),
33 Self::TypeScript => write!(f, "TypeScript"),
34 Self::JavaScript => write!(f, "JavaScript"),
35 Self::Python => write!(f, "Python"),
36 }
37 }
38}
39
40impl std::str::FromStr for Language {
41 type Err = ParseEnumError;
42
43 fn from_str(s: &str) -> Result<Self, Self::Err> {
44 match s {
45 "rust" => Ok(Self::Rust),
46 "typescript" => Ok(Self::TypeScript),
47 "javascript" => Ok(Self::JavaScript),
48 "python" => Ok(Self::Python),
49 _ => Err(ParseEnumError {
50 type_name: "Language",
51 value: s.to_owned(),
52 }),
53 }
54 }
55}
56
57impl Language {
58 pub fn extensions(&self) -> &'static [&'static str] {
60 match self {
61 Self::Rust => &["rs"],
62 Self::TypeScript => &["ts", "tsx"],
63 Self::JavaScript => &["js", "jsx", "mjs", "cjs"],
64 Self::Python => &["py"],
65 }
66 }
67
68 pub fn all() -> &'static [Language] {
70 &[Self::Rust, Self::TypeScript, Self::JavaScript, Self::Python]
71 }
72
73 pub fn from_extension(ext: &str) -> Option<Self> {
77 match ext {
78 "rs" => Some(Self::Rust),
79 "ts" | "tsx" => Some(Self::TypeScript),
80 "js" | "jsx" | "mjs" | "cjs" => Some(Self::JavaScript),
81 "py" => Some(Self::Python),
82 _ => None,
83 }
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub struct ProjectFile {
94 pub path: PathBuf,
95 pub language: Language,
96 pub content_hash: String,
97 pub imports: Vec<Import>,
98 pub exports: Vec<Export>,
99 pub functions: Vec<Function>,
100 pub types: Vec<TypeDef>,
101 pub dependencies_used: Vec<DependencyUsage>,
102 pub language_ir: LanguageIR,
103 #[serde(default)]
112 pub file_doc: Option<String>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub struct Import {
119 pub module: String,
120 pub names: Vec<String>,
121 pub is_type_only: bool,
122 pub line: usize,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub struct Export {
129 pub name: String,
130 pub is_default: bool,
131 pub is_type_only: bool,
132 pub line: usize,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub struct Function {
139 pub name: String,
140 pub is_public: bool,
141 pub is_async: bool,
142 pub line: usize,
143 pub end_line: usize,
144 #[serde(default)]
146 pub parameters: Vec<String>,
147 #[serde(default)]
155 pub doc_comment: Option<String>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub struct TypeDef {
162 pub name: String,
163 pub kind: TypeDefKind,
164 pub is_public: bool,
165 pub line: usize,
166 #[serde(default)]
171 pub doc_comment: Option<String>,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(rename_all = "snake_case")]
177pub enum TypeDefKind {
178 Struct,
179 Enum,
180 Trait,
181 Interface,
182 Class,
183 TypeAlias,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188#[serde(rename_all = "snake_case")]
189pub struct DependencyUsage {
190 pub package: String,
191 pub import_path: String,
192 pub line: usize,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197#[serde(rename_all = "snake_case")]
198pub enum LanguageIR {
199 Rust(RustIR),
200 TypeScript(TypeScriptIR),
201 JavaScript(JavaScriptIR),
202 Python(PythonIR),
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
207#[serde(rename_all = "snake_case")]
208pub struct ModDeclaration {
209 pub name: String,
211 pub line: usize,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
220#[serde(rename_all = "snake_case")]
221pub struct MacroCall {
222 pub name: String,
224 pub line: usize,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
245#[serde(rename_all = "snake_case")]
246pub struct FunctionCall {
247 pub callee: String,
250 pub line: usize,
253 pub end_line: usize,
256 pub snippet: String,
259}
260
261#[derive(Debug, Clone, Default, Serialize, Deserialize)]
263#[serde(rename_all = "snake_case")]
264pub struct RustIR {
265 pub mod_declarations: Vec<ModDeclaration>,
266 pub derive_macros: Vec<DeriveUsage>,
267 pub trait_implementations: Vec<TraitImpl>,
268 pub error_types: Vec<String>,
269 #[serde(default)]
275 pub macro_calls: Vec<MacroCall>,
276 #[serde(default)]
282 pub function_calls: Vec<FunctionCall>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287#[serde(rename_all = "snake_case")]
288pub struct DeriveUsage {
289 pub type_name: String,
290 pub derives: Vec<String>,
291 pub line: usize,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296#[serde(rename_all = "snake_case")]
297pub struct TraitImpl {
298 pub trait_name: String,
299 pub type_name: String,
300 pub line: usize,
301}
302
303#[derive(Debug, Clone, Default, Serialize, Deserialize)]
305#[serde(rename_all = "snake_case")]
306pub struct TypeScriptIR {
307 pub has_barrel_exports: bool,
308 pub type_only_imports: Vec<String>,
309 pub decorators: Vec<String>,
310 pub default_export: bool,
311 #[serde(default)]
316 pub function_calls: Vec<FunctionCall>,
317}
318
319#[derive(Debug, Clone, Default, Serialize, Deserialize)]
321#[serde(rename_all = "snake_case")]
322pub struct JavaScriptIR {
323 pub module_system: ModuleSystem,
324 pub has_module_exports: bool,
325 pub require_calls: Vec<String>,
326 #[serde(default)]
332 pub function_calls: Vec<FunctionCall>,
333}
334
335#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
337#[serde(rename_all = "snake_case")]
338pub enum ModuleSystem {
339 #[default]
340 Unknown,
341 CommonJS,
342 ESM,
343}
344
345#[derive(Debug, Clone, Default, Serialize, Deserialize)]
347#[serde(rename_all = "snake_case")]
348pub struct PythonIR {
349 pub has_all_export: bool,
350 pub is_init_file: bool,
351 pub type_hints_used: bool,
352 pub decorators: Vec<String>,
353 #[serde(default)]
358 pub function_calls: Vec<FunctionCall>,
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn language_display() {
367 assert_eq!(Language::Rust.to_string(), "Rust");
368 assert_eq!(Language::TypeScript.to_string(), "TypeScript");
369 assert_eq!(Language::JavaScript.to_string(), "JavaScript");
370 assert_eq!(Language::Python.to_string(), "Python");
371 }
372
373 #[test]
374 fn language_roundtrip_str() {
375 let langs = [
376 Language::Rust,
377 Language::TypeScript,
378 Language::JavaScript,
379 Language::Python,
380 ];
381 for l in langs {
382 let parsed: Language = l.as_str().parse().unwrap();
383 assert_eq!(parsed, l);
384 }
385 }
386
387 #[test]
388 fn language_parse_unknown() {
389 assert!("go".parse::<Language>().is_err());
390 }
391
392 #[test]
393 fn language_extensions() {
394 assert_eq!(Language::Rust.extensions(), &["rs"]);
395 assert!(Language::TypeScript.extensions().contains(&"tsx"));
396 assert!(Language::JavaScript.extensions().contains(&"mjs"));
397 assert_eq!(Language::Python.extensions(), &["py"]);
398 }
399
400 #[test]
401 fn language_from_extension() {
402 assert_eq!(Language::from_extension("rs"), Some(Language::Rust));
403 assert_eq!(Language::from_extension("ts"), Some(Language::TypeScript));
404 assert_eq!(Language::from_extension("tsx"), Some(Language::TypeScript));
405 assert_eq!(Language::from_extension("js"), Some(Language::JavaScript));
406 assert_eq!(Language::from_extension("jsx"), Some(Language::JavaScript));
407 assert_eq!(Language::from_extension("mjs"), Some(Language::JavaScript));
408 assert_eq!(Language::from_extension("cjs"), Some(Language::JavaScript));
409 assert_eq!(Language::from_extension("py"), Some(Language::Python));
410 assert_eq!(Language::from_extension("go"), None);
411 assert_eq!(Language::from_extension(""), None);
412 }
413
414 #[test]
415 fn language_all() {
416 let all = Language::all();
417 assert_eq!(all.len(), 4);
418 assert!(all.contains(&Language::Rust));
419 assert!(all.contains(&Language::TypeScript));
420 assert!(all.contains(&Language::JavaScript));
421 assert!(all.contains(&Language::Python));
422 }
423
424 #[test]
425 fn language_ir_enum_covers_all_languages() {
426 let _rust = LanguageIR::Rust(RustIR::default());
428 let _ts = LanguageIR::TypeScript(TypeScriptIR::default());
429 let _js = LanguageIR::JavaScript(JavaScriptIR::default());
430 let _py = LanguageIR::Python(PythonIR::default());
431 }
432
433 #[test]
434 fn project_file_serialization_roundtrip() {
435 let pf = ProjectFile {
436 path: PathBuf::from("src/main.rs"),
437 language: Language::Rust,
438 content_hash: "abc123".to_owned(),
439 imports: vec![Import {
440 module: "std::io".to_owned(),
441 names: vec!["Read".to_owned()],
442 is_type_only: false,
443 line: 1,
444 }],
445 exports: Vec::new(),
446 functions: vec![Function {
447 name: "main".to_owned(),
448 is_public: false,
449 is_async: false,
450 line: 3,
451 end_line: 5,
452 parameters: vec![],
453 doc_comment: None,
454 }],
455 types: Vec::new(),
456 dependencies_used: Vec::new(),
457 language_ir: LanguageIR::Rust(RustIR::default()),
458 file_doc: None,
459 };
460
461 let json = serde_json::to_string(&pf).expect("serialize");
462 let deserialized: ProjectFile = serde_json::from_str(&json).expect("deserialize");
463 assert_eq!(deserialized.path, pf.path);
464 assert_eq!(deserialized.language, pf.language);
465 assert_eq!(deserialized.content_hash, pf.content_hash);
466 assert_eq!(deserialized.imports.len(), 1);
467 assert_eq!(deserialized.functions.len(), 1);
468 }
469
470 #[test]
471 fn module_system_default_is_unknown() {
472 assert_eq!(ModuleSystem::default(), ModuleSystem::Unknown);
473 }
474}