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 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 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 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}