1use std::{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, config::Config, context::LintContext, lint::Violation, rule::RuleMetadata,
11 rules::RuleRegistry,
12};
13
14fn parse_source<'a>(engine_state: &'a EngineState, source: &[u8]) -> (Block, StateWorkingSet<'a>) {
22 let mut working_set = StateWorkingSet::new(engine_state);
23 let block = parse(&mut working_set, None, source, false);
24
25 ((*block).clone(), working_set)
26}
27
28pub struct LintEngine {
29 registry: RuleRegistry,
30 config: Config,
31 engine_state: &'static EngineState,
32}
33
34pub struct LintEngineBuilder {
35 registry: Option<RuleRegistry>,
36 config: Option<Config>,
37 engine_state: Option<&'static EngineState>,
38}
39
40impl Default for LintEngineBuilder {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl LintEngineBuilder {
47 #[must_use]
48 pub fn new() -> Self {
49 Self {
50 registry: None,
51 config: None,
52 engine_state: None,
53 }
54 }
55
56 #[must_use]
57 pub fn with_config(mut self, config: Config) -> Self {
58 self.config = Some(config);
59 self
60 }
61
62 #[must_use]
63 pub fn with_registry(mut self, registry: RuleRegistry) -> Self {
64 self.registry = Some(registry);
65 self
66 }
67
68 #[must_use]
69 pub fn with_engine_state(mut self, engine_state: &'static EngineState) -> Self {
70 self.engine_state = Some(engine_state);
71 self
72 }
73
74 #[must_use]
75 pub fn engine_state() -> &'static EngineState {
76 static ENGINE: OnceLock<EngineState> = OnceLock::new();
77 ENGINE.get_or_init(|| {
78 let engine_state = nu_cmd_lang::create_default_context();
79 nu_command::add_shell_command_context(engine_state)
80 })
81 }
82
83 #[must_use]
84 pub fn build(self) -> LintEngine {
85 LintEngine {
86 registry: self
87 .registry
88 .unwrap_or_else(RuleRegistry::with_default_rules),
89 config: self.config.unwrap_or_default(),
90 engine_state: self.engine_state.unwrap_or_else(Self::engine_state),
91 }
92 }
93}
94
95impl LintEngine {
96 #[must_use]
97 pub fn new(config: Config) -> Self {
98 LintEngineBuilder::new().with_config(config).build()
99 }
100
101 #[must_use]
102 pub fn builder() -> LintEngineBuilder {
103 LintEngineBuilder::new()
104 }
105
106 pub fn lint_file(&self, path: &Path) -> Result<Vec<Violation>, LintError> {
112 let source = std::fs::read_to_string(path)?;
113 Ok(self.lint_source(&source, Some(path)))
114 }
115
116 #[must_use]
117 pub fn lint_source(&self, source: &str, path: Option<&Path>) -> Vec<Violation> {
118 let (block, working_set) = parse_source(self.engine_state, source.as_bytes());
119
120 let context = LintContext {
121 source,
122 ast: &block,
123 engine_state: self.engine_state,
124 working_set: &working_set,
125 file_path: path,
126 };
127
128 let mut violations = self.collect_violations(&context);
129 Self::attach_file_path(&mut violations, path);
130 Self::sort_violations(&mut violations);
131 violations
132 }
133
134 fn collect_violations(&self, context: &LintContext) -> Vec<Violation> {
136 let enabled_rules = self.get_enabled_rules();
137
138 enabled_rules.flat_map(|rule| rule.check(context)).collect()
139 }
140
141 fn get_enabled_rules(&self) -> impl Iterator<Item = &crate::rule::Rule> {
143 self.registry.all_rules().filter(|rule| {
144 if let Some(configured_severity) = self.config.rule_severity(rule.id()) {
145 configured_severity == rule.severity()
146 } else {
147 !self.config.rules.contains_key(rule.id())
148 }
149 })
150 }
151
152 fn attach_file_path(violations: &mut [Violation], path: Option<&Path>) {
154 if let Some(file_path) = path.and_then(|p| p.to_str()).map(String::from) {
155 for violation in violations {
156 violation.file = Some(file_path.clone());
157 }
158 }
159 }
160
161 fn sort_violations(violations: &mut [Violation]) {
163 violations.sort_by(|a, b| {
164 a.span
165 .start
166 .cmp(&b.span.start)
167 .then(a.severity.cmp(&b.severity))
168 });
169 }
170
171 #[must_use]
172 pub fn registry(&self) -> &RuleRegistry {
173 &self.registry
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_lint_valid_code() {
183 let engine = LintEngine::new(Config::default());
184 let source = "let my_variable = 5";
185 let violations = engine.lint_source(source, None);
186 assert_eq!(violations.len(), 0);
187 }
188
189 #[test]
190 fn test_lint_invalid_snake_case() {
191 let engine = LintEngine::new(Config::default());
192 let source = "let myVariable = 5";
193 let violations = engine.lint_source(source, None);
194 assert!(!violations.is_empty());
195 assert_eq!(violations[0].rule_id, "snake_case_variables");
196 }
197}