nu_lint/
engine.rs

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
14/// Parse Nushell source code into an AST and return both the Block and
15/// `StateWorkingSet`.
16fn 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    /// Get or initialize the default engine state
30    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            // Add print command (it's in nu-cli but not added by add_cli_context)
38            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    /// Lint a file at the given path.
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if the file cannot be read.
65    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    /// Collect violations from all enabled rules
100    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}