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