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}
69
70impl EdgeKind {
71    pub fn as_str(&self) -> &'static str {
72        match self {
73            EdgeKind::Calls => "CALLS",
74            EdgeKind::Imports => "IMPORTS",
75            EdgeKind::Inherits => "INHERITS",
76            EdgeKind::Exports => "EXPORTS",
77            EdgeKind::CoChanges => "CO_CHANGES",
78            EdgeKind::Owns => "OWNS",
79            EdgeKind::DependsOn => "DEPENDS_ON",
80        }
81    }
82}
83
84#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
85pub struct NodeDef {
86    pub id: String,
87    pub kind: NodeKind,
88    pub name: String,
89    pub path: String,
90    pub line_start: u32,
91    pub line_end: u32,
92    #[serde(default)]
93    pub metadata: serde_json::Value,
94}
95
96impl Default for NodeDef {
97    fn default() -> Self {
98        Self {
99            id: String::new(),
100            kind: NodeKind::File,
101            name: String::new(),
102            path: String::new(),
103            line_start: 0,
104            line_end: 0,
105            metadata: serde_json::Value::Null,
106        }
107    }
108}
109
110#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
111pub struct EdgeDef {
112    pub src: String,
113    pub dst: String,
114    pub kind: EdgeKind,
115    #[serde(default = "default_edge_weight")]
116    pub weight: f64,
117    #[serde(default = "default_edge_weight")]
118    pub confidence: f64,
119}
120
121impl Default for EdgeDef {
122    fn default() -> Self {
123        Self {
124            src: String::new(),
125            dst: String::new(),
126            kind: EdgeKind::Calls,
127            weight: 1.0,
128            confidence: 1.0,
129        }
130    }
131}
132
133fn default_edge_weight() -> f64 {
134    1.0
135}
136
137#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
138pub struct ParseResult {
139    pub nodes: Vec<NodeDef>,
140    pub edges: Vec<EdgeDef>,
141    #[serde(default)]
142    pub comment_tags: Vec<CommentTag>,
143}
144
145impl ParseResult {
146    pub fn new() -> Self {
147        Self {
148            nodes: Vec::new(),
149            edges: Vec::new(),
150            comment_tags: Vec::new(),
151        }
152    }
153}
154
155impl Default for ParseResult {
156    fn default() -> Self {
157        Self::new()
158    }
159}
160
161pub trait LanguageParser: Send + Sync {
162    fn extensions(&self) -> &[&str];
163    fn extract(&self, file: &SourceFile) -> anyhow::Result<ParseResult>;
164}
165
166pub struct ParserRegistry {
167    parsers: HashMap<Language, Box<dyn LanguageParser>>,
168}
169
170impl ParserRegistry {
171    pub fn new() -> Self {
172        let mut parsers: HashMap<Language, Box<dyn LanguageParser>> = HashMap::new();
173
174        parsers.insert(
175            Language::TypeScript,
176            Box::new(super::parsers::ts::TypeScriptParser::new()),
177        );
178        parsers.insert(
179            Language::JavaScript,
180            Box::new(super::parsers::ts::TypeScriptParser::new()),
181        );
182        parsers.insert(
183            Language::Python,
184            Box::new(super::parsers::py::PythonParser::new()),
185        );
186        parsers.insert(
187            Language::Rust,
188            Box::new(super::parsers::rust::RustParser::new()),
189        );
190        parsers.insert(Language::Go, Box::new(super::parsers::go::GoParser::new()));
191        parsers.insert(
192            Language::Java,
193            Box::new(super::parsers::java::JavaParser::new()),
194        );
195        parsers.insert(
196            Language::CSharp,
197            Box::new(super::parsers::java::JavaParser::new()),
198        );
199        parsers.insert(
200            Language::Php,
201            Box::new(super::parsers::php::PhpParser::new()),
202        );
203
204        Self { parsers }
205    }
206
207    pub fn parse(&self, file: &SourceFile) -> anyhow::Result<ParseResult> {
208        if let Some(parser) = self.parsers.get(&file.language) {
209            parser.extract(file)
210        } else {
211            Ok(ParseResult::new())
212        }
213    }
214
215    pub fn parse_all(&self, files: &[SourceFile]) -> Vec<ParseResult> {
216        use rayon::prelude::*;
217        files
218            .par_iter()
219            .map(|file| {
220                self.parse(file).unwrap_or_else(|e| {
221                    tracing::warn!("Parse error in {}: {}", file.relative_path, e);
222                    ParseResult::new()
223                })
224            })
225            .collect()
226    }
227}
228
229impl Default for ParserRegistry {
230    fn default() -> Self {
231        Self::new()
232    }
233}