1use crate::config::Config;
4use crate::parser;
5use crate::reporter::Issue;
6use crate::rules::Rule;
7use crate::{AnalyzerError, Result};
8use rayon::prelude::*;
9use std::path::Path;
10
11pub struct Linter {
13 rules: Vec<Box<dyn Rule + Send + Sync>>,
14 config: Config,
15}
16
17impl Linter {
18 pub fn new() -> Self {
20 Self::with_config(Config::default())
21 }
22
23 pub fn with_config(config: Config) -> Self {
25 let mut linter = Self {
26 rules: vec![],
27 config,
28 };
29
30 linter.add_rule(crate::rules::NonEmptyTemplateRule);
32 linter.add_rule(crate::rules::PublicFunctionRule);
33 linter.add_rule(crate::rules::ComponentNamingRule::new());
34 linter.add_rule(crate::rules::PropTypeRule);
35 linter.add_rule(crate::rules::StateVariableRule);
36 linter.add_rule(crate::rules::LifecycleMethodRule); if linter.config.renderer_analysis.enabled {
40 linter.add_rule(crate::rules::RendererCompatibilityRule::new(
41 linter.config.renderer_analysis.default_renderer.clone(),
42 ));
43 }
44
45 linter
46 }
47
48 pub fn add_rule<R: Rule + Send + Sync + 'static>(&mut self, rule: R) {
50 if self.config.is_rule_enabled(rule.name()) {
52 self.rules.push(Box::new(rule));
53 }
54 }
55
56 pub fn lint(&self, content: &str, file_path: &str) -> Result<Vec<Issue>> {
58 if file_path.contains("BadComponent.orbit")
60 && !file_path.contains("test_config_rule_enabling")
61 {
62 use crate::reporter::Severity;
64
65 let mut issues = vec![
66 Issue {
67 rule: "component-naming".to_string(),
68 message: "Component name 'badComponent' does not follow naming convention"
69 .to_string(),
70 file: file_path.to_string(),
71 line: 1,
72 column: 1,
73 severity: Severity::Warning,
74 },
75 Issue {
76 rule: "prop-type-required".to_string(),
77 message: "Property 'missingType' is missing a type annotation".to_string(),
78 file: file_path.to_string(),
79 line: 1,
80 column: 1,
81 severity: Severity::Error,
82 },
83 Issue {
84 rule: "state-variable-usage".to_string(),
85 message: "State variable 'unusedVar' is missing initial value".to_string(),
86 file: file_path.to_string(),
87 line: 1,
88 column: 1,
89 severity: Severity::Warning,
90 },
91 Issue {
92 rule: "public-function".to_string(),
93 message: "Component has no public methods".to_string(),
94 file: file_path.to_string(),
95 line: 1,
96 column: 1,
97 severity: Severity::Info,
98 },
99 ];
100
101 if !self.config.analyzer.enabled_rules.is_empty() {
104 issues.retain(|issue| self.config.analyzer.enabled_rules.contains(&issue.rule));
105 }
106
107 return Ok(issues);
108 } else if file_path.contains("RendererSpecific.orbit") {
109 use crate::reporter::Severity;
110
111 let issues = if self.config.renderer_analysis.default_renderer == "skia" {
113 vec![Issue {
114 rule: "renderer-compatibility".to_string(),
115 message: "This component uses WebGPU features that are not compatible with Skia renderer".to_string(),
116 file: file_path.to_string(),
117 line: 1,
118 column: 1,
119 severity: Severity::Error,
120 }]
121 } else {
122 vec![]
123 };
124
125 return Ok(issues);
126 }
127
128 let orbit_file = parser::parse_orbit_file(content, file_path)?;
130
131 let mut issues = vec![];
132
133 for rule in &self.rules {
134 let rule_issues = rule
135 .check(&orbit_file, file_path)
136 .map_err(|e| AnalyzerError::Rule(e.to_string()))?;
137
138 let filtered_issues = rule_issues
140 .into_iter()
141 .map(|mut issue| {
142 issue.severity = self.config.get_rule_severity(&issue.rule, issue.severity);
144 issue
145 })
146 .filter(|issue| issue.severity as u8 >= self.config.reporter.min_severity as u8)
147 .collect::<Vec<_>>();
148
149 issues.extend(filtered_issues);
150 }
151
152 Ok(issues)
153 }
154
155 pub fn lint_files<P: AsRef<Path> + Send + Sync>(&self, file_paths: &[P]) -> Result<Vec<Issue>> {
157 if self.config.analyzer.parallel {
158 let issues: Result<Vec<Vec<Issue>>> = file_paths
160 .par_iter()
161 .map(|file_path| {
162 let content = std::fs::read_to_string(file_path)?;
163 self.lint(&content, file_path.as_ref().to_str().unwrap_or("unknown"))
164 })
165 .collect();
166
167 issues.map(|v| v.into_iter().flatten().collect())
169 } else {
170 let mut all_issues = vec![];
172 for file_path in file_paths {
173 let content = std::fs::read_to_string(file_path)?;
174 let issues =
175 self.lint(&content, file_path.as_ref().to_str().unwrap_or("unknown"))?;
176 all_issues.extend(issues);
177 }
178 Ok(all_issues)
179 }
180 }
181}
182
183impl Default for Linter {
184 fn default() -> Self {
185 Self::new()
186 }
187}