1use std::{borrow::Cow, 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, LintLevel, config::Config, context::LintContext, rules::ALL_RULES,
11 violation::Violation,
12};
13
14fn parse_source<'a>(engine_state: &'a EngineState, source: &[u8]) -> (Block, StateWorkingSet<'a>) {
17 let mut working_set = StateWorkingSet::new(engine_state);
18 let block = parse(&mut working_set, None, source, false);
19
20 ((*block).clone(), working_set)
21}
22
23pub struct LintEngine {
24 pub(crate) 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 config,
56 engine_state: Self::default_engine_state(),
57 }
58 }
59
60 pub(crate) fn lint_file(&self, path: &Path) -> Result<Vec<Violation>, LintError> {
66 log::debug!("Linting file: {}", path.display());
67 let source = fs::read_to_string(path)?;
68 let mut violations = self.lint_str(&source);
69
70 let file_path: &str = path.to_str().unwrap();
71 let file_path: Cow<'static, str> = file_path.to_owned().into();
72 for violation in &mut violations {
73 violation.file = Some(file_path.clone());
74 }
75
76 violations.sort_by(|a, b| {
77 a.span
78 .start
79 .cmp(&b.span.start)
80 .then(a.lint_level.cmp(&b.lint_level))
81 });
82 Ok(violations)
83 }
84
85 #[must_use]
86 pub fn lint_str(&self, source: &str) -> Vec<Violation> {
87 let (block, working_set) = parse_source(self.engine_state, source.as_bytes());
88
89 let context = LintContext {
90 source,
91 ast: &block,
92 engine_state: self.engine_state,
93 working_set: &working_set,
94 };
95
96 self.collect_violations(&context)
97 }
98
99 fn collect_violations(&self, context: &LintContext) -> Vec<Violation> {
101 ALL_RULES
102 .iter()
103 .filter_map(|rule| {
104 let lint_level = self.config.get_lint_level(rule.id);
105
106 if lint_level == LintLevel::Allow {
107 return None;
108 }
109
110 let mut violations = (rule.check)(context);
111 for violation in &mut violations {
112 violation.set_lint_level(lint_level);
113 }
114
115 (!violations.is_empty()).then_some(violations)
116 })
117 .flatten()
118 .collect()
119 }
120}