1use std::collections::HashSet;
8use std::path::Path;
9
10use harn_modules::WildcardResolution;
11use harn_parser::{DiagnosticCode as Code, SNode};
12
13mod complexity;
14mod decls;
15mod diagnostic;
16mod engine_rule;
17mod fixes;
18mod harndoc;
19mod linter;
20mod naming;
21pub mod native;
22mod native_rule;
23mod rule;
24mod rules;
25
26#[cfg(test)]
27mod tests;
28
29pub use diagnostic::{LintDiagnostic, LintOptions, LintSeverity, DEFAULT_COMPLEXITY_THRESHOLD};
30pub use naming::simplify_bool_comparison;
31pub use rules::file_header::derive_file_header_title;
32pub use rules::template_variant_explosion::DEFAULT_BRANCH_THRESHOLD as DEFAULT_TEMPLATE_VARIANT_BRANCH_THRESHOLD;
33
34pub fn lint_prompt_template(
48 source: &str,
49 branch_threshold: Option<usize>,
50 disabled_rules: &[String],
51) -> Vec<LintDiagnostic> {
52 let constructs = match harn_vm::stdlib::template::lint::parse(source) {
53 Ok(constructs) => constructs,
54 Err(message) => {
55 return vec![LintDiagnostic {
56 code: Code::LintTemplateParse,
57 rule: "template-parse".into(),
58 message: format!("template did not parse: {message}"),
59 span: harn_lexer::Span::dummy(),
60 severity: LintSeverity::Error,
61 suggestion: None,
62 fix: None,
63 }];
64 }
65 };
66 let threshold = branch_threshold.unwrap_or(DEFAULT_TEMPLATE_VARIANT_BRANCH_THRESHOLD);
67 let mut diagnostics = Vec::new();
68 diagnostics.extend(rules::template_provider_identity::check(
69 &constructs,
70 source,
71 ));
72 diagnostics.extend(rules::template_variant_explosion::check(
73 &constructs,
74 threshold,
75 source,
76 ));
77 if disabled_rules.is_empty() {
78 diagnostics
79 } else {
80 diagnostics
81 .into_iter()
82 .filter(|d| !rule_disabled(&d.rule, disabled_rules))
83 .collect()
84 }
85}
86
87use linter::Linter;
88use rules::file_header::check_require_file_header;
89
90pub fn lint(program: &[SNode]) -> Vec<LintDiagnostic> {
92 lint_with_config_and_source(program, &[], None)
93}
94
95pub fn lint_with_source(program: &[SNode], source: &str) -> Vec<LintDiagnostic> {
97 lint_with_config_and_source(program, &[], Some(source))
98}
99
100pub fn lint_with_config(program: &[SNode], disabled_rules: &[String]) -> Vec<LintDiagnostic> {
102 lint_with_config_and_source(program, disabled_rules, None)
103}
104
105pub fn lint_with_config_and_source(
107 program: &[SNode],
108 disabled_rules: &[String],
109 source: Option<&str>,
110) -> Vec<LintDiagnostic> {
111 lint_full(
112 program,
113 disabled_rules,
114 source,
115 &HashSet::new(),
116 &LintOptions::default(),
117 None,
118 )
119}
120
121pub fn lint_with_cross_file_imports(
125 program: &[SNode],
126 disabled_rules: &[String],
127 source: Option<&str>,
128 externally_imported_names: &HashSet<String>,
129) -> Vec<LintDiagnostic> {
130 lint_full(
131 program,
132 disabled_rules,
133 source,
134 externally_imported_names,
135 &LintOptions::default(),
136 None,
137 )
138}
139
140pub fn lint_with_module_graph(
142 program: &[SNode],
143 disabled_rules: &[String],
144 source: Option<&str>,
145 externally_imported_names: &HashSet<String>,
146 module_graph: &harn_modules::ModuleGraph,
147 file_path: &Path,
148 options: &LintOptions<'_>,
149) -> Vec<LintDiagnostic> {
150 lint_full(
151 program,
152 disabled_rules,
153 source,
154 externally_imported_names,
155 options,
156 Some((module_graph, file_path)),
157 )
158}
159
160pub fn lint_with_options(
162 program: &[SNode],
163 disabled_rules: &[String],
164 source: Option<&str>,
165 externally_imported_names: &HashSet<String>,
166 options: &LintOptions<'_>,
167) -> Vec<LintDiagnostic> {
168 lint_full(
169 program,
170 disabled_rules,
171 source,
172 externally_imported_names,
173 options,
174 None,
175 )
176}
177
178pub const GENERATED_HARN_SUFFIX: &str = ".generated.harn";
181
182pub fn is_generated_path(path: &Path) -> bool {
196 path.file_name()
197 .and_then(|name| name.to_str())
198 .is_some_and(|name| name.ends_with(GENERATED_HARN_SUFFIX))
199}
200
201fn lint_full(
202 program: &[SNode],
203 disabled_rules: &[String],
204 source: Option<&str>,
205 externally_imported_names: &HashSet<String>,
206 options: &LintOptions<'_>,
207 module_graph: Option<(&harn_modules::ModuleGraph, &Path)>,
208) -> Vec<LintDiagnostic> {
209 if options.file_path.is_some_and(is_generated_path) {
213 return Vec::new();
214 }
215 let mut linter = Linter::new(source);
216 for engine_source in options.engine_rules {
219 if let Some(rule) = crate::engine_rule::EngineRule::from_toml(engine_source) {
220 linter.rules.push(Box::new(rule));
221 }
222 }
223 let (mut native_rules, mut native_load_diagnostics) =
224 crate::native_rule::load_rules_from_paths(options.native_rule_paths);
225 linter.rules.append(&mut native_rules);
226 linter.rules_visit_nodes = linter.rules.iter().any(|rule| rule.visits_nodes());
227 linter.diagnostics.append(&mut native_load_diagnostics);
228 linter.file_path = options.file_path.map(Path::to_path_buf);
229 linter
230 .externally_imported_names
231 .clone_from(externally_imported_names);
232 if let Some((module_graph, file_path)) = module_graph {
233 linter.use_module_graph_for_wildcards = true;
234 linter.module_graph_wildcard_exports = match module_graph.wildcard_exports_for(file_path) {
235 WildcardResolution::Resolved(exports) => Some(exports),
236 WildcardResolution::Unknown => None,
237 };
238 }
239 if let Some(threshold) = options.complexity_threshold {
240 linter.complexity_threshold = threshold;
241 }
242 linter.require_stdlib_metadata = options.require_stdlib_metadata;
243 linter.require_docstrings = options.require_docstrings;
244 linter
245 .persona_step_allowlist
246 .extend(options.persona_step_allowlist.iter().cloned());
247 linter.lint_program(program);
248 if let Some(src) = source {
249 if options.require_file_header {
250 check_require_file_header(src, options.file_path, &mut linter.diagnostics);
251 }
252 }
253 linter.finalize();
254 let mut diagnostics: Vec<LintDiagnostic> = if disabled_rules.is_empty() {
255 linter.diagnostics
256 } else {
257 linter
258 .diagnostics
259 .into_iter()
260 .filter(|d| !rule_disabled(&d.rule, disabled_rules))
261 .collect()
262 };
263 if !options.severity_overrides.is_empty() {
265 for diagnostic in &mut diagnostics {
266 if let Some(&severity) = options.severity_overrides.get(diagnostic.rule.as_ref()) {
267 diagnostic.severity = severity;
268 }
269 }
270 }
271 diagnostics
272}
273
274pub fn lint_diagnostics_from_type_diagnostics(
278 diagnostics: &[harn_parser::TypeDiagnostic],
279 disabled_rules: &[String],
280) -> Vec<LintDiagnostic> {
281 diagnostics
282 .iter()
283 .filter_map(type_diagnostic_as_lint)
284 .filter(|diagnostic| !rule_disabled(&diagnostic.rule, disabled_rules))
285 .collect()
286}
287
288pub fn type_diagnostic_lint_disabled(
291 diagnostic: &harn_parser::TypeDiagnostic,
292 disabled_rules: &[String],
293) -> bool {
294 type_diagnostic_lint_rule(diagnostic).is_some_and(|rule| rule_disabled(rule, disabled_rules))
295}
296
297fn type_diagnostic_as_lint(diagnostic: &harn_parser::TypeDiagnostic) -> Option<LintDiagnostic> {
298 let rule = type_diagnostic_lint_rule(diagnostic)?;
299 let span = diagnostic.span?;
300 Some(LintDiagnostic {
301 code: diagnostic.code,
302 rule: rule.into(),
303 message: diagnostic.message.clone(),
304 span,
305 severity: match diagnostic.severity {
306 harn_parser::DiagnosticSeverity::Warning => LintSeverity::Warning,
307 harn_parser::DiagnosticSeverity::Error => LintSeverity::Error,
308 },
309 suggestion: diagnostic.help.clone(),
310 fix: diagnostic.fix.clone(),
311 })
312}
313
314fn type_diagnostic_lint_rule(diagnostic: &harn_parser::TypeDiagnostic) -> Option<&'static str> {
315 match &diagnostic.details {
316 Some(harn_parser::DiagnosticDetails::LintRule { rule }) => Some(*rule),
317 _ => None,
318 }
319}
320
321fn rule_disabled(rule: &str, disabled_rules: &[String]) -> bool {
322 disabled_rules
323 .iter()
324 .any(|disabled| rule_matches_disabled(rule, disabled))
325}
326
327fn rule_matches_disabled(rule: &str, disabled: &str) -> bool {
328 rule == disabled || (rule == "dead-code-after-return" && disabled == "unreachable-code")
329}
330
331pub fn collect_selective_import_names(program: &[SNode]) -> HashSet<String> {
334 let mut names = HashSet::new();
335 for snode in program {
336 if let harn_parser::Node::SelectiveImport {
337 names: imported, ..
338 } = &snode.node
339 {
340 names.extend(imported.iter().cloned());
341 }
342 }
343 names
344}