use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use crate::config::LintConfig;
use crate::file_discovery::{FileDiscoveryError, collect_r_files};
use crate::incremental::{
Analysis, IncrementalDatabase, IncrementalDb, SourceFile, parsed_tree_root, semantic_model,
};
use crate::project::{
ExternalResolution, FileScope, Project, ProjectMember, external_resolution, package_root,
visible_symbols, workspace_project,
};
use crate::rindex::provider::IndexedProvider;
use crate::semantic::SymbolProvider;
use super::diagnostic::Diagnostic;
use super::rules::{ResolvedRules, default_symbol_provider, run_rules};
use super::suppression::SuppressionMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LintStatus {
Clean,
Findings { count: usize },
ParseDiagnostics { count: usize },
}
#[derive(Debug, Clone)]
pub struct LintFileReport {
pub path: PathBuf,
pub status: LintStatus,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone)]
pub struct LintResult {
pub checked_files: usize,
pub total_findings: usize,
pub reports: Vec<LintFileReport>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LintError {
MissingPaths,
NoRFiles,
NonRFilePath { path: PathBuf },
WalkError { path: PathBuf, message: String },
ReadError { path: PathBuf, source: String },
UnknownRule { rule: String },
}
impl fmt::Display for LintError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingPaths => {
write!(
f,
"lint requires at least one input path (file or directory)"
)
}
Self::NoRFiles => write!(f, "no .R files found under the provided input paths"),
Self::NonRFilePath { path } => write!(
f,
"input file {} is not an .R file; lint only supports .R files",
path.display()
),
Self::WalkError { path, message } => {
write!(f, "failed while scanning {}: {message}", path.display())
}
Self::ReadError { path, source } => {
write!(f, "failed to read {}: {source}", path.display())
}
Self::UnknownRule { rule } => write!(f, "unknown lint rule: `{rule}`"),
}
}
}
impl std::error::Error for LintError {}
impl From<FileDiscoveryError> for LintError {
fn from(value: FileDiscoveryError) -> Self {
match value {
FileDiscoveryError::NonRFilePath { path } => Self::NonRFilePath { path },
FileDiscoveryError::WalkError { path, message } => Self::WalkError { path, message },
}
}
}
pub fn check_paths(paths: &[PathBuf]) -> Result<LintResult, LintError> {
check_paths_with_config(paths, &LintConfig::default())
}
pub fn check_paths_with_config(
paths: &[PathBuf],
config: &LintConfig,
) -> Result<LintResult, LintError> {
check_paths_with_index(paths, config, IndexedProvider::empty())
}
pub fn check_paths_with_index(
paths: &[PathBuf],
config: &LintConfig,
indexed: IndexedProvider,
) -> Result<LintResult, LintError> {
if paths.is_empty() {
return Err(LintError::MissingPaths);
}
let (rules, unknown) = ResolvedRules::resolve(config.select.as_deref(), &config.ignore);
if let Some(rule) = unknown.into_iter().next() {
return Err(LintError::UnknownRule { rule });
}
let files = collect_r_files(paths).map_err(LintError::from)?;
if files.is_empty() {
return Err(LintError::NoRFiles);
}
let mut db = IncrementalDatabase::default();
let mut tracked: HashMap<PathBuf, SourceFile> = HashMap::new();
let mut parse_errors: HashMap<PathBuf, usize> = HashMap::new();
for path in &files {
let content = fs::read_to_string(path).map_err(|err| LintError::ReadError {
path: path.clone(),
source: err.to_string(),
})?;
let file = db.upsert_file(path, content);
tracked.insert(path.clone(), file);
let parse_diag_count = db.parse_diagnostics(file).len();
if parse_diag_count != 0 {
parse_errors.insert(path.clone(), parse_diag_count);
}
}
let manifest = db.set_library_index(indexed);
db.set_workspace_members(tracked.values().copied().collect(), files.clone());
let project = workspace_project(&db);
let fallback = default_symbol_provider();
let mut reports = Vec::new();
let mut total_findings = 0usize;
for path in files {
let file = tracked[&path];
let (status, diagnostics) = if let Some(&count) = parse_errors.get(&path) {
(LintStatus::ParseDiagnostics { count }, Vec::new())
} else {
let visibility = visible_symbols(&db, project, file);
let file_scope = visibility.scope();
let resolution = external_resolution(&db, manifest, project, file);
let kept = lint_parsed_file(
&db,
file,
&path,
&rules,
&fallback,
Some(&file_scope),
Some(resolution),
);
total_findings += kept.len();
let status = if kept.is_empty() {
LintStatus::Clean
} else {
LintStatus::Findings { count: kept.len() }
};
(status, kept)
};
reports.push(LintFileReport {
path,
status,
diagnostics,
});
}
Ok(LintResult {
checked_files: tracked.len(),
total_findings,
reports,
})
}
fn intern_project<'db>(
db: &'db dyn IncrementalDb,
mut members: Vec<ProjectMember>,
namespaces: Vec<(PathBuf, String)>,
) -> Project<'db> {
members.sort_by(|a, b| a.path.cmp(&b.path));
Project::new(db, members, namespaces)
}
fn lint_parsed_file(
db: &dyn IncrementalDb,
file: SourceFile,
path: &Path,
rules: &ResolvedRules,
provider: &dyn SymbolProvider,
project: Option<&FileScope<'_>>,
resolution: Option<&ExternalResolution>,
) -> Vec<Diagnostic> {
let root_node = parsed_tree_root(db, file);
let model = semantic_model(db, file);
let mut diagnostics = run_rules(
&rules.rules,
path,
&root_node,
model,
provider,
project,
resolution,
);
let suppress = SuppressionMap::build(&root_node);
diagnostics.retain(|d| !suppress.is_suppressed(d.rule, d.range));
for d in &mut diagnostics {
d.path = path.to_path_buf();
}
diagnostics
}
pub fn check_tracked_file(
db: &IncrementalDatabase,
file: SourceFile,
path: &Path,
config: &LintConfig,
provider: &dyn SymbolProvider,
) -> Result<Vec<Diagnostic>, LintError> {
let (rules, unknown) = ResolvedRules::resolve(config.select.as_deref(), &config.ignore);
if let Some(rule) = unknown.into_iter().next() {
return Err(LintError::UnknownRule { rule });
}
if !db.parse_diagnostics(file).is_empty() {
return Ok(Vec::new());
}
Ok(lint_parsed_file(
db, file, path, &rules, provider, None, None,
))
}
pub struct PreparedProject {
active: SourceFile,
rules: ResolvedRules,
members: Vec<ProjectMember>,
namespaces: Vec<(PathBuf, String)>,
}
pub fn prepare_document_in_project(
db: &mut IncrementalDatabase,
_path: &Path,
active: SourceFile,
config: &LintConfig,
) -> Result<Option<PreparedProject>, LintError> {
let (rules, unknown) = ResolvedRules::resolve(config.select.as_deref(), &config.ignore);
if let Some(rule) = unknown.into_iter().next() {
return Err(LintError::UnknownRule { rule });
}
if !db.parse_diagnostics(active).is_empty() {
return Ok(None);
}
let project = workspace_project(&*db);
let members = project.members(&*db).clone();
let namespaces = project.namespaces(&*db).clone();
Ok(Some(PreparedProject {
active,
rules,
members,
namespaces,
}))
}
pub fn seed_workspace_for(db: &mut IncrementalDatabase, path: &Path, active: SourceFile) {
let (mut files, mut roots) = match db.workspace() {
Some(ws) => (ws.members(&*db).to_vec(), ws.roots(&*db).to_vec()),
None => (Vec::new(), Vec::new()),
};
files.push(active);
let search_dir =
package_root(path).or_else(|| path.parent().filter(|p| p.is_dir()).map(Path::to_path_buf));
if let Some(dir) = search_dir {
for sibling in collect_r_files(std::slice::from_ref(&dir)).unwrap_or_default() {
if sibling == path {
continue;
}
if let Ok(text) = fs::read_to_string(&sibling) {
files.push(db.upsert_file(&sibling, text));
}
}
if !roots.contains(&dir) {
roots.push(dir);
}
}
db.set_workspace_members(files, roots);
}
pub fn analyze_prepared(
analysis: &Analysis,
prepared: &PreparedProject,
provider: &dyn SymbolProvider,
) -> Vec<Diagnostic> {
let db = analysis.as_db();
let project = intern_project(db, prepared.members.clone(), prepared.namespaces.clone());
let active_path = analysis
.file_path(prepared.active)
.map(Path::to_path_buf)
.unwrap_or_default();
let visibility = visible_symbols(db, project, prepared.active);
let file_scope = visibility.scope();
let resolution = analysis
.library_index()
.map(|manifest| external_resolution(db, manifest, project, prepared.active));
lint_parsed_file(
db,
prepared.active,
&active_path,
&prepared.rules,
provider,
Some(&file_scope),
resolution,
)
}
pub fn check_document_in_project(
db: &mut IncrementalDatabase,
path: &Path,
active: SourceFile,
config: &LintConfig,
provider: &dyn SymbolProvider,
) -> Result<Vec<Diagnostic>, LintError> {
seed_workspace_for(db, path, active);
match prepare_document_in_project(db, path, active, config)? {
Some(prepared) => {
let analysis = db.snapshot();
Ok(analyze_prepared(&analysis, &prepared, provider))
}
None => Ok(Vec::new()),
}
}
pub fn check_document(
path: &Path,
content: &str,
config: &LintConfig,
) -> Result<Vec<Diagnostic>, LintError> {
check_document_with_provider(path, content, config, &default_symbol_provider())
}
pub fn check_document_with_provider(
path: &Path,
content: &str,
config: &LintConfig,
provider: &dyn SymbolProvider,
) -> Result<Vec<Diagnostic>, LintError> {
let db = IncrementalDatabase::default();
let file = db.add_file(content.to_string());
check_tracked_file(&db, file, path, config, provider)
}