1use std::sync::Arc;
2use std::sync::RwLock;
3use std::sync::RwLockReadGuard;
4
5use mago_interner::ThreadedInterner;
6use mago_project::module::Module;
7use mago_reflection::CodebaseReflection;
8use mago_reporting::IssueCollection;
9use mago_reporting::Level;
10
11use crate::plugin::Plugin;
12use crate::rule::ConfiguredRule;
13use crate::rule::Rule;
14use crate::runner::Runner;
15use crate::settings::RuleSettings;
16use crate::settings::Settings;
17
18pub mod consts;
19pub mod context;
20pub mod definition;
21pub mod directive;
22pub mod plugin;
23pub mod rule;
24pub mod scope;
25pub mod settings;
26
27mod ast;
28mod pragma;
29mod runner;
30mod utils;
31
32#[derive(Debug, Clone)]
33pub struct Linter {
34 settings: Settings,
35 interner: ThreadedInterner,
36 codebase: Arc<CodebaseReflection>,
37 rules: Arc<RwLock<Vec<ConfiguredRule>>>,
38}
39
40impl Linter {
41 pub fn new(settings: Settings, interner: ThreadedInterner, codebase: CodebaseReflection) -> Self {
55 Self { settings, interner, codebase: Arc::new(codebase), rules: Arc::new(RwLock::new(Vec::new())) }
56 }
57
58 pub fn with_all_plugins(settings: Settings, interner: ThreadedInterner, codebase: CodebaseReflection) -> Self {
73 let mut linter = Self::new(settings, interner, codebase);
74
75 crate::foreach_plugin!(|plugin| linter.add_plugin(plugin));
76
77 linter
78 }
79
80 pub fn add_plugin(&mut self, plugin: impl Plugin) {
89 let plugin_definition = plugin.get_definition();
90 let plugin_slug = plugin_definition.get_slug();
91
92 tracing::debug!("Loading plugin: {plugin_slug}");
93
94 let enabled = self.settings.plugins.iter().any(|p| p.eq_ignore_ascii_case(&plugin_slug));
95 if !enabled {
96 if self.settings.default_plugins && plugin_definition.enabled_by_default {
97 tracing::debug!("Enabling default plugin: {plugin_slug}");
98 } else {
99 tracing::debug!("Plugin '{plugin_slug}' skipped, as it is not enabled by default or in the settings.",);
100
101 return;
102 }
103 } else {
104 tracing::debug!("Enabling plugin: {plugin_slug}");
105 }
106
107 for rule in plugin.get_rules() {
108 self.add_rule(&plugin_slug, rule);
109 }
110
111 tracing::debug!("Plugin '{plugin_slug}' loaded successfully.");
112 }
113
114 pub fn add_rule(&mut self, plugin_slug: impl Into<String>, rule: Box<dyn Rule>) {
123 let rule_definition = rule.get_definition();
124 let plugin_slug = plugin_slug.into();
125 let slug = format!("{}/{}", plugin_slug, rule_definition.get_slug());
126
127 tracing::debug!("Initializing rule `{slug}`...");
128
129 let settings = self.settings.get_rule_settings(slug.as_str());
130 if !rule_definition.supports_php_version(self.settings.php_version) {
131 tracing::debug!("Rule `{slug}` does not support PHP version `{}`.", self.settings.php_version);
132
133 if let Some(version) = rule_definition.minimum_supported_php_version {
134 tracing::debug!("Rule `{slug}` requires PHP >= `{version}`.");
135 }
136
137 if let Some(version) = rule_definition.maximum_supported_php_version {
138 tracing::debug!("Rule `{slug}` requires PHP < `{version}`.");
139 }
140
141 if settings.is_some() {
142 tracing::warn!("Configuration for rule `{slug}` ignored due to PHP version mismatch.");
143 }
144
145 tracing::debug!("Rule `{slug}` skipped due to PHP version mismatch.");
146
147 return;
148 }
149
150 let settings = settings.cloned().unwrap_or_else(|| {
151 tracing::debug!("No configuration found for rule `{slug}`, using default.");
152
153 RuleSettings::from_level(rule_definition.level)
154 });
155
156 if !settings.enabled {
157 tracing::debug!("Rule `{slug}` has been disabled.");
158
159 return;
160 }
161
162 let level = match settings.level {
163 Some(level) => level,
164 None => match rule_definition.level {
165 Some(level) => level,
166 None => {
167 tracing::debug!("Rule `{slug}` is disabled");
168
169 return;
170 }
171 },
172 };
173
174 tracing::debug!("Rule `{slug}` is enabled with level `{level}`.");
175
176 self.rules.write().expect("Unable to add rule: poisoned lock").push(ConfiguredRule {
177 slug,
178 level,
179 settings,
180 rule,
181 });
182 }
183
184 pub fn get_configured_rules(&self) -> RwLockReadGuard<'_, Vec<ConfiguredRule>> {
194 self.rules.read().expect("Unable to get rule: poisoned lock")
195 }
196
197 pub fn get_rule_level(&self, slug: &str) -> Option<Level> {
211 let configured_rules = self.rules.read().expect("Unable to get rule: poisoned lock");
212
213 configured_rules.iter().find(|r| r.slug == slug).map(|r| r.level)
214 }
215
216 pub fn lint(&self, module: &Module) -> IssueCollection {
228 let configured_rules = self.rules.read().expect("Unable to read rules: poisoned lock");
229 if configured_rules.is_empty() {
230 tracing::warn!("Linting aborted - no rules configured.");
231
232 return IssueCollection::new();
233 }
234
235 let program = module.parse(&self.interner);
236 let mut runner = Runner::new(self.settings.php_version, &self.interner, &self.codebase, module, &program);
237 for configured_rule in configured_rules.iter() {
238 runner.run(configured_rule);
239 }
240
241 runner.finish()
242 }
243}