use std::collections::HashSet;
use std::path::Path;
use harn_lint::LintSeverity;
use harn_parser::{DiagnosticSeverity, TypeChecker, TypeDiagnostic};
use serde::Serialize;
use crate::package::CheckConfig;
use crate::parse_source_file;
use super::check_cmd::{CheckDiagnostic, CheckFileStatus, CheckSpan};
use super::lint::path_is_stdlib_source;
use super::outcome::CommandOutcome;
pub(crate) const LINT_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize)]
pub(crate) struct LintReport {
pub files: Vec<LintFileReport>,
pub summary: LintSummary,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct LintFileReport {
pub path: String,
pub status: CheckFileStatus,
pub diagnostics: Vec<CheckDiagnostic>,
pub fixable: usize,
pub fixed: usize,
}
impl LintFileReport {
pub(crate) fn outcome(&self) -> CommandOutcome {
CommandOutcome {
has_error: matches!(self.status, CheckFileStatus::Error),
has_warning: matches!(self.status, CheckFileStatus::Warning),
}
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub(crate) struct LintSummary {
pub ok: usize,
pub warnings: usize,
pub errors: usize,
pub diagnostics: usize,
pub fixable: usize,
pub fixed: usize,
}
impl LintReport {
pub(crate) fn from_files(files: Vec<LintFileReport>) -> Self {
let mut summary = LintSummary::default();
for file in &files {
match file.status {
CheckFileStatus::Ok => summary.ok += 1,
CheckFileStatus::Warning => summary.warnings += 1,
CheckFileStatus::Error => summary.errors += 1,
}
summary.diagnostics += file.diagnostics.len();
summary.fixable += file.fixable;
summary.fixed += file.fixed;
}
Self { files, summary }
}
}
pub(crate) fn lint_file_report(
path: &Path,
config: &CheckConfig,
externally_imported_names: &HashSet<String>,
module_graph: &harn_modules::ModuleGraph,
require_file_header: bool,
complexity_threshold: Option<usize>,
persona_step_allowlist: &[String],
) -> LintFileReport {
let path_str = path.to_string_lossy().into_owned();
let source = match std::fs::read_to_string(&path_str) {
Ok(text) => text,
Err(error) => {
return LintFileReport {
path: path_str.clone(),
status: CheckFileStatus::Error,
diagnostics: vec![CheckDiagnostic {
source: "io",
severity: "error",
code: None,
message: format!("Error reading {path_str}: {error}"),
span: None,
help: None,
}],
fixable: 0,
fixed: 0,
};
}
};
let program = match harn_parser::parse_source(&source) {
Ok(program) => program,
Err(error) => {
let span = error.span().copied();
let diagnostic = match error {
harn_parser::PipelineError::Lex(error) => CheckDiagnostic {
source: "lexer",
severity: "error",
code: Some(harn_parser::diagnostic::lexer_error_code(&error).to_string()),
message: error.to_string(),
span: span.map(check_span),
help: None,
},
harn_parser::PipelineError::Parse(error) => CheckDiagnostic {
source: "parser",
severity: "error",
code: Some(harn_parser::diagnostic::parser_error_code(&error).to_string()),
message: harn_parser::diagnostic::parser_error_message(&error),
span: span.map(check_span),
help: harn_parser::diagnostic::parser_error_help(&error).map(str::to_string),
},
harn_parser::PipelineError::TypeCheck(error) => CheckDiagnostic {
source: "type",
severity: type_severity_label(error.severity),
code: Some(error.code.to_string()),
message: error.message.clone(),
span: error.span.map(check_span),
help: error.help.clone(),
},
};
return LintFileReport {
path: path_str,
status: CheckFileStatus::Error,
diagnostics: vec![diagnostic],
fixable: 0,
fixed: 0,
};
}
};
let options = harn_lint::LintOptions {
file_path: Some(path),
require_file_header,
complexity_threshold,
persona_step_allowlist,
require_stdlib_metadata: path_is_stdlib_source(path),
};
let lint_diagnostics = harn_lint::lint_with_module_graph(
&program,
&config.disable_rules,
Some(&source),
externally_imported_names,
module_graph,
path,
&options,
);
let type_diags = type_check_for_lint(path, config, module_graph, &program, &source);
let type_lint_diagnostics =
harn_lint::lint_diagnostics_from_type_diagnostics(&type_diags, &config.disable_rules);
let mut has_error = false;
let mut has_warning = false;
let mut fixable = 0usize;
let mut diagnostics: Vec<CheckDiagnostic> = Vec::new();
for diag in &lint_diagnostics {
match diag.severity {
LintSeverity::Error => has_error = true,
LintSeverity::Warning => has_warning = true,
LintSeverity::Info => {}
}
if diag.fix.is_some() {
fixable += 1;
}
diagnostics.push(CheckDiagnostic {
source: "lint",
severity: lint_severity_label(diag.severity),
code: Some(diag.code.to_string()),
message: diag.message.clone(),
span: Some(check_span(diag.span)),
help: diag.suggestion.clone(),
});
}
for diag in &type_lint_diagnostics {
match diag.severity {
LintSeverity::Error => has_error = true,
LintSeverity::Warning => has_warning = true,
LintSeverity::Info => {}
}
if diag.fix.is_some() {
fixable += 1;
}
diagnostics.push(CheckDiagnostic {
source: "lint",
severity: lint_severity_label(diag.severity),
code: Some(diag.code.to_string()),
message: diag.message.clone(),
span: Some(check_span(diag.span)),
help: diag.suggestion.clone(),
});
}
let status = if has_error {
CheckFileStatus::Error
} else if has_warning {
CheckFileStatus::Warning
} else {
CheckFileStatus::Ok
};
LintFileReport {
path: path_str,
status,
diagnostics,
fixable,
fixed: 0,
}
}
fn type_check_for_lint(
path: &Path,
config: &CheckConfig,
module_graph: &harn_modules::ModuleGraph,
program: &[harn_parser::SNode],
source: &str,
) -> Vec<TypeDiagnostic> {
let mut checker = TypeChecker::with_strict_types(config.strict_types);
if let Some(imported) = module_graph.imported_names_for_file(path) {
checker = checker.with_imported_names(imported);
}
if let Some(imported) = module_graph.imported_type_declarations_for_file(path) {
checker = checker.with_imported_type_decls(imported);
}
if let Some(imported) = module_graph.imported_callable_declarations_for_file(path) {
checker = checker.with_imported_callable_decls(imported);
}
checker.check_with_source(program, source)
}
fn type_severity_label(severity: DiagnosticSeverity) -> &'static str {
match severity {
DiagnosticSeverity::Error => "error",
DiagnosticSeverity::Warning => "warning",
}
}
fn lint_severity_label(severity: LintSeverity) -> &'static str {
match severity {
LintSeverity::Info => "info",
LintSeverity::Error => "error",
LintSeverity::Warning => "warning",
}
}
fn check_span(span: harn_lexer::Span) -> CheckSpan {
CheckSpan {
start: span.start,
end: span.end,
}
}
#[allow(dead_code)]
fn _ensure_parse_helper_imported() {
let _ = parse_source_file;
}