code_baseline/rules/ast/
mod.rs1pub 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#[derive(Debug, Clone, Copy)]
17pub enum Lang {
18 Tsx,
19 Typescript,
20 Jsx,
21 Javascript,
22}
23
24pub 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
35pub 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
48pub 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
78pub 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
100fn 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 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}