cadi_core/atomizer/languages/
jsx.rs

1//! JSX atomizer (React / web)
2
3use crate::atomizer::{AtomizerConfig, ExtractedAtom, AtomKind};
4use crate::error::CadiResult;
5
6/// JSX atomizer (uses JS extractor semantics)
7pub struct JSXAtomizer {
8    _config: AtomizerConfig,
9}
10
11impl JSXAtomizer {
12    pub fn new(config: AtomizerConfig) -> Self {
13        Self { _config: config }
14    }
15
16    /// Extract atoms from JSX/JS files using Tree-sitter when available
17    #[cfg(feature = "ast-parsing")]
18    pub fn extract(&self, source: &str) -> CadiResult<Vec<ExtractedAtom>> {
19        use tree_sitter::{Parser, Query, QueryCursor};
20
21        let mut parser = Parser::new();
22        parser.set_language(&tree_sitter_javascript::language())?;
23        let tree = parser
24            .parse(source, None)
25            .ok_or_else(|| crate::error::CadiError::AtomizerError("Parse failed".into()))?;
26
27        let query_src = r#"
28            (function_declaration
29                name: (identifier) @fn_name
30            ) @function
31
32            (lexical_declaration
33                (variable_declarator
34                    name: (identifier) @var_name
35                    value: (arrow_function) @arrow_fn
36                )
37            ) @var_fn
38
39            (export_statement (function_declaration name: (identifier) @exported_fn_name)) @exported_function
40
41            (class_declaration
42                name: (identifier) @class_name
43            ) @class
44
45            (jsx_element) @jsx
46        "#;
47
48        let query = Query::new(&tree_sitter_javascript::language(), query_src)?;
49        let mut cursor = QueryCursor::new();
50
51        let mut atoms = Vec::new();
52
53        for m in cursor.matches(&query, tree.root_node(), source.as_bytes()) {
54            let mut name = "unknown".to_string();
55            let mut kind = AtomKind::Function;
56            let mut atom_node = None;
57
58            for capture in m.captures {
59                let capture_name = query.capture_names()[capture.index as usize];
60                match capture_name {
61                    "fn_name" | "var_name" | "exported_fn_name" | "class_name" => {
62                        name = capture.node.utf8_text(source.as_bytes()).unwrap_or("unknown").to_string();
63                    }
64                    "function" | "var_fn" | "exported_function" => {
65                        kind = AtomKind::Function;
66                        atom_node = Some(capture.node);
67                    }
68                    "class" => {
69                        kind = AtomKind::Class;
70                        atom_node = Some(capture.node);
71                    }
72                    "jsx" => {
73                        // If we see a JSX element outside of other captures, record a small fragment
74                        let node = capture.node;
75                        let start = node.start_byte();
76                        let end = node.end_byte();
77
78                        atoms.push(ExtractedAtom {
79                            name: "jsx_fragment".to_string(),
80                            kind: AtomKind::Module,
81                            source: source[start..end].to_string(),
82                            start_byte: start,
83                            end_byte: end,
84                            start_line: node.start_position().row + 1,
85                            end_line: node.end_position().row + 1,
86                            defines: Vec::new(),
87                            references: Vec::new(),
88                            doc_comment: None,
89                            visibility: crate::atomizer::extractor::Visibility::Public,
90                            parent: None,
91                            decorators: Vec::new(),
92                        });
93                    }
94                    _ => {}
95                }
96            }
97
98            if let Some(node) = atom_node {
99                let start = node.start_byte();
100                let end = node.end_byte();
101                let start_point = node.start_position();
102                let end_point = node.end_position();
103
104                atoms.push(ExtractedAtom {
105                    name: name.clone(),
106                    kind,
107                    source: source[start..end].to_string(),
108                    start_byte: start,
109                    end_byte: end,
110                    start_line: start_point.row + 1,
111                    end_line: end_point.row + 1,
112                    defines: vec![name.clone()],
113                    references: Vec::new(),
114                    doc_comment: None,
115                    visibility: crate::atomizer::extractor::Visibility::Public,
116                    parent: None,
117                    decorators: Vec::new(),
118                });
119            }
120        }
121
122        Ok(atoms)
123    }
124
125    /// Fallback extraction without Tree-sitter
126    #[cfg(not(feature = "ast-parsing"))]
127    pub fn extract(&self, source: &str) -> CadiResult<Vec<ExtractedAtom>> {
128        use crate::atomizer::AtomExtractor;
129        // Use javascript extractor for JSX
130        AtomExtractor::new("javascript", self.config.clone()).extract(source)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::atomizer::AtomizerConfig;
138
139    #[test]
140    fn test_jsx_extraction_component() {
141        let source = r#"
142            export function Hello() { return <div>Hello</div> }
143            const X = () => <span>Hi</span>;
144            class C extends React.Component { render() { return <p/> } }
145        "#;
146
147        let atomizer = JSXAtomizer::new(AtomizerConfig::default());
148        let atoms = atomizer.extract(source).unwrap();
149
150        assert!(atoms.iter().any(|a| a.name == "Hello"));
151        assert!(atoms.iter().any(|a| a.name == "X"));
152        assert!(atoms.iter().any(|a| a.name == "C"));
153    }
154}