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    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        let source = fs::read_to_string(path)?;
67        let mut violations = self.lint_str(&source);
68
69        let file_path: &str = path.to_str().unwrap();
70        let file_path: Cow<'static, str> = file_path.to_owned().into();
71        for violation in &mut violations {
72            violation.file = Some(file_path.clone());
73        }
74
75        violations.sort_by(|a, b| {
76            a.span
77                .start
78                .cmp(&b.span.start)
79                .then(a.lint_level.cmp(&b.lint_level))
80        });
81        Ok(violations)
82    }
83
84    #[must_use]
85    pub fn lint_str(&self, source: &str) -> Vec<Violation> {
86        let (block, working_set) = parse_source(self.engine_state, source.as_bytes());
87
88        let context = LintContext {
89            source,
90            ast: &block,
91            engine_state: self.engine_state,
92            working_set: &working_set,
93        };
94
95        self.collect_violations(&context)
96    }
97
98    /// Collect violations from all enabled rules
99    fn collect_violations(&self, context: &LintContext) -> Vec<Violation> {
100        ALL_RULES
101            .iter()
102            .filter_map(|rule| {
103                let lint_level = self.config.get_lint_level(rule.id);
104
105                if lint_level == LintLevel::Allow {
106                    return None;
107                }
108
109                let mut violations = (rule.check)(context);
110                for violation in &mut violations {
111                    violation.set_lint_level(lint_level);
112                }
113
114                (!violations.is_empty()).then_some(violations)
115            })
116            .flatten()
117            .collect()
118    }
119}