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, lint::Violation, rules::RuleRegistry,
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 nu_command::add_shell_command_context(engine_state)
35 })
36 }
37
38 #[must_use]
39 pub fn new(config: Config) -> Self {
40 Self {
41 registry: RuleRegistry::with_default_rules(),
42 config,
43 engine_state: Self::default_engine_state(),
44 }
45 }
46
47 pub fn lint_file(&self, path: &Path) -> Result<Vec<Violation>, LintError> {
53 let source = std::fs::read_to_string(path)?;
54 Ok(self.lint_source(&source, Some(path)))
55 }
56
57 #[must_use]
58 pub fn lint_source(&self, source: &str, path: Option<&Path>) -> Vec<Violation> {
59 let (block, working_set) = parse_source(self.engine_state, source.as_bytes());
60
61 let context = LintContext {
62 source,
63 file_path: path,
64 ast: &block,
65 engine_state: self.engine_state,
66 working_set: &working_set,
67 };
68
69 let mut violations = self.collect_violations(&context);
70
71 violations.extend(self.convert_parse_errors_to_violations(&working_set));
73
74 Self::attach_file_path(&mut violations, path);
75 Self::sort_violations(&mut violations);
76 violations
77 }
78
79 fn collect_violations(&self, context: &LintContext) -> Vec<Violation> {
81 let eligible_rules = self.get_eligible_rules();
82
83 eligible_rules
84 .flat_map(|rule| {
85 let rule_violations = (rule.check)(context);
86 let rule_severity = self.get_effective_rule_severity(rule);
87
88 rule_violations
90 .into_iter()
91 .map(|rule_violation| rule_violation.into_violation(rule_severity))
92 .collect::<Vec<_>>()
93 })
94 .collect()
95 }
96
97 fn convert_parse_errors_to_violations(&self, working_set: &StateWorkingSet) -> Vec<Violation> {
99 let parse_error_rule = self.registry.get_rule("nu_parse_error");
101
102 if parse_error_rule.is_none() {
103 return vec![];
104 }
105
106 let rule = parse_error_rule.unwrap();
107 let rule_severity = self.get_effective_rule_severity(rule);
108
109 if let Some(min_threshold) = self.get_minimum_severity_threshold()
111 && rule_severity < min_threshold
112 {
113 return vec![];
114 }
115
116 let mut seen = HashSet::new();
117
118 working_set
120 .parse_errors
121 .iter()
122 .filter_map(|parse_error| {
123 let key = (
124 parse_error.span().start,
125 parse_error.span().end,
126 parse_error.to_string(),
127 );
128 if seen.insert(key.clone()) {
129 use crate::lint::RuleViolation;
130
131 Some(
132 RuleViolation::new_dynamic(
133 "nu_parse_error",
134 parse_error.to_string(),
135 parse_error.span(),
136 )
137 .into_violation(rule_severity),
138 )
139 } else {
140 None
141 }
142 })
143 .collect()
144 }
145
146 fn get_enabled_rules(&self) -> impl Iterator<Item = &crate::rule::Rule> {
148 self.registry.all_rules().filter(|rule| {
149 !matches!(
152 self.config.rules.get(rule.id),
153 Some(&crate::config::RuleSeverity::Off)
154 )
155 })
156 }
157
158 fn get_eligible_rules(&self) -> impl Iterator<Item = &crate::rule::Rule> {
162 let min_severity_threshold = self.get_minimum_severity_threshold();
163
164 self.get_enabled_rules().filter(move |rule| {
165 let rule_severity = self.get_effective_rule_severity(rule);
166
167 if matches!(
169 self.config.general.min_severity,
170 crate::config::RuleSeverity::Off
171 ) {
172 return false;
173 }
174
175 match min_severity_threshold {
177 Some(min_threshold) => rule_severity >= min_threshold,
178 None => true, }
180 })
181 }
182
183 fn get_effective_rule_severity(&self, rule: &crate::rule::Rule) -> crate::lint::Severity {
185 if let Some(config_severity) = self.config.rule_severity(rule.id) {
186 config_severity
187 } else {
188 rule.severity
189 }
190 }
191
192 fn get_minimum_severity_threshold(&self) -> Option<crate::lint::Severity> {
200 use crate::config::RuleSeverity;
201 match self.config.general.min_severity {
202 RuleSeverity::Error => Some(crate::lint::Severity::Error), RuleSeverity::Warning => Some(crate::lint::Severity::Warning), RuleSeverity::Info | RuleSeverity::Off => None, }
207 }
208
209 fn attach_file_path(violations: &mut [Violation], path: Option<&Path>) {
211 if let Some(file_path_str) = path.and_then(|p| p.to_str()) {
212 use std::borrow::Cow;
213 let file_path: Cow<'static, str> = file_path_str.to_owned().into();
214 for violation in violations {
215 violation.file = Some(file_path.clone());
216 }
217 }
218 }
219
220 fn sort_violations(violations: &mut [Violation]) {
222 violations.sort_by(|a, b| {
223 a.span
224 .start
225 .cmp(&b.span.start)
226 .then(a.severity.cmp(&b.severity))
227 });
228 }
229
230 #[must_use]
231 pub fn registry(&self) -> &RuleRegistry {
232 &self.registry
233 }
234}