#![deny(clippy::print_stdout)]
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process;
use harn_lint::LintSeverity;
use harn_parser::analysis::AnalysisDatabase;
use crate::package::CheckConfig;
use super::analysis::{analyze_file, render_file_analysis_error_or_exit};
use super::outcome::{print_lint_diagnostics, render_lint_diagnostics, CommandOutcome};
use harn_lint::is_generated_path;
pub(crate) fn project_engine_rule_sources(path: &Path) -> Vec<String> {
let Some((manifest, dir)) = crate::package::nearest_manifest_or_warn(path) else {
return Vec::new();
};
let mut sources = Vec::new();
for rel in &manifest.rules.rule_dirs {
let Ok(entries) = std::fs::read_dir(dir.join(rel)) else {
continue;
};
let mut files: Vec<_> = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("toml"))
.collect();
files.sort();
for file in files {
if let Ok(src) = std::fs::read_to_string(&file) {
if rule_targets_harn(&src) {
sources.push(src);
}
}
}
}
sources
}
pub(crate) fn project_native_rule_paths(path: &Path) -> Vec<PathBuf> {
let Some((manifest, dir)) = crate::package::nearest_manifest_or_warn(path) else {
return Vec::new();
};
let mut paths = Vec::new();
for rel in &manifest.rules.native_rule_dirs {
let Ok(entries) = std::fs::read_dir(dir.join(rel)) else {
continue;
};
let mut files: Vec<_> = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|p| {
p.extension().and_then(|e| e.to_str()) == Some(std::env::consts::DLL_EXTENSION)
})
.collect();
files.sort();
paths.extend(files);
}
paths
}
fn rule_targets_harn(src: &str) -> bool {
toml::from_str::<toml::Value>(src)
.ok()
.as_ref()
.and_then(|v| v.get("language"))
.and_then(|l| l.as_str())
== Some("harn")
}
#[cfg(test)]
pub(crate) fn lint_file_inner(
analysis: &mut AnalysisDatabase,
path: &Path,
config: &CheckConfig,
externally_imported_names: &HashSet<String>,
module_graph: &harn_modules::ModuleGraph,
lint_config: &super::config::HarnLintConfig,
script_rule_diagnostics: &[harn_lint::LintDiagnostic],
) -> CommandOutcome {
let path_str = path.to_string_lossy().into_owned();
let output = analyze_file(analysis, path, config, module_graph)
.unwrap_or_else(|error| render_file_analysis_error_or_exit(&path_str, error));
let (outcome, rendered) = lint_analyzed_file(
path,
&output,
config,
externally_imported_names,
module_graph,
lint_config,
script_rule_diagnostics,
);
eprint!("{rendered}");
outcome
}
pub(crate) struct LintedFile {
pub(crate) outcome: CommandOutcome,
pub(crate) rendered: String,
pub(crate) strict: bool,
}
pub(crate) fn lint_files(
files: &[PathBuf],
module_graph: &harn_modules::ModuleGraph,
parsed_targets: &super::config::ParsedTargets,
externally_imported_names: &HashSet<String>,
script_rule_diagnostics: &std::collections::HashMap<PathBuf, Vec<harn_lint::LintDiagnostic>>,
require_file_header: bool,
trusted_host_dispatch: bool,
) -> Vec<LintedFile> {
super::driver::run_ordered(
files,
super::driver::worker_count(files.len()),
AnalysisDatabase::new,
|analysis, file| {
let mut config = crate::package::load_check_config(Some(file));
let mut lint_config = super::config::load_harn_lint_config(file);
lint_config.require_file_header |= require_file_header;
super::config::apply_loaded_harn_lint_config(&lint_config, &mut config);
config.trusted_host_dispatch |= trusted_host_dispatch;
parsed_targets.seed(analysis, file);
let path_str = file.to_string_lossy().into_owned();
let output = analyze_file(analysis, file, &config, module_graph);
analysis.remove_source(&harn_parser::analysis::SourceId::path(file));
let (outcome, rendered) = match output {
Ok(output) => lint_analyzed_file(
file,
&output,
&config,
externally_imported_names,
module_graph,
&lint_config,
script_rule_diagnostics
.get(file)
.map(Vec::as_slice)
.unwrap_or(&[]),
),
Err(error) => (
CommandOutcome {
has_error: true,
..CommandOutcome::default()
},
super::analysis::render_file_analysis_error_to_string(&path_str, &error),
),
};
LintedFile {
outcome,
rendered,
strict: config.strict,
}
},
|file| LintedFile {
outcome: CommandOutcome {
has_error: true,
..CommandOutcome::default()
},
rendered: format!(
"{}: error: internal `harn lint` failure while analyzing this file\n",
file.display()
),
strict: false,
},
)
}
fn lint_analyzed_file(
path: &Path,
output: &harn_parser::analysis::TypeCheckOutput,
config: &CheckConfig,
externally_imported_names: &HashSet<String>,
module_graph: &harn_modules::ModuleGraph,
lint_config: &super::config::HarnLintConfig,
script_rule_diagnostics: &[harn_lint::LintDiagnostic],
) -> (CommandOutcome, String) {
let path_str = path.to_string_lossy().into_owned();
let engine_rules = project_engine_rule_sources(path);
let native_rule_paths = project_native_rule_paths(path);
let options = super::config::lint_options(
path,
config,
lint_config,
&engine_rules,
&native_rule_paths,
super::config::LintSurface::Lint,
);
let options = harn_lint::LintOptions {
binding_types: Some(&output.binding_types),
..options
};
let generated = is_generated_path(path);
let mut diagnostics = harn_lint::lint_with_module_graph(
&output.program,
&config.disable_rules,
Some(&output.source),
externally_imported_names,
module_graph,
path,
&options,
);
diagnostics.extend(harn_lint::lint_diagnostics_from_type_diagnostics(
&output.diagnostics,
&config.disable_rules,
));
if !generated {
diagnostics.extend(
script_rule_diagnostics
.iter()
.filter(|d| {
!config
.disable_rules
.iter()
.any(|r| r.as_str() == d.rule.as_ref())
})
.cloned(),
);
}
if diagnostics.is_empty() {
return (
CommandOutcome::default(),
format!("{path_str}: no issues found\n"),
);
}
let has_warning = diagnostics
.iter()
.any(|d| d.severity == LintSeverity::Warning);
let (has_error, fixable, rendered) =
render_lint_diagnostics(&path_str, &output.source, &diagnostics);
(
CommandOutcome {
has_error,
has_warning,
findings: diagnostics.len(),
fixable,
},
rendered,
)
}
pub(crate) fn lint_fix_file(
analysis: &mut AnalysisDatabase,
path: &Path,
config: &CheckConfig,
externally_imported_names: &HashSet<String>,
module_graph: &harn_modules::ModuleGraph,
lint_config: &super::config::HarnLintConfig,
) -> CommandOutcome {
let path_str = path.to_string_lossy().into_owned();
let output = analyze_file(analysis, path, config, module_graph)
.unwrap_or_else(|error| render_file_analysis_error_or_exit(&path_str, error));
let source = output.source;
let program = output.program;
let engine_rules = project_engine_rule_sources(path);
let native_rule_paths = project_native_rule_paths(path);
let options = super::config::lint_options(
path,
config,
lint_config,
&engine_rules,
&native_rule_paths,
super::config::LintSurface::Lint,
);
let options = harn_lint::LintOptions {
binding_types: Some(&output.binding_types),
..options
};
let lint_diags = harn_lint::lint_with_module_graph(
&program,
&config.disable_rules,
Some(&source),
externally_imported_names,
module_graph,
path,
&options,
);
let edits: Vec<harn_lexer::FixEdit> = lint_diags
.iter()
.filter_map(harn_lint::LintDiagnostic::machine_applicable_fix)
.chain(
output
.diagnostics
.iter()
.filter(|d| !harn_lint::type_diagnostic_lint_disabled(d, &config.disable_rules))
.filter_map(harn_parser::TypeDiagnostic::machine_applicable_fix),
)
.flatten()
.cloned()
.collect();
if edits.is_empty() {
let mut diagnostics = lint_diags;
diagnostics.extend(harn_lint::lint_diagnostics_from_type_diagnostics(
&output.diagnostics,
&config.disable_rules,
));
return outcome_from_diagnostics(&path_str, &source, &diagnostics);
}
let applied = harn_lexer::FixEdit::dedupe_overlapping(&edits).len();
let result = harn_lexer::FixEdit::apply_all(&source, &edits);
let result =
crate::commands::source_formatting::keep_canonical_formatting(path, &source, result);
std::fs::write(path, &result).unwrap_or_else(|e| {
eprintln!("Failed to write {path_str}: {e}");
process::exit(1);
});
eprintln!("{path_str}: applied {applied} fix(es)");
let output2 = analyze_file(analysis, path, config, module_graph)
.unwrap_or_else(|error| render_file_analysis_error_or_exit(&path_str, error));
let source2 = output2.source;
let program2 = output2.program;
let mut remaining = harn_lint::lint_with_module_graph(
&program2,
&config.disable_rules,
Some(&source2),
externally_imported_names,
module_graph,
path,
&harn_lint::LintOptions {
binding_types: Some(&output2.binding_types),
..options
},
);
remaining.extend(harn_lint::lint_diagnostics_from_type_diagnostics(
&output2.diagnostics,
&config.disable_rules,
));
outcome_from_diagnostics(&path_str, &source2, &remaining)
}
fn outcome_from_diagnostics(
path_str: &str,
source: &str,
diagnostics: &[harn_lint::LintDiagnostic],
) -> CommandOutcome {
if diagnostics.is_empty() {
return CommandOutcome::default();
}
let has_warning = diagnostics
.iter()
.any(|d| d.severity == LintSeverity::Warning);
let (has_error, fixable) = print_lint_diagnostics(path_str, source, diagnostics);
CommandOutcome {
has_error,
has_warning,
findings: diagnostics.len(),
fixable,
}
}