Skip to main content

cgx_engine/
parser.rs

1use std::collections::HashMap;
2
3use crate::walker::{Language, SourceFile};
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
6pub enum NodeKind {
7    File,
8    Function,
9    Class,
10    Variable,
11    Type,
12    Module,
13    Author,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
17pub enum CommentKind {
18    /// Regular JS/JSDoc comment above a function or at the top level
19    Standard,
20    /// `{/* ... */}` expression comment inside JSX return body
21    JsxExpression,
22    /// JSX expression comment whose inner text starts with `<` — commented-out JSX code
23    JsxCommentedCode,
24}
25
26impl CommentKind {
27    pub fn as_str(&self) -> &'static str {
28        match self {
29            CommentKind::Standard => "code",
30            CommentKind::JsxExpression => "jsx",
31            CommentKind::JsxCommentedCode => "jsx_commented_code",
32        }
33    }
34}
35
36#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
37pub struct CommentTag {
38    pub tag_type: String,
39    pub text: String,
40    pub line: u32,
41    pub comment_kind: CommentKind,
42}
43
44impl NodeKind {
45    pub fn as_str(&self) -> &'static str {
46        match self {
47            NodeKind::File => "File",
48            NodeKind::Function => "Function",
49            NodeKind::Class => "Class",
50            NodeKind::Variable => "Variable",
51            NodeKind::Type => "Type",
52            NodeKind::Module => "Module",
53            NodeKind::Author => "Author",
54        }
55    }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
59#[serde(rename_all = "UPPERCASE")]
60pub enum EdgeKind {
61    Calls,
62    Imports,
63    Inherits,
64    Exports,
65    CoChanges,
66    Owns,
67    DependsOn,
68    Tests,
69}
70
71impl EdgeKind {
72    pub fn as_str(&self) -> &'static str {
73        match self {
74            EdgeKind::Calls => "CALLS",
75            EdgeKind::Imports => "IMPORTS",
76            EdgeKind::Inherits => "INHERITS",
77            EdgeKind::Exports => "EXPORTS",
78            EdgeKind::CoChanges => "CO_CHANGES",
79            EdgeKind::Owns => "OWNS",
80            EdgeKind::DependsOn => "DEPENDS_ON",
81            EdgeKind::Tests => "TESTS",
82        }
83    }
84}
85
86#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
87pub struct NodeDef {
88    pub id: String,
89    pub kind: NodeKind,
90    pub name: String,
91    pub path: String,
92    pub line_start: u32,
93    pub line_end: u32,
94    #[serde(default)]
95    pub metadata: serde_json::Value,
96}
97
98impl Default for NodeDef {
99    fn default() -> Self {
100        Self {
101            id: String::new(),
102            kind: NodeKind::File,
103            name: String::new(),
104            path: String::new(),
105            line_start: 0,
106            line_end: 0,
107            metadata: serde_json::Value::Null,
108        }
109    }
110}
111
112#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
113pub struct EdgeDef {
114    pub src: String,
115    pub dst: String,
116    pub kind: EdgeKind,
117    #[serde(default = "default_edge_weight")]
118    pub weight: f64,
119    #[serde(default = "default_edge_weight")]
120    pub confidence: f64,
121}
122
123impl Default for EdgeDef {
124    fn default() -> Self {
125        Self {
126            src: String::new(),
127            dst: String::new(),
128            kind: EdgeKind::Calls,
129            weight: 1.0,
130            confidence: 1.0,
131        }
132    }
133}
134
135fn default_edge_weight() -> f64 {
136    1.0
137}
138
139#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
140pub struct ParseResult {
141    pub nodes: Vec<NodeDef>,
142    pub edges: Vec<EdgeDef>,
143    #[serde(default)]
144    pub comment_tags: Vec<CommentTag>,
145}
146
147impl ParseResult {
148    pub fn new() -> Self {
149        Self {
150            nodes: Vec::new(),
151            edges: Vec::new(),
152            comment_tags: Vec::new(),
153        }
154    }
155}
156
157impl Default for ParseResult {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163pub trait LanguageParser: Send + Sync {
164    fn extensions(&self) -> &[&str];
165    fn extract(&self, file: &SourceFile) -> anyhow::Result<ParseResult>;
166}
167
168pub struct ParserRegistry {
169    parsers: HashMap<Language, Box<dyn LanguageParser>>,
170}
171
172impl ParserRegistry {
173    pub fn new() -> Self {
174        let mut parsers: HashMap<Language, Box<dyn LanguageParser>> = HashMap::new();
175
176        parsers.insert(
177            Language::TypeScript,
178            Box::new(super::parsers::ts::TypeScriptParser::new()),
179        );
180        parsers.insert(
181            Language::JavaScript,
182            Box::new(super::parsers::ts::TypeScriptParser::new()),
183        );
184        parsers.insert(
185            Language::Python,
186            Box::new(super::parsers::py::PythonParser::new()),
187        );
188        parsers.insert(
189            Language::Rust,
190            Box::new(super::parsers::rust::RustParser::new()),
191        );
192        parsers.insert(Language::Go, Box::new(super::parsers::go::GoParser::new()));
193        parsers.insert(
194            Language::Java,
195            Box::new(super::parsers::java::JavaParser::new()),
196        );
197        parsers.insert(
198            Language::CSharp,
199            Box::new(super::parsers::java::JavaParser::new()),
200        );
201        parsers.insert(
202            Language::Php,
203            Box::new(super::parsers::php::PhpParser::new()),
204        );
205
206        Self { parsers }
207    }
208
209    pub fn parse(&self, file: &SourceFile) -> anyhow::Result<ParseResult> {
210        if let Some(parser) = self.parsers.get(&file.language) {
211            parser.extract(file)
212        } else {
213            Ok(ParseResult::new())
214        }
215    }
216
217    pub fn parse_all(&self, files: &[SourceFile]) -> Vec<ParseResult> {
218        use rayon::prelude::*;
219        files
220            .par_iter()
221            .map(|file| {
222                self.parse(file).unwrap_or_else(|e| {
223                    tracing::warn!("Parse error in {}: {}", file.relative_path, e);
224                    ParseResult::new()
225                })
226            })
227            .collect()
228    }
229}
230
231impl Default for ParserRegistry {
232    fn default() -> Self {
233        Self::new()
234    }
235}