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 #[must_use]
99 pub fn visibility_keyword(self, is_public: bool) -> &'static str {
100 if !is_public {
101 return "";
102 }
103 match self {
104 Self::Rust => "pub ",
105 Self::TypeScript | Self::JavaScript => "export ",
106 Self::Python => "",
107 }
108 }
109
110 #[must_use]
113 pub fn function_keyword(self) -> &'static str {
114 match self {
115 Self::Rust => "fn",
116 Self::TypeScript | Self::JavaScript => "function",
117 Self::Python => "def",
118 }
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub struct ProjectFile {
129 pub path: PathBuf,
130 pub language: Language,
131 pub content_hash: String,
132 pub imports: Vec<Import>,
133 pub exports: Vec<Export>,
134 pub functions: Vec<Function>,
135 pub types: Vec<TypeDef>,
136 pub dependencies_used: Vec<DependencyUsage>,
137 pub language_ir: LanguageIR,
138 #[serde(default)]
147 pub file_doc: Option<String>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub struct Import {
154 pub module: String,
155 pub names: Vec<String>,
156 pub is_type_only: bool,
157 pub line: usize,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162#[serde(rename_all = "snake_case")]
163pub struct Export {
164 pub name: String,
165 pub is_default: bool,
166 pub is_type_only: bool,
167 pub line: usize,
168 pub end_line: usize,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(rename_all = "snake_case")]
187pub struct Function {
188 pub name: String,
189 pub is_public: bool,
190 pub is_async: bool,
191 pub line: usize,
192 pub end_line: usize,
193 #[serde(default)]
195 pub parameters: Vec<String>,
196 #[serde(default)]
204 pub doc_comment: Option<String>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(rename_all = "snake_case")]
210pub struct TypeDef {
211 pub name: String,
212 pub kind: TypeDefKind,
213 pub is_public: bool,
214 pub line: usize,
215 pub end_line: usize,
228 #[serde(default)]
233 pub doc_comment: Option<String>,
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(rename_all = "snake_case")]
239pub enum TypeDefKind {
240 Struct,
241 Enum,
242 Trait,
243 Interface,
244 Class,
245 TypeAlias,
246}
247
248impl TypeDefKind {
249 #[must_use]
256 pub fn keyword(&self) -> &'static str {
257 match self {
258 Self::Struct => "struct",
259 Self::Enum => "enum",
260 Self::Trait => "trait",
261 Self::Interface => "interface",
262 Self::Class => "class",
263 Self::TypeAlias => "type",
264 }
265 }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270#[serde(rename_all = "snake_case")]
271pub struct DependencyUsage {
272 pub package: String,
273 pub import_path: String,
274 pub line: usize,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279#[serde(rename_all = "snake_case")]
280pub enum LanguageIR {
281 Rust(RustIR),
282 TypeScript(TypeScriptIR),
283 JavaScript(JavaScriptIR),
284 Python(PythonIR),
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
289#[serde(rename_all = "snake_case")]
290pub struct ModDeclaration {
291 pub name: String,
293 pub line: usize,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
302#[serde(rename_all = "snake_case")]
303pub struct MacroCall {
304 pub name: String,
306 pub line: usize,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
327#[serde(rename_all = "snake_case")]
328pub struct FunctionCall {
329 pub callee: String,
332 pub line: usize,
335 pub end_line: usize,
338 pub snippet: String,
341}
342
343#[derive(Debug, Clone, Default, Serialize, Deserialize)]
345#[serde(rename_all = "snake_case")]
346pub struct RustIR {
347 pub mod_declarations: Vec<ModDeclaration>,
348 pub derive_macros: Vec<DeriveUsage>,
349 pub trait_implementations: Vec<TraitImpl>,
350 pub error_types: Vec<String>,
351 #[serde(default)]
357 pub macro_calls: Vec<MacroCall>,
358 #[serde(default)]
364 pub function_calls: Vec<FunctionCall>,
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
369#[serde(rename_all = "snake_case")]
370pub struct DeriveUsage {
371 pub type_name: String,
372 pub derives: Vec<String>,
373 pub line: usize,
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
378#[serde(rename_all = "snake_case")]
379pub struct TraitImpl {
380 pub trait_name: String,
381 pub type_name: String,
382 pub line: usize,
383}
384
385#[derive(Debug, Clone, Default, Serialize, Deserialize)]
387#[serde(rename_all = "snake_case")]
388pub struct TypeScriptIR {
389 pub has_barrel_exports: bool,
390 pub type_only_imports: Vec<String>,
391 pub decorators: Vec<String>,
392 pub default_export: bool,
393 #[serde(default)]
398 pub function_calls: Vec<FunctionCall>,
399}
400
401#[derive(Debug, Clone, Default, Serialize, Deserialize)]
403#[serde(rename_all = "snake_case")]
404pub struct JavaScriptIR {
405 pub module_system: ModuleSystem,
406 pub has_module_exports: bool,
407 pub require_calls: Vec<String>,
408 #[serde(default)]
414 pub function_calls: Vec<FunctionCall>,
415}
416
417#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
419#[serde(rename_all = "snake_case")]
420pub enum ModuleSystem {
421 #[default]
422 Unknown,
423 CommonJS,
424 ESM,
425}
426
427#[derive(Debug, Clone, Default, Serialize, Deserialize)]
429#[serde(rename_all = "snake_case")]
430pub struct PythonIR {
431 pub has_all_export: bool,
432 pub is_init_file: bool,
433 pub type_hints_used: bool,
434 pub decorators: Vec<String>,
435 #[serde(default)]
440 pub function_calls: Vec<FunctionCall>,
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn language_display() {
449 assert_eq!(Language::Rust.to_string(), "Rust");
450 assert_eq!(Language::TypeScript.to_string(), "TypeScript");
451 assert_eq!(Language::JavaScript.to_string(), "JavaScript");
452 assert_eq!(Language::Python.to_string(), "Python");
453 }
454
455 #[test]
456 fn language_roundtrip_str() {
457 let langs = [
458 Language::Rust,
459 Language::TypeScript,
460 Language::JavaScript,
461 Language::Python,
462 ];
463 for l in langs {
464 let parsed: Language = l.as_str().parse().unwrap();
465 assert_eq!(parsed, l);
466 }
467 }
468
469 #[test]
470 fn language_parse_unknown() {
471 assert!("go".parse::<Language>().is_err());
472 }
473
474 #[test]
475 fn language_extensions() {
476 assert_eq!(Language::Rust.extensions(), &["rs"]);
477 assert!(Language::TypeScript.extensions().contains(&"tsx"));
478 assert!(Language::JavaScript.extensions().contains(&"mjs"));
479 assert_eq!(Language::Python.extensions(), &["py"]);
480 }
481
482 #[test]
483 fn language_from_extension() {
484 assert_eq!(Language::from_extension("rs"), Some(Language::Rust));
485 assert_eq!(Language::from_extension("ts"), Some(Language::TypeScript));
486 assert_eq!(Language::from_extension("tsx"), Some(Language::TypeScript));
487 assert_eq!(Language::from_extension("js"), Some(Language::JavaScript));
488 assert_eq!(Language::from_extension("jsx"), Some(Language::JavaScript));
489 assert_eq!(Language::from_extension("mjs"), Some(Language::JavaScript));
490 assert_eq!(Language::from_extension("cjs"), Some(Language::JavaScript));
491 assert_eq!(Language::from_extension("py"), Some(Language::Python));
492 assert_eq!(Language::from_extension("go"), None);
493 assert_eq!(Language::from_extension(""), None);
494 }
495
496 #[test]
497 fn language_all() {
498 let all = Language::all();
499 assert_eq!(all.len(), 4);
500 assert!(all.contains(&Language::Rust));
501 assert!(all.contains(&Language::TypeScript));
502 assert!(all.contains(&Language::JavaScript));
503 assert!(all.contains(&Language::Python));
504 }
505
506 #[test]
507 fn language_ir_enum_covers_all_languages() {
508 let _rust = LanguageIR::Rust(RustIR::default());
510 let _ts = LanguageIR::TypeScript(TypeScriptIR::default());
511 let _js = LanguageIR::JavaScript(JavaScriptIR::default());
512 let _py = LanguageIR::Python(PythonIR::default());
513 }
514
515 #[test]
516 fn project_file_serialization_roundtrip() {
517 let pf = ProjectFile {
518 path: PathBuf::from("src/main.rs"),
519 language: Language::Rust,
520 content_hash: "abc123".to_owned(),
521 imports: vec![Import {
522 module: "std::io".to_owned(),
523 names: vec!["Read".to_owned()],
524 is_type_only: false,
525 line: 1,
526 }],
527 exports: Vec::new(),
528 functions: vec![Function {
529 name: "main".to_owned(),
530 is_public: false,
531 is_async: false,
532 line: 3,
533 end_line: 5,
534 parameters: vec![],
535 doc_comment: None,
536 }],
537 types: Vec::new(),
538 dependencies_used: Vec::new(),
539 language_ir: LanguageIR::Rust(RustIR::default()),
540 file_doc: None,
541 };
542
543 let json = serde_json::to_string(&pf).expect("serialize");
544 let deserialized: ProjectFile = serde_json::from_str(&json).expect("deserialize");
545 assert_eq!(deserialized.path, pf.path);
546 assert_eq!(deserialized.language, pf.language);
547 assert_eq!(deserialized.content_hash, pf.content_hash);
548 assert_eq!(deserialized.imports.len(), 1);
549 assert_eq!(deserialized.functions.len(), 1);
550 }
551
552 #[test]
553 fn module_system_default_is_unknown() {
554 assert_eq!(ModuleSystem::default(), ModuleSystem::Unknown);
555 }
556}