1use std::{collections::HashSet, fs, 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, RuleViolation, Severity,
11 config::{Config, RuleSeverity},
12 context::LintContext,
13 rule::Rule,
14 rules::RuleRegistry,
15 violation::Violation,
16};
17
18fn parse_source<'a>(engine_state: &'a EngineState, source: &[u8]) -> (Block, StateWorkingSet<'a>) {
21 let mut working_set = StateWorkingSet::new(engine_state);
22 let block = parse(&mut working_set, None, source, false);
23
24 ((*block).clone(), working_set)
25}
26
27pub struct LintEngine {
28 pub registry: RuleRegistry,
29 config: Config,
30 engine_state: &'static EngineState,
31}
32
33impl LintEngine {
34 fn default_engine_state() -> &'static EngineState {
36 static ENGINE: OnceLock<EngineState> = OnceLock::new();
37 ENGINE.get_or_init(|| {
38 let engine_state = nu_cmd_lang::create_default_context();
39 let engine_state = nu_command::add_shell_command_context(engine_state);
40 let mut engine_state = nu_cli::add_cli_context(engine_state);
41
42 let delta = {
44 let mut working_set = StateWorkingSet::new(&engine_state);
45 working_set.add_decl(Box::new(nu_cli::Print));
46 working_set.render()
47 };
48
49 if let Err(err) = engine_state.merge_delta(delta) {
50 eprintln!("Error adding Print command: {err:?}");
51 }
52
53 engine_state
54 })
55 }
56
57 #[must_use]
58 pub fn new(config: Config) -> Self {
59 Self {
60 registry: RuleRegistry::with_default_rules(),
61 config,
62 engine_state: Self::default_engine_state(),
63 }
64 }
65
66 pub(crate) fn lint_file(&self, path: &Path) -> Result<Vec<Violation>, LintError> {
72 let source = fs::read_to_string(path)?;
73 Ok(self.lint_source(&source, Some(path)))
74 }
75
76 #[must_use]
77 pub fn lint_source(&self, source: &str, path: Option<&Path>) -> Vec<Violation> {
78 let (block, working_set) = parse_source(self.engine_state, source.as_bytes());
79
80 let context = LintContext {
81 source,
82 ast: &block,
83 engine_state: self.engine_state,
84 working_set: &working_set,
85 };
86
87 let mut violations = self.collect_violations(&context);
88
89 violations.extend(self.convert_parse_errors_to_violations(&working_set));
91
92 Self::attach_file_path(&mut violations, path);
93 Self::sort_violations(&mut violations);
94 violations
95 }
96
97 fn collect_violations(&self, context: &LintContext) -> Vec<Violation> {
99 let eligible_rules = self.get_eligible_rules();
100
101 eligible_rules
102 .flat_map(|rule| {
103 let rule_violations = (rule.check)(context);
104 let rule_severity = self.get_effective_rule_severity(rule);
105
106 rule_violations
108 .into_iter()
109 .map(|rule_violation| rule_violation.into_violation(rule_severity))
110 .collect::<Vec<_>>()
111 })
112 .collect()
113 }
114
115 fn convert_parse_errors_to_violations(&self, working_set: &StateWorkingSet) -> Vec<Violation> {
117 let parse_error_rule = self.registry.get_rule("nu_parse_error");
119
120 if parse_error_rule.is_none() {
121 return vec![];
122 }
123
124 let rule = parse_error_rule.unwrap();
125 let rule_severity = self.get_effective_rule_severity(rule);
126
127 if let Some(min_threshold) = self.get_minimum_severity_threshold()
129 && rule_severity < min_threshold
130 {
131 return vec![];
132 }
133
134 let mut seen = HashSet::new();
135
136 working_set
138 .parse_errors
139 .iter()
140 .filter_map(|parse_error| {
141 let key = (
142 parse_error.span().start,
143 parse_error.span().end,
144 parse_error.to_string(),
145 );
146 seen.insert(key).then(|| {
147 RuleViolation::new_dynamic(
148 "nu_parse_error",
149 parse_error.to_string(),
150 parse_error.span(),
151 )
152 .into_violation(rule_severity)
153 })
154 })
155 .collect()
156 }
157
158 fn get_enabled_rules(&self) -> impl Iterator<Item = &Rule> {
160 self.registry.all_rules().filter(|rule| {
161 !matches!(self.config.rules.get(rule.id), Some(&RuleSeverity::Off))
164 })
165 }
166
167 fn get_eligible_rules(&self) -> impl Iterator<Item = &Rule> {
171 let min_severity_threshold = self.get_minimum_severity_threshold();
172
173 self.get_enabled_rules().filter(move |rule| {
174 let rule_severity = self.get_effective_rule_severity(rule);
175
176 if matches!(self.config.general.min_severity, RuleSeverity::Off) {
178 return false;
179 }
180
181 min_severity_threshold.is_none_or(|min_threshold| rule_severity >= min_threshold)
183 })
184 }
185
186 fn get_effective_rule_severity(&self, rule: &Rule) -> Severity {
188 self.config
189 .rule_severity(rule.id)
190 .map_or(rule.severity, |config_severity| config_severity)
191 }
192
193 const fn get_minimum_severity_threshold(&self) -> Option<Severity> {
201 match self.config.general.min_severity {
202 RuleSeverity::Error => Some(Severity::Error), RuleSeverity::Warning => Some(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}