Skip to main content

codesynapse_core/ts_extract/
razor.rs

1use super::make_file_node;
2use crate::error::Result;
3use crate::extract::{make_id, ImportNode, LanguageExtractor};
4use crate::types::{Edge, ExtractionFragment, Node};
5use std::collections::HashMap;
6use std::path::Path;
7
8pub struct RazorExtractor;
9
10impl RazorExtractor {
11    pub fn extract(source: &[u8], path: &Path) -> Result<ExtractionFragment> {
12        let (file_id, _, file_node) = make_file_node(path);
13        let mut fragment = ExtractionFragment {
14            nodes: vec![file_node],
15            edges: vec![],
16        };
17
18        let content = std::str::from_utf8(source).unwrap_or("");
19
20        // @using directives → import edges
21        let using_re = regex::Regex::new(r"(?m)^@using\s+([\w.]+)");
22        if let Ok(r) = using_re {
23            for cap in r.captures_iter(content) {
24                let ns = cap.get(1).map(|m| m.as_str()).unwrap_or("");
25                if !ns.is_empty() {
26                    let id = make_id(&[ns]);
27                    fragment.nodes.push(Node {
28                        id: id.clone(),
29                        label: ns.to_string(),
30                        file_type: "code".to_string(),
31                        source_file: path.to_string_lossy().to_string(),
32                        source_location: None,
33                        community: None,
34                        rationale: None,
35                        docstring: None,
36                        metadata: HashMap::new(),
37                    });
38                    fragment.edges.push(Edge {
39                        source: file_id.clone(),
40                        target: id,
41                        relation: "imports".to_string(),
42                        confidence: "EXTRACTED".to_string(),
43                        source_file: Some(path.to_string_lossy().to_string()),
44                        weight: 1.0,
45                        context: Some("import".to_string()),
46                    });
47                }
48            }
49        }
50
51        // @inject directives
52        let inject_re = regex::Regex::new(r"(?m)^@inject\s+(\w+)\s+\w+");
53        if let Ok(r) = inject_re {
54            for cap in r.captures_iter(content) {
55                let service = cap.get(1).map(|m| m.as_str()).unwrap_or("");
56                if !service.is_empty() {
57                    let id = make_id(&[service]);
58                    if !fragment.nodes.iter().any(|n| n.id == id) {
59                        fragment.nodes.push(Node {
60                            id: id.clone(),
61                            label: service.to_string(),
62                            file_type: "code".to_string(),
63                            source_file: path.to_string_lossy().to_string(),
64                            source_location: None,
65                            community: None,
66                            rationale: None,
67                            docstring: None,
68                            metadata: HashMap::new(),
69                        });
70                    }
71                    fragment.edges.push(Edge {
72                        source: file_id.clone(),
73                        target: id,
74                        relation: "imports".to_string(),
75                        confidence: "EXTRACTED".to_string(),
76                        source_file: Some(path.to_string_lossy().to_string()),
77                        weight: 1.0,
78                        context: Some("import".to_string()),
79                    });
80                }
81            }
82        }
83
84        // @inherits → inherits edge
85        let inherits_re = regex::Regex::new(r"(?m)^@inherits\s+(\w+)");
86        if let Ok(r) = inherits_re {
87            for cap in r.captures_iter(content) {
88                let base = cap.get(1).map(|m| m.as_str()).unwrap_or("");
89                if !base.is_empty() {
90                    let base_id = make_id(&[base]);
91                    if !fragment.nodes.iter().any(|n| n.id == base_id) {
92                        fragment.nodes.push(Node {
93                            id: base_id.clone(),
94                            label: base.to_string(),
95                            file_type: "code".to_string(),
96                            source_file: path.to_string_lossy().to_string(),
97                            source_location: None,
98                            community: None,
99                            rationale: None,
100                            docstring: None,
101                            metadata: HashMap::new(),
102                        });
103                    }
104                    fragment.edges.push(Edge {
105                        source: file_id.clone(),
106                        target: base_id,
107                        relation: "inherits".to_string(),
108                        confidence: "EXTRACTED".to_string(),
109                        source_file: Some(path.to_string_lossy().to_string()),
110                        weight: 1.0,
111                        context: None,
112                    });
113                }
114            }
115        }
116
117        // Component tags: <ComponentName ...> where name starts with uppercase
118        let component_re = regex::Regex::new(r"<([A-Z][A-Za-z]*)\b[^/]*(?:/>|>)");
119        if let Ok(r) = component_re {
120            for cap in r.captures_iter(content) {
121                let comp = cap.get(1).map(|m| m.as_str()).unwrap_or("");
122                // Skip standard HTML-like tags
123                if comp.is_empty() {
124                    continue;
125                }
126                let comp_id = make_id(&[comp]);
127                if !fragment.nodes.iter().any(|n| n.id == comp_id) {
128                    fragment.nodes.push(Node {
129                        id: comp_id.clone(),
130                        label: comp.to_string(),
131                        file_type: "code".to_string(),
132                        source_file: path.to_string_lossy().to_string(),
133                        source_location: None,
134                        community: None,
135                        rationale: None,
136                        docstring: None,
137                        metadata: HashMap::new(),
138                    });
139                }
140                fragment.edges.push(Edge {
141                    source: file_id.clone(),
142                    target: comp_id,
143                    relation: "calls".to_string(),
144                    confidence: "EXTRACTED".to_string(),
145                    source_file: Some(path.to_string_lossy().to_string()),
146                    weight: 1.0,
147                    context: None,
148                });
149            }
150        }
151
152        // @code { ... } block: extract methods
153        // Simple regex for method signatures: (private|public|protected|async)? (void|Task|...) MethodName(
154        if let Some(code_start) = content.find("@code") {
155            let code_block = &content[code_start..];
156            let method_re = regex::Regex::new(
157                r"(?m)(?:(?:private|public|protected|override|async|virtual|static)\s+)*(?:\w+(?:<[^>]*>)?(?:\?)?)\s+(\w+)\s*\(",
158            );
159            if let Ok(r) = method_re {
160                for cap in r.captures_iter(code_block) {
161                    let method_name = cap.get(1).map(|m| m.as_str()).unwrap_or("");
162                    if method_name.is_empty() || is_csharp_keyword(method_name) {
163                        continue;
164                    }
165                    let id = make_id(&[&file_id, method_name, "()"]);
166                    if !fragment.nodes.iter().any(|n| n.id == id) {
167                        fragment.nodes.push(Node {
168                            id: id.clone(),
169                            label: format!("{}()", method_name),
170                            file_type: "code".to_string(),
171                            source_file: path.to_string_lossy().to_string(),
172                            source_location: None,
173                            community: None,
174                            rationale: None,
175                            docstring: None,
176                            metadata: HashMap::new(),
177                        });
178                        fragment.edges.push(Edge {
179                            source: file_id.clone(),
180                            target: id,
181                            relation: "contains".to_string(),
182                            confidence: "EXTRACTED".to_string(),
183                            source_file: Some(path.to_string_lossy().to_string()),
184                            weight: 1.0,
185                            context: None,
186                        });
187                    }
188                }
189            }
190        }
191
192        Ok(fragment)
193    }
194}
195
196fn is_csharp_keyword(s: &str) -> bool {
197    matches!(
198        s,
199        "if" | "else"
200            | "for"
201            | "foreach"
202            | "while"
203            | "do"
204            | "switch"
205            | "case"
206            | "return"
207            | "new"
208            | "var"
209            | "class"
210            | "namespace"
211            | "using"
212            | "await"
213            | "async"
214            | "base"
215            | "this"
216            | "null"
217            | "true"
218            | "false"
219            | "int"
220            | "string"
221            | "bool"
222            | "void"
223            | "List"
224            | "Task"
225            | "override"
226            | "protected"
227            | "private"
228            | "public"
229            | "static"
230            | "virtual"
231            | "abstract"
232    )
233}
234
235impl LanguageExtractor for RazorExtractor {
236    fn file_extensions(&self) -> Vec<&'static str> {
237        vec!["razor", "cshtml"]
238    }
239    fn extract(&self, source: &[u8], path: &Path) -> Result<ExtractionFragment> {
240        Self::extract(source, path)
241    }
242    fn resolve_imports(&self, _imports: &[ImportNode]) -> Vec<Edge> {
243        vec![]
244    }
245    fn collect_type_refs(&self, _fragment: &mut ExtractionFragment) {}
246}