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 pub end_line: usize,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(rename_all = "snake_case")]
152pub struct Function {
153 pub name: String,
154 pub is_public: bool,
155 pub is_async: bool,
156 pub line: usize,
157 pub end_line: usize,
158 #[serde(default)]
160 pub parameters: Vec<String>,
161 #[serde(default)]
169 pub doc_comment: Option<String>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174#[serde(rename_all = "snake_case")]
175pub struct TypeDef {
176 pub name: String,
177 pub kind: TypeDefKind,
178 pub is_public: bool,
179 pub line: usize,
180 pub end_line: usize,
193 #[serde(default)]
198 pub doc_comment: Option<String>,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(rename_all = "snake_case")]
204pub enum TypeDefKind {
205 Struct,
206 Enum,
207 Trait,
208 Interface,
209 Class,
210 TypeAlias,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215#[serde(rename_all = "snake_case")]
216pub struct DependencyUsage {
217 pub package: String,
218 pub import_path: String,
219 pub line: usize,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(rename_all = "snake_case")]
225pub enum LanguageIR {
226 Rust(RustIR),
227 TypeScript(TypeScriptIR),
228 JavaScript(JavaScriptIR),
229 Python(PythonIR),
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
234#[serde(rename_all = "snake_case")]
235pub struct ModDeclaration {
236 pub name: String,
238 pub line: usize,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
247#[serde(rename_all = "snake_case")]
248pub struct MacroCall {
249 pub name: String,
251 pub line: usize,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
272#[serde(rename_all = "snake_case")]
273pub struct FunctionCall {
274 pub callee: String,
277 pub line: usize,
280 pub end_line: usize,
283 pub snippet: String,
286}
287
288#[derive(Debug, Clone, Default, Serialize, Deserialize)]
290#[serde(rename_all = "snake_case")]
291pub struct RustIR {
292 pub mod_declarations: Vec<ModDeclaration>,
293 pub derive_macros: Vec<DeriveUsage>,
294 pub trait_implementations: Vec<TraitImpl>,
295 pub error_types: Vec<String>,
296 #[serde(default)]
302 pub macro_calls: Vec<MacroCall>,
303 #[serde(default)]
309 pub function_calls: Vec<FunctionCall>,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314#[serde(rename_all = "snake_case")]
315pub struct DeriveUsage {
316 pub type_name: String,
317 pub derives: Vec<String>,
318 pub line: usize,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323#[serde(rename_all = "snake_case")]
324pub struct TraitImpl {
325 pub trait_name: String,
326 pub type_name: String,
327 pub line: usize,
328}
329
330#[derive(Debug, Clone, Default, Serialize, Deserialize)]
332#[serde(rename_all = "snake_case")]
333pub struct TypeScriptIR {
334 pub has_barrel_exports: bool,
335 pub type_only_imports: Vec<String>,
336 pub decorators: Vec<String>,
337 pub default_export: bool,
338 #[serde(default)]
343 pub function_calls: Vec<FunctionCall>,
344}
345
346#[derive(Debug, Clone, Default, Serialize, Deserialize)]
348#[serde(rename_all = "snake_case")]
349pub struct JavaScriptIR {
350 pub module_system: ModuleSystem,
351 pub has_module_exports: bool,
352 pub require_calls: Vec<String>,
353 #[serde(default)]
359 pub function_calls: Vec<FunctionCall>,
360}
361
362#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
364#[serde(rename_all = "snake_case")]
365pub enum ModuleSystem {
366 #[default]
367 Unknown,
368 CommonJS,
369 ESM,
370}
371
372#[derive(Debug, Clone, Default, Serialize, Deserialize)]
374#[serde(rename_all = "snake_case")]
375pub struct PythonIR {
376 pub has_all_export: bool,
377 pub is_init_file: bool,
378 pub type_hints_used: bool,
379 pub decorators: Vec<String>,
380 #[serde(default)]
385 pub function_calls: Vec<FunctionCall>,
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn language_display() {
394 assert_eq!(Language::Rust.to_string(), "Rust");
395 assert_eq!(Language::TypeScript.to_string(), "TypeScript");
396 assert_eq!(Language::JavaScript.to_string(), "JavaScript");
397 assert_eq!(Language::Python.to_string(), "Python");
398 }
399
400 #[test]
401 fn language_roundtrip_str() {
402 let langs = [
403 Language::Rust,
404 Language::TypeScript,
405 Language::JavaScript,
406 Language::Python,
407 ];
408 for l in langs {
409 let parsed: Language = l.as_str().parse().unwrap();
410 assert_eq!(parsed, l);
411 }
412 }
413
414 #[test]
415 fn language_parse_unknown() {
416 assert!("go".parse::<Language>().is_err());
417 }
418
419 #[test]
420 fn language_extensions() {
421 assert_eq!(Language::Rust.extensions(), &["rs"]);
422 assert!(Language::TypeScript.extensions().contains(&"tsx"));
423 assert!(Language::JavaScript.extensions().contains(&"mjs"));
424 assert_eq!(Language::Python.extensions(), &["py"]);
425 }
426
427 #[test]
428 fn language_from_extension() {
429 assert_eq!(Language::from_extension("rs"), Some(Language::Rust));
430 assert_eq!(Language::from_extension("ts"), Some(Language::TypeScript));
431 assert_eq!(Language::from_extension("tsx"), Some(Language::TypeScript));
432 assert_eq!(Language::from_extension("js"), Some(Language::JavaScript));
433 assert_eq!(Language::from_extension("jsx"), Some(Language::JavaScript));
434 assert_eq!(Language::from_extension("mjs"), Some(Language::JavaScript));
435 assert_eq!(Language::from_extension("cjs"), Some(Language::JavaScript));
436 assert_eq!(Language::from_extension("py"), Some(Language::Python));
437 assert_eq!(Language::from_extension("go"), None);
438 assert_eq!(Language::from_extension(""), None);
439 }
440
441 #[test]
442 fn language_all() {
443 let all = Language::all();
444 assert_eq!(all.len(), 4);
445 assert!(all.contains(&Language::Rust));
446 assert!(all.contains(&Language::TypeScript));
447 assert!(all.contains(&Language::JavaScript));
448 assert!(all.contains(&Language::Python));
449 }
450
451 #[test]
452 fn language_ir_enum_covers_all_languages() {
453 let _rust = LanguageIR::Rust(RustIR::default());
455 let _ts = LanguageIR::TypeScript(TypeScriptIR::default());
456 let _js = LanguageIR::JavaScript(JavaScriptIR::default());
457 let _py = LanguageIR::Python(PythonIR::default());
458 }
459
460 #[test]
461 fn project_file_serialization_roundtrip() {
462 let pf = ProjectFile {
463 path: PathBuf::from("src/main.rs"),
464 language: Language::Rust,
465 content_hash: "abc123".to_owned(),
466 imports: vec![Import {
467 module: "std::io".to_owned(),
468 names: vec!["Read".to_owned()],
469 is_type_only: false,
470 line: 1,
471 }],
472 exports: Vec::new(),
473 functions: vec![Function {
474 name: "main".to_owned(),
475 is_public: false,
476 is_async: false,
477 line: 3,
478 end_line: 5,
479 parameters: vec![],
480 doc_comment: None,
481 }],
482 types: Vec::new(),
483 dependencies_used: Vec::new(),
484 language_ir: LanguageIR::Rust(RustIR::default()),
485 file_doc: None,
486 };
487
488 let json = serde_json::to_string(&pf).expect("serialize");
489 let deserialized: ProjectFile = serde_json::from_str(&json).expect("deserialize");
490 assert_eq!(deserialized.path, pf.path);
491 assert_eq!(deserialized.language, pf.language);
492 assert_eq!(deserialized.content_hash, pf.content_hash);
493 assert_eq!(deserialized.imports.len(), 1);
494 assert_eq!(deserialized.functions.len(), 1);
495 }
496
497 #[test]
498 fn module_system_default_is_unknown() {
499 assert_eq!(ModuleSystem::default(), ModuleSystem::Unknown);
500 }
501}