1use std::{collections::HashSet, path::Path, sync::OnceLock};
2
3use nu_parser::parse;
4use nu_protocol::{
5 ast::Block,
6 engine::{EngineState, StateWorkingSet},
7};
8
9use crate::{
10 LintError, config::Config, context::LintContext, rules::RuleRegistry, violation::Violation,
11};
12
13fn parse_source<'a>(engine_state: &'a EngineState, source: &[u8]) -> (Block, StateWorkingSet<'a>) {
16 let mut working_set = StateWorkingSet::new(engine_state);
17 let block = parse(&mut working_set, None, source, false);
18
19 ((*block).clone(), working_set)
20}
21
22pub struct LintEngine {
23 registry: RuleRegistry,
24 config: Config,
25 engine_state: &'static EngineState,
26}
27
28impl LintEngine {
29 fn default_engine_state() -> &'static EngineState {
31 static ENGINE: OnceLock<EngineState> = OnceLock::new();
32 ENGINE.get_or_init(|| {
33 let engine_state = nu_cmd_lang::create_default_context();
34 let engine_state = nu_command::add_shell_command_context(engine_state);
35 let mut engine_state = nu_cli::add_cli_context(engine_state);
36
37 let delta = {
39 let mut working_set = StateWorkingSet::new(&engine_state);
40 working_set.add_decl(Box::new(nu_cli::Print));
41 working_set.render()
42 };
43
44 if let Err(err) = engine_state.merge_delta(delta) {
45 eprintln!("Error adding Print command: {err:?}");
46 }
47
48 engine_state
49 })
50 }
51
52 #[must_use]
53 pub fn new(config: Config) -> Self {
54 Self {
55 registry: RuleRegistry::with_default_rules(),
56 config,
57 engine_state: Self::default_engine_state(),
58 }
59 }
60
61 pub fn lint_file(&self, path: &Path) -> Result<Vec<Violation>, LintError> {
67 let source = std::fs::read_to_string(path)?;
68 Ok(self.lint_source(&source, Some(path)))
69 }
70
71 #[must_use]
72 pub fn lint_source(&self, source: &str, path: Option<&Path>) -> Vec<Violation> {
73 let (block, working_set) = parse_source(self.engine_state, source.as_bytes());
74
75 let context = LintContext {
76 source,
77 ast: &block,
78 engine_state: self.engine_state,
79 working_set: &working_set,
80 };
81
82 let mut violations = self.collect_violations(&context);
83
84 violations.extend(self.convert_parse_errors_to_violations(&working_set));
86
87 Self::attach_file_path(&mut violations, path);
88 Self::sort_violations(&mut violations);
89 violations
90 }
91
92 fn collect_violations(&self, context: &LintContext) -> Vec<Violation> {
94 let eligible_rules = self.get_eligible_rules();
95
96 eligible_rules
97 .flat_map(|rule| {
98 let rule_violations = (rule.check)(context);
99 let rule_severity = self.get_effective_rule_severity(rule);
100
101 rule_violations
103 .into_iter()
104 .map(|rule_violation| rule_violation.into_violation(rule_severity))
105 .collect::<Vec<_>>()
106 })
107 .collect()
108 }
109
110 fn convert_parse_errors_to_violations(&self, working_set: &StateWorkingSet) -> Vec<Violation> {
112 let parse_error_rule = self.registry.get_rule("nu_parse_error");
114
115 if parse_error_rule.is_none() {
116 return vec![];
117 }
118
119 let rule = parse_error_rule.unwrap();
120 let rule_severity = self.get_effective_rule_severity(rule);
121
122 if let Some(min_threshold) = self.get_minimum_severity_threshold()
124 && rule_severity < min_threshold
125 {
126 return vec![];
127 }
128
129 let mut seen = HashSet::new();
130
131 working_set
133 .parse_errors
134 .iter()
135 .filter_map(|parse_error| {
136 let key = (
137 parse_error.span().start,
138 parse_error.span().end,
139 parse_error.to_string(),
140 );
141 if seen.insert(key.clone()) {
142 use crate::violation::RuleViolation;
143
144 Some(
145 RuleViolation::new_dynamic(
146 "nu_parse_error",
147 parse_error.to_string(),
148 parse_error.span(),
149 )
150 .into_violation(rule_severity),
151 )
152 } else {
153 None
154 }
155 })
156 .collect()
157 }
158
159 fn get_enabled_rules(&self) -> impl Iterator<Item = &crate::rule::Rule> {
161 self.registry.all_rules().filter(|rule| {
162 !matches!(
165 self.config.rules.get(rule.id),
166 Some(&crate::config::RuleSeverity::Off)
167 )
168 })
169 }
170
171 fn get_eligible_rules(&self) -> impl Iterator<Item = &crate::rule::Rule> {
175 let min_severity_threshold = self.get_minimum_severity_threshold();
176
177 self.get_enabled_rules().filter(move |rule| {
178 let rule_severity = self.get_effective_rule_severity(rule);
179
180 if matches!(
182 self.config.general.min_severity,
183 crate::config::RuleSeverity::Off
184 ) {
185 return false;
186 }
187
188 match min_severity_threshold {
190 Some(min_threshold) => rule_severity >= min_threshold,
191 None => true, }
193 })
194 }
195
196 fn get_effective_rule_severity(&self, rule: &crate::rule::Rule) -> crate::violation::Severity {
198 if let Some(config_severity) = self.config.rule_severity(rule.id) {
199 config_severity
200 } else {
201 rule.severity
202 }
203 }
204
205 fn get_minimum_severity_threshold(&self) -> Option<crate::violation::Severity> {
213 use crate::config::RuleSeverity;
214 match self.config.general.min_severity {
215 RuleSeverity::Error => Some(crate::violation::Severity::Error), RuleSeverity::Warning => Some(crate::violation::Severity::Warning), RuleSeverity::Info | RuleSeverity::Off => None, }
220 }
221
222 fn attach_file_path(violations: &mut [Violation], path: Option<&Path>) {
224 if let Some(file_path_str) = path.and_then(|p| p.to_str()) {
225 use std::borrow::Cow;
226 let file_path: Cow<'static, str> = file_path_str.to_owned().into();
227 for violation in violations {
228 violation.file = Some(file_path.clone());
229 }
230 }
231 }
232
233 fn sort_violations(violations: &mut [Violation]) {
235 violations.sort_by(|a, b| {
236 a.span
237 .start
238 .cmp(&b.span.start)
239 .then(a.severity.cmp(&b.severity))
240 });
241 }
242
243 #[must_use]
244 pub fn registry(&self) -> &RuleRegistry {
245 &self.registry
246 }
247}