Skip to main content

code_baseline/rules/ast/
mod.rs

1pub mod max_component_size;
2pub mod no_cascading_set_state;
3pub mod no_nested_components;
4pub mod prefer_use_reducer;
5pub mod require_img_alt;
6
7pub use max_component_size::MaxComponentSizeRule;
8pub use no_cascading_set_state::NoCascadingSetStateRule;
9pub use no_nested_components::NoNestedComponentsRule;
10pub use prefer_use_reducer::PreferUseReducerRule;
11pub use require_img_alt::RequireImgAltRule;
12
13use std::path::Path;
14
15/// Supported languages for AST parsing.
16#[derive(Debug, Clone, Copy)]
17pub enum Lang {
18    Tsx,
19    Typescript,
20    Jsx,
21    Javascript,
22}
23
24/// Detect language from file extension.
25pub fn detect_language(path: &Path) -> Option<Lang> {
26    match path.extension()?.to_str()? {
27        "tsx" => Some(Lang::Tsx),
28        "ts" => Some(Lang::Typescript),
29        "jsx" => Some(Lang::Jsx),
30        "js" => Some(Lang::Javascript),
31        _ => None,
32    }
33}
34
35/// Parse a file into a tree-sitter syntax tree.
36pub fn parse_file(path: &Path, content: &str) -> Option<tree_sitter::Tree> {
37    let lang = detect_language(path)?;
38    let mut parser = tree_sitter::Parser::new();
39    let ts_lang: tree_sitter::Language = match lang {
40        Lang::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
41        Lang::Typescript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
42        Lang::Jsx | Lang::Javascript => tree_sitter_javascript::LANGUAGE.into(),
43    };
44    parser.set_language(&ts_lang).ok()?;
45    parser.parse(content, None)
46}
47
48/// Check if a tree-sitter node represents a React component declaration.
49///
50/// Recognizes PascalCase function declarations, arrow functions assigned to
51/// PascalCase variables, and PascalCase class declarations.
52pub fn is_component_node(node: &tree_sitter::Node, source: &[u8]) -> bool {
53    match node.kind() {
54        "function_declaration" => node
55            .child_by_field_name("name")
56            .and_then(|n| n.utf8_text(source).ok())
57            .map_or(false, starts_with_uppercase),
58        "arrow_function" => node
59            .parent()
60            .filter(|p| p.kind() == "variable_declarator")
61            .and_then(|p| p.child_by_field_name("name"))
62            .and_then(|n| n.utf8_text(source).ok())
63            .map_or(false, starts_with_uppercase),
64        "class_declaration" => node
65            .child_by_field_name("name")
66            .and_then(|n| n.utf8_text(source).ok())
67            .map_or(false, starts_with_uppercase),
68        _ => false,
69    }
70}
71
72fn starts_with_uppercase(name: &str) -> bool {
73    name.chars()
74        .next()
75        .map_or(false, |c| c.is_ascii_uppercase())
76}
77
78/// Count calls to a specific function within a node's subtree,
79/// skipping nested component definitions.
80pub fn count_calls_in_scope(
81    node: tree_sitter::Node,
82    source: &[u8],
83    target_name: &str,
84) -> usize {
85    let mut count = 0;
86    for i in 0..node.child_count() {
87        if let Some(child) = node.child(i) {
88            if is_component_node(&child, source) {
89                continue;
90            }
91            if child.kind() == "call_expression" && is_call_to(&child, source, target_name) {
92                count += 1;
93            }
94            count += count_calls_in_scope(child, source, target_name);
95        }
96    }
97    count
98}
99
100/// Check if a call_expression node calls a function with the given name.
101fn is_call_to(node: &tree_sitter::Node, source: &[u8], name: &str) -> bool {
102    node.child_by_field_name("function")
103        .filter(|f| f.kind() == "identifier")
104        .and_then(|f| f.utf8_text(source).ok())
105        .map_or(false, |n| n == name)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use std::path::Path;
112
113    #[test]
114    fn detect_tsx() {
115        assert!(matches!(
116            detect_language(Path::new("foo.tsx")),
117            Some(Lang::Tsx)
118        ));
119    }
120
121    #[test]
122    fn detect_ts() {
123        assert!(matches!(
124            detect_language(Path::new("bar.ts")),
125            Some(Lang::Typescript)
126        ));
127    }
128
129    #[test]
130    fn detect_jsx() {
131        assert!(matches!(
132            detect_language(Path::new("baz.jsx")),
133            Some(Lang::Jsx)
134        ));
135    }
136
137    #[test]
138    fn detect_js() {
139        assert!(matches!(
140            detect_language(Path::new("qux.js")),
141            Some(Lang::Javascript)
142        ));
143    }
144
145    #[test]
146    fn detect_unknown() {
147        assert!(detect_language(Path::new("file.rs")).is_none());
148    }
149
150    #[test]
151    fn parse_tsx_file() {
152        let content = "function App() { return <div />; }";
153        let tree = parse_file(Path::new("app.tsx"), content);
154        assert!(tree.is_some());
155    }
156
157    #[test]
158    fn parse_unknown_ext_returns_none() {
159        let tree = parse_file(Path::new("app.rs"), "fn main() {}");
160        assert!(tree.is_none());
161    }
162
163    #[test]
164    fn component_function_declaration() {
165        let content = "function MyComponent() { return <div />; }";
166        let tree = parse_file(Path::new("a.tsx"), content).unwrap();
167        let root = tree.root_node();
168        let func = root.child(0).unwrap();
169        assert!(is_component_node(&func, content.as_bytes()));
170    }
171
172    #[test]
173    fn non_component_lowercase() {
174        let content = "function helper() { return 1; }";
175        let tree = parse_file(Path::new("a.tsx"), content).unwrap();
176        let root = tree.root_node();
177        let func = root.child(0).unwrap();
178        assert!(!is_component_node(&func, content.as_bytes()));
179    }
180
181    #[test]
182    fn component_arrow_function() {
183        let content = "const MyComponent = () => { return <div />; };";
184        let tree = parse_file(Path::new("a.tsx"), content).unwrap();
185        let source = content.as_bytes();
186        let root = tree.root_node();
187        // Walk to find the arrow_function
188        let mut found = false;
189        visit_all(root, &mut |node| {
190            if node.kind() == "arrow_function" && is_component_node(&node, source) {
191                found = true;
192            }
193        });
194        assert!(found);
195    }
196
197    fn visit_all<F: FnMut(tree_sitter::Node)>(node: tree_sitter::Node, f: &mut F) {
198        f(node);
199        for i in 0..node.child_count() {
200            if let Some(child) = node.child(i) {
201                visit_all(child, f);
202            }
203        }
204    }
205}