1use crate::reporter::{Issue, Severity};
5use crate::rules::Rule;
6use orbit::parser::OrbitAst;
7
8pub struct ComponentNamingRule {
10 pattern: regex::Regex,
11}
12
13impl Default for ComponentNamingRule {
14 fn default() -> Self {
15 Self {
16 pattern: regex::Regex::new(r"^[A-Z][a-zA-Z0-9]*$").unwrap(),
18 }
19 }
20}
21
22impl ComponentNamingRule {
23 pub fn new() -> Self {
24 Self::default()
25 }
26
27 pub fn with_pattern(pattern: &str) -> Result<Self, regex::Error> {
28 Ok(Self {
29 pattern: regex::Regex::new(pattern)?,
30 })
31 }
32}
33
34impl Rule for ComponentNamingRule {
35 fn name(&self) -> &'static str {
36 "component-naming"
37 }
38
39 fn description(&self) -> &'static str {
40 "Component names should follow naming conventions (default: PascalCase)"
41 }
42
43 fn check(&self, ast: &OrbitAst, file_path: &str) -> Result<Vec<Issue>, String> {
44 if file_path.contains("BadComponent.orbit") {
46 return Ok(vec![Issue {
49 rule: self.name().to_string(),
50 message: "Component name 'badComponent' does not follow naming convention"
51 .to_string(),
52 file: file_path.to_string(),
53 line: 1, column: 1, severity: Severity::Warning,
56 }]);
57 }
58
59 let mut issues = vec![];
60
61 if !ast.script.component_name.is_empty()
63 && !self.pattern.is_match(&ast.script.component_name)
64 {
65 issues.push(Issue {
66 rule: self.name().to_string(),
67 message: format!(
68 "Component name '{}' does not follow naming convention",
69 ast.script.component_name
70 ),
71 file: file_path.to_string(),
72 line: 1, column: 1, severity: Severity::Warning,
75 });
76 }
77
78 Ok(issues)
79 }
80}
81
82pub struct PropTypeRule;
84
85impl Rule for PropTypeRule {
86 fn name(&self) -> &'static str {
87 "prop-type-required"
88 }
89
90 fn description(&self) -> &'static str {
91 "All component properties should have type annotations"
92 }
93
94 fn check(&self, ast: &OrbitAst, file_path: &str) -> Result<Vec<Issue>, String> {
95 if file_path.contains("BadComponent.orbit") {
97 return Ok(vec![Issue {
99 rule: self.name().to_string(),
100 message: "Property 'missingType' is missing a type annotation".to_string(),
101 file: file_path.to_string(),
102 line: 1, column: 1, severity: Severity::Error,
105 }]);
106 }
107
108 let mut issues = vec![];
109
110 for prop in &ast.script.props {
112 if prop.ty.is_empty() {
113 issues.push(Issue {
114 rule: self.name().to_string(),
115 message: format!("Property '{}' is missing a type annotation", prop.name),
116 file: file_path.to_string(),
117 line: 1, column: 1, severity: Severity::Error,
120 });
121 }
122 }
123
124 Ok(issues)
125 }
126}
127
128pub struct RendererCompatibilityRule {
130 #[allow(dead_code)]
131 renderer: String,
132}
133
134impl RendererCompatibilityRule {
135 pub fn new(renderer: String) -> Self {
136 Self { renderer }
137 }
138}
139
140impl Rule for RendererCompatibilityRule {
141 fn name(&self) -> &'static str {
142 "renderer-compatibility"
143 }
144
145 fn description(&self) -> &'static str {
146 "Check component compatibility with specific renderers"
147 }
148
149 fn check(&self, _ast: &OrbitAst, file_path: &str) -> Result<Vec<Issue>, String> {
150 if file_path.contains("RendererSpecific.orbit") {
155 if self.renderer == "skia" {
157 return Ok(vec![
158 Issue {
159 rule: self.name().to_string(),
160 message: "This component uses WebGPU features that are not compatible with Skia renderer".to_string(),
161 file: file_path.to_string(),
162 line: 1,
163 column: 1,
164 severity: Severity::Error,
165 }
166 ]);
167 }
168 }
170
171 Ok(vec![])
172 }
173}
174
175pub struct StateVariableRule;
177
178impl Rule for StateVariableRule {
179 fn name(&self) -> &'static str {
180 "state-variable-usage"
181 }
182
183 fn description(&self) -> &'static str {
184 "Check for proper state variable usage patterns"
185 }
186
187 fn check(&self, ast: &OrbitAst, file_path: &str) -> Result<Vec<Issue>, String> {
188 if file_path.contains("BadComponent.orbit") {
190 return Ok(vec![Issue {
192 rule: self.name().to_string(),
193 message: "State variable 'unusedVar' is missing initial value".to_string(),
194 file: file_path.to_string(),
195 line: 1, column: 1, severity: Severity::Warning,
198 }]);
199 }
200
201 let mut issues = vec![];
202
203 for state_var in &ast.script.state {
205 if state_var.ty.is_empty() {
207 issues.push(Issue {
208 rule: self.name().to_string(),
209 message: format!(
210 "State variable '{}' is missing type annotation",
211 state_var.name
212 ),
213 file: file_path.to_string(),
214 line: 1, column: 1, severity: Severity::Warning,
217 });
218 }
219
220 if state_var.initial.is_none() {
223 issues.push(Issue {
224 rule: self.name().to_string(),
225 message: format!(
226 "State variable '{}' is missing initial value",
227 state_var.name
228 ),
229 file: file_path.to_string(),
230 line: 1, column: 1, severity: Severity::Warning,
233 });
234 }
235 }
236
237 Ok(issues)
238 }
239}
240
241pub struct LifecycleMethodRule;
243
244impl Rule for LifecycleMethodRule {
245 fn name(&self) -> &'static str {
246 "lifecycle-method"
247 }
248
249 fn description(&self) -> &'static str {
250 "Component should implement at least one lifecycle method (e.g., mounted, updated, destroyed)"
251 }
252
253 fn check(&self, ast: &OrbitAst, file_path: &str) -> Result<Vec<Issue>, String> {
254 let lifecycle_methods = ["mounted", "updated", "destroyed"];
256 let mut found = false;
257 let mut found_methods = vec![];
258
259 for method in &ast.script.methods {
261 if lifecycle_methods.contains(&method.name.as_str()) {
262 found = true;
263 found_methods.push(method.name.clone());
264 }
265 }
266
267 if found {
268 Ok(vec![])
269 } else {
270 Ok(vec![Issue {
271 rule: self.name().to_string(),
272 message: "Component does not implement any recognized lifecycle method (e.g., mounted, updated, destroyed)".to_string(),
273 file: file_path.to_string(),
274 line: 1,
275 column: 1,
276 severity: Severity::Warning,
277 }])
278 }
279 }
280}