use std::path::{Path, PathBuf};
use std::process;
pub(crate) fn run_lint(
paths: &[PathBuf],
config_path: Option<&std::path::Path>,
errors_only: bool,
deny_warnings: bool,
) {
use seqc::lint;
use std::fs;
let config = match config_path {
Some(path) => {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error reading lint config: {}", e);
process::exit(1);
}
};
match lint::LintConfig::from_toml(&content) {
Ok(user_config) => {
let mut default = match lint::LintConfig::default_config() {
Ok(d) => d,
Err(e) => {
eprintln!("Error loading default lint config: {}", e);
process::exit(1);
}
};
default.merge(user_config);
default
}
Err(e) => {
eprintln!("Error parsing lint config: {}", e);
process::exit(1);
}
}
}
None => match lint::LintConfig::default_config() {
Ok(c) => c,
Err(e) => {
eprintln!("Error loading default lint config: {}", e);
process::exit(1);
}
},
};
let linter = match lint::Linter::new(&config) {
Ok(l) => l,
Err(e) => {
eprintln!("Error creating linter: {}", e);
process::exit(1);
}
};
let mut all_diagnostics = Vec::new();
let mut files_checked = 0;
for path in paths {
if path.is_dir() {
for entry in walkdir(path) {
if entry.extension().is_some_and(|e| e == "seq") {
if let Some(parent) = entry.parent() {
let has_manifest = std::fs::read_dir(parent)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.any(|e| e.path().extension().is_some_and(|ext| ext == "toml"))
})
.unwrap_or(false);
if has_manifest {
continue;
}
}
lint_file(&entry, &linter, &mut all_diagnostics);
files_checked += 1;
}
}
} else if path.exists() {
lint_file(path, &linter, &mut all_diagnostics);
files_checked += 1;
} else {
eprintln!("Warning: {} does not exist", path.display());
}
}
if errors_only {
all_diagnostics.retain(|d| d.severity == lint::Severity::Error);
}
if all_diagnostics.is_empty() {
println!("No lint issues found in {} file(s)", files_checked);
} else {
print!("{}", lint::format_diagnostics(&all_diagnostics));
let error_count = all_diagnostics
.iter()
.filter(|d| d.severity == lint::Severity::Error)
.count();
let warning_count = all_diagnostics
.iter()
.filter(|d| d.severity == lint::Severity::Warning)
.count();
let hint_count = all_diagnostics
.iter()
.filter(|d| d.severity == lint::Severity::Hint)
.count();
let issue_count = error_count + warning_count;
let files_with_issues: std::collections::HashSet<_> = all_diagnostics
.iter()
.filter(|d| d.severity != lint::Severity::Hint)
.map(|d| &d.file)
.collect();
if issue_count > 0 {
println!(
"\n{} issue(s) in {} file(s) ({} file(s) checked)",
issue_count,
files_with_issues.len(),
files_checked
);
}
if hint_count > 0 {
println!("{} hint(s) ({} file(s) checked)", hint_count, files_checked);
}
if issue_count == 0 && hint_count > 0 {
println!(
"\nNo errors or warnings ({} file(s) checked)",
files_checked
);
}
if error_count > 0 || (deny_warnings && warning_count > 0) {
process::exit(1);
}
}
}
fn lint_file(path: &PathBuf, linter: &seqc::Linter, diagnostics: &mut Vec<seqc::LintDiagnostic>) {
use seqc::{
ErrorFlagAnalyzer, Parser, ProgramResourceAnalyzer, TypeChecker, call_graph, lint,
resolver::Resolver,
};
use std::fs;
let source = match fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading {}: {}", path.display(), e);
return;
}
};
let mut parser = Parser::new(&source);
let mut program = match parser.parse() {
Ok(p) => p,
Err(e) => {
eprintln!("Parse error in {}: {}", path.display(), e);
return;
}
};
if let Err(e) = program.generate_constructors() {
eprintln!("Constructor error in {}: {}", path.display(), e);
return;
}
let file_diagnostics = linter.lint_program(&program, path);
diagnostics.extend(file_diagnostics);
let mut resource_analyzer = ProgramResourceAnalyzer::new(path);
let resource_diagnostics = resource_analyzer.analyze_program(&program);
diagnostics.extend(resource_diagnostics);
let mut flag_analyzer = ErrorFlagAnalyzer::new(path);
let flag_diagnostics = flag_analyzer.analyze_program(&program);
diagnostics.extend(flag_diagnostics);
let mut resolver = Resolver::new(None);
let mut resolved = match resolver.resolve(path, program) {
Ok(r) => r,
Err(e) => {
eprintln!("Include resolution error in {}: {}", path.display(), e);
return;
}
};
if !resolved.ffi_includes.is_empty() {
return;
}
if let Err(e) = resolved.program.generate_constructors() {
eprintln!("Constructor error in {}: {}", path.display(), e);
return;
}
let call_graph = call_graph::CallGraph::build(&resolved.program);
let mut type_checker = TypeChecker::new();
type_checker.set_call_graph(call_graph);
if let Err(e) = type_checker.check_program(&resolved.program) {
diagnostics.push(lint::LintDiagnostic {
id: "type-error".to_string(),
severity: lint::Severity::Error,
message: e,
file: path.clone(),
line: 0, start_column: None,
end_line: None,
end_column: None,
word_name: String::new(),
start_index: 0,
end_index: 0,
replacement: String::new(),
});
}
}
fn walkdir(dir: &Path) -> Vec<PathBuf> {
use std::fs;
let mut files = Vec::new();
match fs::read_dir(dir) {
Ok(entries) => {
for entry in entries {
match entry {
Ok(entry) => {
let path = entry.path();
if path.is_dir() {
files.extend(walkdir(&path));
} else {
files.push(path);
}
}
Err(e) => {
eprintln!(
"Warning: Could not read directory entry in {}: {}",
dir.display(),
e
);
}
}
}
}
Err(e) => {
eprintln!("Warning: Could not read directory {}: {}", dir.display(), e);
}
}
files
}