code_baseline/rules/ast/
no_nested_components.rs1use crate::config::{RuleConfig, Severity};
2use crate::rules::ast::{is_component_node, parse_file};
3use crate::rules::{Rule, RuleBuildError, ScanContext, Violation};
4
5pub struct NoNestedComponentsRule {
11 id: String,
12 severity: Severity,
13 message: String,
14 suggest: Option<String>,
15 glob: Option<String>,
16}
17
18impl NoNestedComponentsRule {
19 pub fn new(config: &RuleConfig) -> Result<Self, RuleBuildError> {
20 Ok(Self {
21 id: config.id.clone(),
22 severity: config.severity,
23 message: config.message.clone(),
24 suggest: config.suggest.clone(),
25 glob: config.glob.clone(),
26 })
27 }
28}
29
30impl Rule for NoNestedComponentsRule {
31 fn id(&self) -> &str {
32 &self.id
33 }
34
35 fn severity(&self) -> Severity {
36 self.severity
37 }
38
39 fn file_glob(&self) -> Option<&str> {
40 self.glob.as_deref()
41 }
42
43 fn check_file(&self, ctx: &ScanContext) -> Vec<Violation> {
44 let mut violations = Vec::new();
45 let tree = match parse_file(ctx.file_path, ctx.content) {
46 Some(t) => t,
47 None => return violations,
48 };
49 let source = ctx.content.as_bytes();
50 self.visit(tree.root_node(), source, ctx, &mut violations);
51 violations
52 }
53}
54
55impl NoNestedComponentsRule {
56 fn visit(
57 &self,
58 node: tree_sitter::Node,
59 source: &[u8],
60 ctx: &ScanContext,
61 violations: &mut Vec<Violation>,
62 ) {
63 if is_component_node(&node, source) && has_component_ancestor(&node, source) {
64 let line = node.start_position().row;
65 violations.push(Violation {
66 rule_id: self.id.clone(),
67 severity: self.severity,
68 file: ctx.file_path.to_path_buf(),
69 line: Some(line + 1),
70 column: Some(node.start_position().column + 1),
71 message: self.message.clone(),
72 suggest: self.suggest.clone(),
73 source_line: ctx.content.lines().nth(line).map(String::from),
74 fix: None,
75 });
76 }
77
78 for i in 0..node.child_count() {
79 if let Some(child) = node.child(i) {
80 self.visit(child, source, ctx, violations);
81 }
82 }
83 }
84}
85
86fn has_component_ancestor(node: &tree_sitter::Node, source: &[u8]) -> bool {
88 let mut current = node.parent();
89 while let Some(parent) = current {
90 if is_component_node(&parent, source) {
91 return true;
92 }
93 current = parent.parent();
94 }
95 false
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use std::path::Path;
102
103 fn make_config() -> RuleConfig {
104 RuleConfig {
105 id: "no-nested-components".into(),
106 severity: Severity::Error,
107 message: "Nested component definition".into(),
108 suggest: Some("Move the component to the module level".into()),
109 glob: Some("**/*.tsx".into()),
110 ..Default::default()
111 }
112 }
113
114 fn check(rule: &NoNestedComponentsRule, content: &str) -> Vec<Violation> {
115 let ctx = ScanContext {
116 file_path: Path::new("test.tsx"),
117 content,
118 };
119 rule.check_file(&ctx)
120 }
121
122 #[test]
123 fn no_nesting_no_violation() {
124 let rule = NoNestedComponentsRule::new(&make_config()).unwrap();
125 let content = "\
126function Outer() {
127 return <div>hello</div>;
128}
129
130function Other() {
131 return <span>world</span>;
132}";
133 let violations = check(&rule, content);
134 assert!(violations.is_empty());
135 }
136
137 #[test]
138 fn nested_function_declaration() {
139 let rule = NoNestedComponentsRule::new(&make_config()).unwrap();
140 let content = "\
141function Outer() {
142 function Inner() {
143 return <span>nested</span>;
144 }
145 return <div><Inner /></div>;
146}";
147 let violations = check(&rule, content);
148 assert_eq!(violations.len(), 1);
149 assert_eq!(violations[0].line, Some(2));
150 }
151
152 #[test]
153 fn nested_arrow_component() {
154 let rule = NoNestedComponentsRule::new(&make_config()).unwrap();
155 let content = "\
156const Outer = () => {
157 const Inner = () => {
158 return <div>nested</div>;
159 };
160 return <Inner />;
161};";
162 let violations = check(&rule, content);
163 assert_eq!(violations.len(), 1);
164 }
165
166 #[test]
167 fn nested_inside_class_component() {
168 let rule = NoNestedComponentsRule::new(&make_config()).unwrap();
169 let content = "\
170class Outer extends React.Component {
171 render() {
172 function Inner() {
173 return <span />;
174 }
175 return <Inner />;
176 }
177}";
178 let violations = check(&rule, content);
179 assert_eq!(violations.len(), 1);
180 }
181
182 #[test]
183 fn lowercase_nested_function_ignored() {
184 let rule = NoNestedComponentsRule::new(&make_config()).unwrap();
185 let content = "\
186function Outer() {
187 function helper() {
188 return 42;
189 }
190 return <div>{helper()}</div>;
191}";
192 let violations = check(&rule, content);
193 assert!(violations.is_empty());
194 }
195
196 #[test]
197 fn deeply_nested_components() {
198 let rule = NoNestedComponentsRule::new(&make_config()).unwrap();
199 let content = "\
200function A() {
201 function B() {
202 function C() {
203 return <div />;
204 }
205 return <C />;
206 }
207 return <B />;
208}";
209 let violations = check(&rule, content);
210 assert_eq!(violations.len(), 2);
212 }
213
214 #[test]
215 fn non_tsx_file_skipped() {
216 let rule = NoNestedComponentsRule::new(&make_config()).unwrap();
217 let ctx = ScanContext {
218 file_path: Path::new("test.rs"),
219 content: "fn main() {}",
220 };
221 assert!(rule.check_file(&ctx).is_empty());
222 }
223}