use std::collections::HashSet;
use std::path::Path;
use harn_modules::WildcardResolution;
use harn_parser::{DiagnosticCode as Code, SNode};
mod complexity;
mod decls;
mod diagnostic;
mod engine_rule;
mod fixes;
mod harndoc;
mod linter;
mod naming;
pub mod native;
mod native_rule;
mod rule;
mod rules;
#[cfg(test)]
mod tests;
pub use diagnostic::{LintDiagnostic, LintOptions, LintSeverity, DEFAULT_COMPLEXITY_THRESHOLD};
pub use naming::simplify_bool_comparison;
pub use rules::file_header::derive_file_header_title;
pub use rules::template_variant_explosion::DEFAULT_BRANCH_THRESHOLD as DEFAULT_TEMPLATE_VARIANT_BRANCH_THRESHOLD;
pub fn lint_prompt_template(
source: &str,
branch_threshold: Option<usize>,
disabled_rules: &[String],
) -> Vec<LintDiagnostic> {
let constructs = match harn_vm::stdlib::template::lint::parse(source) {
Ok(constructs) => constructs,
Err(message) => {
return vec![LintDiagnostic {
code: Code::LintTemplateParse,
rule: "template-parse".into(),
message: format!("template did not parse: {message}"),
span: harn_lexer::Span::dummy(),
severity: LintSeverity::Error,
suggestion: None,
fix: None,
}];
}
};
let threshold = branch_threshold.unwrap_or(DEFAULT_TEMPLATE_VARIANT_BRANCH_THRESHOLD);
let mut diagnostics = Vec::new();
diagnostics.extend(rules::template_provider_identity::check(
&constructs,
source,
));
diagnostics.extend(rules::template_variant_explosion::check(
&constructs,
threshold,
source,
));
if disabled_rules.is_empty() {
diagnostics
} else {
diagnostics
.into_iter()
.filter(|d| !rule_disabled(&d.rule, disabled_rules))
.collect()
}
}
use linter::Linter;
use rules::file_header::check_require_file_header;
pub fn lint(program: &[SNode]) -> Vec<LintDiagnostic> {
lint_with_config_and_source(program, &[], None)
}
pub fn lint_with_source(program: &[SNode], source: &str) -> Vec<LintDiagnostic> {
lint_with_config_and_source(program, &[], Some(source))
}
pub fn lint_with_config(program: &[SNode], disabled_rules: &[String]) -> Vec<LintDiagnostic> {
lint_with_config_and_source(program, disabled_rules, None)
}
pub fn lint_with_config_and_source(
program: &[SNode],
disabled_rules: &[String],
source: Option<&str>,
) -> Vec<LintDiagnostic> {
lint_full(
program,
disabled_rules,
source,
&HashSet::new(),
&LintOptions::default(),
None,
)
}
pub fn lint_with_cross_file_imports(
program: &[SNode],
disabled_rules: &[String],
source: Option<&str>,
externally_imported_names: &HashSet<String>,
) -> Vec<LintDiagnostic> {
lint_full(
program,
disabled_rules,
source,
externally_imported_names,
&LintOptions::default(),
None,
)
}
pub fn lint_with_module_graph(
program: &[SNode],
disabled_rules: &[String],
source: Option<&str>,
externally_imported_names: &HashSet<String>,
module_graph: &harn_modules::ModuleGraph,
file_path: &Path,
options: &LintOptions<'_>,
) -> Vec<LintDiagnostic> {
lint_full(
program,
disabled_rules,
source,
externally_imported_names,
options,
Some((module_graph, file_path)),
)
}
pub fn lint_with_options(
program: &[SNode],
disabled_rules: &[String],
source: Option<&str>,
externally_imported_names: &HashSet<String>,
options: &LintOptions<'_>,
) -> Vec<LintDiagnostic> {
lint_full(
program,
disabled_rules,
source,
externally_imported_names,
options,
None,
)
}
fn lint_full(
program: &[SNode],
disabled_rules: &[String],
source: Option<&str>,
externally_imported_names: &HashSet<String>,
options: &LintOptions<'_>,
module_graph: Option<(&harn_modules::ModuleGraph, &Path)>,
) -> Vec<LintDiagnostic> {
let mut linter = Linter::new(source);
for engine_source in options.engine_rules {
if let Some(rule) = crate::engine_rule::EngineRule::from_toml(engine_source) {
linter.rules.push(Box::new(rule));
}
}
let (mut native_rules, mut native_load_diagnostics) =
crate::native_rule::load_rules_from_paths(options.native_rule_paths);
linter.rules.append(&mut native_rules);
linter.rules_visit_nodes = linter.rules.iter().any(|rule| rule.visits_nodes());
linter.diagnostics.append(&mut native_load_diagnostics);
linter.file_path = options.file_path.map(Path::to_path_buf);
linter
.externally_imported_names
.clone_from(externally_imported_names);
if let Some((module_graph, file_path)) = module_graph {
linter.use_module_graph_for_wildcards = true;
linter.module_graph_wildcard_exports = match module_graph.wildcard_exports_for(file_path) {
WildcardResolution::Resolved(exports) => Some(exports),
WildcardResolution::Unknown => None,
};
}
if let Some(threshold) = options.complexity_threshold {
linter.complexity_threshold = threshold;
}
linter.require_stdlib_metadata = options.require_stdlib_metadata;
linter
.persona_step_allowlist
.extend(options.persona_step_allowlist.iter().cloned());
linter.lint_program(program);
if let Some(src) = source {
if options.require_file_header {
check_require_file_header(src, options.file_path, &mut linter.diagnostics);
}
}
linter.finalize();
let mut diagnostics: Vec<LintDiagnostic> = if disabled_rules.is_empty() {
linter.diagnostics
} else {
linter
.diagnostics
.into_iter()
.filter(|d| !rule_disabled(&d.rule, disabled_rules))
.collect()
};
if !options.severity_overrides.is_empty() {
for diagnostic in &mut diagnostics {
if let Some(&severity) = options.severity_overrides.get(diagnostic.rule.as_ref()) {
diagnostic.severity = severity;
}
}
}
diagnostics
}
pub fn lint_diagnostics_from_type_diagnostics(
diagnostics: &[harn_parser::TypeDiagnostic],
disabled_rules: &[String],
) -> Vec<LintDiagnostic> {
diagnostics
.iter()
.filter_map(type_diagnostic_as_lint)
.filter(|diagnostic| !rule_disabled(&diagnostic.rule, disabled_rules))
.collect()
}
pub fn type_diagnostic_lint_disabled(
diagnostic: &harn_parser::TypeDiagnostic,
disabled_rules: &[String],
) -> bool {
type_diagnostic_lint_rule(diagnostic).is_some_and(|rule| rule_disabled(rule, disabled_rules))
}
fn type_diagnostic_as_lint(diagnostic: &harn_parser::TypeDiagnostic) -> Option<LintDiagnostic> {
let rule = type_diagnostic_lint_rule(diagnostic)?;
let span = diagnostic.span?;
Some(LintDiagnostic {
code: diagnostic.code,
rule: rule.into(),
message: diagnostic.message.clone(),
span,
severity: match diagnostic.severity {
harn_parser::DiagnosticSeverity::Warning => LintSeverity::Warning,
harn_parser::DiagnosticSeverity::Error => LintSeverity::Error,
},
suggestion: diagnostic.help.clone(),
fix: diagnostic.fix.clone(),
})
}
fn type_diagnostic_lint_rule(diagnostic: &harn_parser::TypeDiagnostic) -> Option<&'static str> {
match &diagnostic.details {
Some(harn_parser::DiagnosticDetails::LintRule { rule }) => Some(*rule),
_ => None,
}
}
fn rule_disabled(rule: &str, disabled_rules: &[String]) -> bool {
disabled_rules
.iter()
.any(|disabled| rule_matches_disabled(rule, disabled))
}
fn rule_matches_disabled(rule: &str, disabled: &str) -> bool {
rule == disabled || (rule == "dead-code-after-return" && disabled == "unreachable-code")
}
pub fn collect_selective_import_names(program: &[SNode]) -> HashSet<String> {
let mut names = HashSet::new();
for snode in program {
if let harn_parser::Node::SelectiveImport {
names: imported, ..
} = &snode.node
{
names.extend(imported.iter().cloned());
}
}
names
}