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
201pub fn path_is_stdlib_source(path: &Path) -> bool {
208 use std::path::Component;
209
210 let mut prev: Option<&std::ffi::OsStr> = None;
211 let mut prev_prev: Option<&std::ffi::OsStr> = None;
212 for comp in path.components() {
213 if let Component::Normal(name) = comp {
214 if prev == Some(std::ffi::OsStr::new("src"))
215 && prev_prev == Some(std::ffi::OsStr::new("harn-stdlib"))
216 && name == std::ffi::OsStr::new("stdlib")
217 {
218 return true;
219 }
220 prev_prev = prev;
221 prev = Some(name);
222 } else {
223 prev_prev = prev;
224 prev = None;
225 }
226 }
227 false
228}
229
230fn lint_full(
231 program: &[SNode],
232 disabled_rules: &[String],
233 source: Option<&str>,
234 externally_imported_names: &HashSet<String>,
235 options: &LintOptions<'_>,
236 module_graph: Option<(&harn_modules::ModuleGraph, &Path)>,
237) -> Vec<LintDiagnostic> {
238 if options.file_path.is_some_and(is_generated_path) {
242 return Vec::new();
243 }
244 let mut linter = Linter::new(source);
245 for engine_source in options.engine_rules {
248 if let Some(rule) = crate::engine_rule::EngineRule::from_toml(engine_source) {
249 linter.rules.push(Box::new(rule));
250 }
251 }
252 let (mut native_rules, mut native_load_diagnostics) =
253 crate::native_rule::load_rules_from_paths(options.native_rule_paths);
254 linter.rules.append(&mut native_rules);
255 linter.rules_visit_nodes = linter.rules.iter().any(|rule| rule.visits_nodes());
256 linter.diagnostics.append(&mut native_load_diagnostics);
257 linter.file_path = options.file_path.map(Path::to_path_buf);
258 linter
259 .externally_imported_names
260 .clone_from(externally_imported_names);
261 if let Some((module_graph, file_path)) = module_graph {
262 linter.use_module_graph_for_wildcards = true;
263 linter.module_graph_wildcard_exports = match module_graph.wildcard_exports_for(file_path) {
264 WildcardResolution::Resolved(exports) => Some(exports),
265 WildcardResolution::Unknown => None,
266 };
267 }
268 if let Some(threshold) = options.complexity_threshold {
269 linter.complexity_threshold = threshold;
270 }
271 linter.require_stdlib_metadata = options.require_stdlib_metadata;
272 linter.require_docstrings = options.require_docstrings;
273 linter
274 .persona_step_allowlist
275 .extend(options.persona_step_allowlist.iter().cloned());
276 linter.lint_program(program);
277 if let Some(src) = source {
278 if options.require_file_header {
279 check_require_file_header(src, options.file_path, &mut linter.diagnostics);
280 }
281 }
282 linter.finalize();
283 let mut diagnostics: Vec<LintDiagnostic> = if disabled_rules.is_empty() {
284 linter.diagnostics
285 } else {
286 linter
287 .diagnostics
288 .into_iter()
289 .filter(|d| !rule_disabled(&d.rule, disabled_rules))
290 .collect()
291 };
292 if !options.severity_overrides.is_empty() {
294 for diagnostic in &mut diagnostics {
295 if let Some(&severity) = options.severity_overrides.get(diagnostic.rule.as_ref()) {
296 diagnostic.severity = severity;
297 }
298 }
299 }
300 diagnostics
301}
302
303pub fn lint_diagnostics_from_type_diagnostics(
307 diagnostics: &[harn_parser::TypeDiagnostic],
308 disabled_rules: &[String],
309) -> Vec<LintDiagnostic> {
310 diagnostics
311 .iter()
312 .filter_map(type_diagnostic_as_lint)
313 .filter(|diagnostic| !rule_disabled(&diagnostic.rule, disabled_rules))
314 .collect()
315}
316
317pub fn type_diagnostic_lint_disabled(
320 diagnostic: &harn_parser::TypeDiagnostic,
321 disabled_rules: &[String],
322) -> bool {
323 type_diagnostic_lint_rule(diagnostic).is_some_and(|rule| rule_disabled(rule, disabled_rules))
324}
325
326fn type_diagnostic_as_lint(diagnostic: &harn_parser::TypeDiagnostic) -> Option<LintDiagnostic> {
327 let rule = type_diagnostic_lint_rule(diagnostic)?;
328 let span = diagnostic.span?;
329 Some(LintDiagnostic {
330 code: diagnostic.code,
331 rule: rule.into(),
332 message: diagnostic.message.clone(),
333 span,
334 severity: match diagnostic.severity {
335 harn_parser::DiagnosticSeverity::Warning => LintSeverity::Warning,
336 harn_parser::DiagnosticSeverity::Error => LintSeverity::Error,
337 },
338 suggestion: diagnostic.help.clone(),
339 fix: diagnostic.fix.clone(),
340 })
341}
342
343fn type_diagnostic_lint_rule(diagnostic: &harn_parser::TypeDiagnostic) -> Option<&'static str> {
344 match &diagnostic.details {
345 Some(harn_parser::DiagnosticDetails::LintRule { rule }) => Some(*rule),
346 _ => None,
347 }
348}
349
350fn rule_disabled(rule: &str, disabled_rules: &[String]) -> bool {
351 disabled_rules
352 .iter()
353 .any(|disabled| rule_matches_disabled(rule, disabled))
354}
355
356fn rule_matches_disabled(rule: &str, disabled: &str) -> bool {
357 rule == disabled || (rule == "dead-code-after-return" && disabled == "unreachable-code")
358}
359
360pub fn collect_selective_import_names(program: &[SNode]) -> HashSet<String> {
363 let mut names = HashSet::new();
364 for snode in program {
365 if let harn_parser::Node::SelectiveImport {
366 names: imported, ..
367 } = &snode.node
368 {
369 names.extend(imported.iter().cloned());
370 }
371 }
372 names
373}