oparry_parser/
javascript_complete.rs1use oparry_core::Result;
4use oxc_allocator::Allocator;
5use oxc_ast::ast::*;
6use oxc_parser::Parser;
7use oxc_span::SourceType;
8
9#[derive(Debug, Clone)]
11pub enum ImportSpecifierInfo {
12 Default { local_name: String },
14 Named { imported_name: String, local_name: String },
16 Namespace { local_name: String },
18}
19
20#[derive(Debug, Clone)]
22pub struct ImportInfo {
23 pub source: String,
25 pub specifiers: Vec<ImportSpecifierInfo>,
27 pub is_type_only: bool,
29}
30
31#[derive(Debug, Clone)]
33pub enum ExportInfo {
34 Named { names: Vec<String> },
36 Default,
38 AllFrom { source: String },
40 Declaration { kind: String, name: String },
42}
43
44#[derive(Debug, Clone)]
46pub struct ComponentInfo {
47 pub name: String,
49 pub line: usize,
51 pub has_children: bool,
53}
54
55#[derive(Debug, Clone)]
57pub struct HookInfo {
58 pub name: String,
60 pub line: usize,
62}
63
64#[derive(Debug, Clone, Default)]
66pub struct ParseResults {
67 pub imports: Vec<ImportInfo>,
69 pub exports: Vec<ExportInfo>,
71 pub components: Vec<ComponentInfo>,
73 pub hooks: Vec<HookInfo>,
75}
76
77pub struct JavaScriptAst {
79 source: String,
80 pub results: ParseResults,
81}
82
83impl JavaScriptAst {
84 pub fn new(source: String, results: ParseResults) -> Self {
86 Self { source, results }
87 }
88
89 pub fn source(&self) -> &str {
91 &self.source
92 }
93
94 pub fn results(&self) -> &ParseResults {
96 &self.results
97 }
98
99 pub fn extract_imports(&self) -> Vec<ImportInfo> {
101 self.results.imports.clone()
102 }
103
104 pub fn extract_exports(&self) -> Vec<ExportInfo> {
106 self.results.exports.clone()
107 }
108
109 pub fn extract_jsx_components(&self) -> Vec<ComponentInfo> {
111 self.results.components.clone()
112 }
113
114 pub fn extract_hooks(&self) -> Vec<HookInfo> {
116 self.results.hooks.clone()
117 }
118}
119
120pub fn is_component_name(name: &str) -> bool {
122 name.starts_with(char::is_uppercase)
123}
124
125pub struct JavaScriptParser;
127
128impl JavaScriptParser {
129 pub fn new() -> Self {
130 Self
131 }
132
133 pub fn parse(&self, source: &str, source_type: SourceType) -> Result<JavaScriptAst> {
135 let allocator = Allocator::default();
136
137 let parser = Parser::new(&allocator, source, source_type);
139 let result = parser.parse();
140
141 if !result.errors.is_empty() {
142 let e = &result.errors[0];
143 return Err(oparry_core::Error::Parse {
144 file: "<unknown>".to_string(),
145 line: 0,
146 column: 0,
147 message: e.message.to_string(),
148 });
149 }
150
151 let results = Self::extract_info(source);
153 let source_str = source.to_string();
154
155 Ok(JavaScriptAst::new(source_str, results))
156 }
157
158 fn extract_info(source: &str) -> ParseResults {
160 let mut results = ParseResults::default();
161
162 let import_re = regex::Regex::new(
164 r#"import\s+(?:(?P<default>\w+)|\{(?P<named>[^}]+)\}|\*\s+as\s+(?P<namespace>\w+))\s+from\s+['"](?P<source>[^'"]+)['"]"#
165 ).unwrap();
166
167 for caps in import_re.captures_iter(source) {
168 let source = caps.name("source")
169 .map(|m| m.as_str().to_string())
170 .unwrap_or_default();
171 let is_type_only = source.contains("type");
172
173 let specifiers = if caps.name("default").is_some() {
174 vec![ImportSpecifierInfo::Default {
175 local_name: caps.name("default").unwrap().as_str().to_string(),
176 }]
177 } else if caps.name("namespace").is_some() {
178 vec![ImportSpecifierInfo::Namespace {
179 local_name: caps.name("namespace").unwrap().as_str().to_string(),
180 }]
181 } else if let Some(named) = caps.name("named") {
182 named.as_str().split(',')
183 .map(|s| ImportSpecifierInfo::Named {
184 imported_name: s.trim().to_string(),
185 local_name: s.trim().to_string(),
186 })
187 .collect()
188 } else {
189 vec![]
190 };
191
192 results.imports.push(ImportInfo {
193 source,
194 specifiers,
195 is_type_only,
196 });
197 }
198
199 let jsx_re = regex::Regex::new(r#"<([A-Z][a-zA-Z0-9]*)"#).unwrap();
201 let mut component_names = std::collections::HashSet::new();
202 for caps in jsx_re.captures_iter(source) {
203 if let Some(name) = caps.get(1) {
204 component_names.insert(name.as_str().to_string());
205 }
206 }
207
208 results.components = component_names.into_iter()
209 .enumerate()
210 .map(|(_i, name)| ComponentInfo {
211 name,
212 line: 0,
213 has_children: false,
214 })
215 .collect();
216
217 let hook_re = regex::Regex::new(
219 r#"\b(useState|useEffect|useCallback|useMemo|useRef|useContext|useReducer|useTransition|useId|useSyncExternalStore|useLayoutEffect|useImperativeHandle|useDeferredValue|useDebugValue|useInsertionEffect)\b"#
220 ).unwrap();
221 let mut hook_names = std::collections::HashSet::new();
222 for caps in hook_re.captures_iter(source) {
223 if let Some(name) = caps.get(1) {
224 hook_names.insert(name.as_str().to_string());
225 }
226 }
227
228 results.hooks = hook_names.into_iter()
229 .map(|name| HookInfo {
230 name,
231 line: 0,
232 })
233 .collect();
234
235 if source.contains("export default") {
237 results.exports.push(ExportInfo::Default);
238 }
239
240 let export_all_re = regex::Regex::new(r#"export\s+\*\s+from\s+['"]([^'"]+)['"]"#).unwrap();
242 for caps in export_all_re.captures_iter(source) {
243 if let Some(from) = caps.get(1) {
244 results.exports.push(ExportInfo::AllFrom {
245 source: from.as_str().to_string(),
246 });
247 }
248 }
249
250 let export_decl_re = regex::Regex::new(r#"export\s+(?:const|function|class)\s+(\w+)"#).unwrap();
252 for caps in export_decl_re.captures_iter(source) {
253 if let Some(name) = caps.get(1) {
254 results.exports.push(ExportInfo::Declaration {
255 kind: "declaration".to_string(),
256 name: name.as_str().to_string(),
257 });
258 }
259 }
260
261 results
262 }
263}
264
265impl Default for JavaScriptParser {
266 fn default() -> Self {
267 Self::new()
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_parse_simple_js() {
277 let parser = JavaScriptParser::new();
278 let source = r#"
279 function hello() {
280 return "world";
281 }
282 "#;
283
284 let ast = parser.parse(source, SourceType::default()).unwrap();
285 assert_eq!(ast.source(), source);
286 }
287
288 #[test]
289 fn test_parse_invalid_js() {
290 let parser = JavaScriptParser::new();
291 let source = "function { invalid";
292
293 let result = parser.parse(source, SourceType::default());
294 assert!(result.is_err());
295 }
296
297 #[test]
298 fn test_extract_imports() {
299 let parser = JavaScriptParser::new();
300 let source = r#"
301 import React from 'react';
302 import { useState, useEffect } from 'react';
303 import * as utils from './utils';
304 "#;
305
306 let ast = parser.parse(source, SourceType::default()).unwrap();
307 let imports = ast.extract_imports();
308 assert_eq!(imports.len(), 3);
309 }
310
311 #[test]
312 fn test_extract_components() {
313 let parser = JavaScriptParser::new();
314 let source = r#"
315 function App() {
316 return <div><Header /><Main /></div>;
317 }
318 "#;
319
320 let ast = parser.parse(source, SourceType::tsx()).unwrap();
321 let components = ast.extract_jsx_components();
322 assert!(!components.is_empty());
323 assert!(components.iter().any(|c| c.name == "Header"));
324 }
325
326 #[test]
327 fn test_extract_hooks() {
328 let parser = JavaScriptParser::new();
329 let source = r#"
330 function Component() {
331 const [state, setState] = useState(0);
332 useEffect(() => {}, []);
333 const memoized = useMemo(() => compute(), []);
334 return <div />;
335 }
336 "#;
337
338 let ast = parser.parse(source, SourceType::tsx()).unwrap();
339 let hooks = ast.extract_hooks();
340 assert!(!hooks.is_empty());
341 assert!(hooks.iter().any(|h| h.name == "useState"));
342 assert!(hooks.iter().any(|h| h.name == "useEffect"));
343 }
344
345 #[test]
346 fn test_is_component_name() {
347 assert!(is_component_name("Button"));
348 assert!(is_component_name("MyComponent"));
349 assert!(!is_component_name("button"));
350 assert!(!is_component_name("div"));
351 }
352
353 #[test]
354 fn test_parse_typescript() {
355 let parser = JavaScriptParser::new();
356 let source = r#"
357 interface User {
358 name: string;
359 age: number;
360 }
361
362 function greet(user: User): string {
363 return `Hello ${user.name}`;
364 }
365 "#;
366
367 let ast = parser.parse(source, SourceType::ts()).unwrap();
368 assert_eq!(ast.source(), source);
369 }
370
371 #[test]
372 fn test_parse_tsx() {
373 let parser = JavaScriptParser::new();
374 let source = r#"
375 interface Props {
376 children: string;
377 }
378
379 export function Button({ children }: Props) {
380 return <button>{children}</button>;
381 }
382 "#;
383
384 let ast = parser.parse(source, SourceType::tsx()).unwrap();
385 assert_eq!(ast.source(), source);
386 }
387
388 #[test]
389 fn test_ast_new() {
390 let source = "test source".to_string();
391 let results = ParseResults::default();
392 let ast = JavaScriptAst::new(source, results);
393
394 assert_eq!(ast.source(), "test source");
395 }
396}