use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use anyhow::{Context, Result};
use globset::{Glob, GlobSet, GlobSetBuilder};
use ignore::WalkBuilder;
use crate::check::SymbolRef;
use crate::config::Config;
use crate::indexer;
use crate::lang::{self, Import, Target};
use crate::scip;
use crate::suppress::{self, Suppression};
#[derive(Debug, Clone)]
pub struct Dep {
pub file: PathBuf,
pub line: usize,
pub specifier: String,
pub target: Target,
}
struct Parsed {
modified: Option<SystemTime>,
len: u64,
imports: Vec<Import>,
suppressions: Vec<Suppression>,
}
#[derive(Default)]
pub struct Cache {
parsed: HashMap<PathBuf, Parsed>,
pub no_index_cache: bool,
}
impl Cache {
pub fn new(no_index_cache: bool) -> Self {
Self {
no_index_cache,
..Self::default()
}
}
}
pub struct Scan {
pub all_files: Vec<String>,
pub paths: Vec<String>,
pub deps: Vec<Dep>,
pub symbols: Vec<SymbolRef>,
pub suppressions: Vec<Suppression>,
pub uncovered: Vec<String>,
pub indexed: (usize, usize),
}
pub fn walker(root: &Path) -> WalkBuilder {
let mut builder = WalkBuilder::new(root);
builder
.require_git(false)
.filter_entry(|e| e.file_name() != "node_modules");
builder
}
pub fn project_files(root: &Path, config: &Config) -> Result<Vec<String>> {
let exclude = exclude_set(&config.exclude)?;
let mut files = Vec::new();
for entry in walker(root).build() {
let entry = entry?;
if !entry.file_type().is_some_and(|t| t.is_file()) {
continue;
}
if let Some(rel) = lang::relative(root, entry.path())
&& !exclude.is_match(&rel)
{
files.push(slash(&rel));
}
}
Ok(files)
}
pub fn scan(root: &Path, config: &Config, cache: &mut Cache) -> Result<Scan> {
let exclude = exclude_set(&config.exclude)?;
let adapters = lang::adapters(root)?;
let mut paths = Vec::new();
let mut all_files = Vec::new();
let mut deps = Vec::new();
let mut suppressions = Vec::new();
let mut seen = HashSet::new();
for entry in walker(root).build() {
let entry = entry?;
if !entry.file_type().is_some_and(|t| t.is_file()) {
continue;
}
let path = entry.path();
let Some(rel) = lang::relative(root, path) else {
continue;
};
if exclude.is_match(&rel) {
continue;
}
all_files.push(slash(&rel));
if indexer::LANGUAGES.iter().any(|l| l.covers(&slash(&rel))) {
continue; }
let Some(adapter) = adapters.iter().find(|a| a.handles(path)) else {
continue;
};
let meta = entry.metadata()?;
let modified = meta.modified().ok();
let fresh = cache
.parsed
.get(&rel)
.is_some_and(|p| p.len == meta.len() && p.modified == modified && modified.is_some());
if !fresh {
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(_) => continue, };
let parsed = Parsed {
modified,
len: meta.len(),
imports: adapter.imports(path, &source).with_context(|| {
format!("{} adapter failed on {}", adapter.name(), rel.display())
})?,
suppressions: suppress::parse(&slash(&rel), &source),
};
cache.parsed.insert(rel.clone(), parsed);
}
let parsed = &cache.parsed[&rel];
seen.insert(rel.clone());
paths.push(slash(&rel));
suppressions.extend(parsed.suppressions.iter().cloned());
for import in &parsed.imports {
deps.push(Dep {
file: rel.clone(),
line: import.line,
specifier: import.specifier.clone(),
target: adapter.resolve(path, &import.specifier),
});
}
}
cache.parsed.retain(|path, _| seen.contains(path));
let indexed = indexer::run(root, &all_files, !cache.no_index_cache, &config.go.tags)?;
let project: HashSet<&str> = all_files.iter().map(String::as_str).collect();
let documents: Vec<scip::Document> = indexed
.documents
.0
.into_iter()
.filter(|d| project.contains(d.path.as_str()))
.collect();
for document in &documents {
paths.push(document.path.clone());
if let Ok(source) = std::fs::read_to_string(root.join(&document.path)) {
suppressions.extend(suppress::parse(&document.path, &source));
}
}
let facts = scip::facts(&documents, config.uses_symbols());
let in_project = |dep: &Dep| project.contains(slash(&dep.file).as_str());
deps.extend(facts.deps.into_iter().filter(in_project));
let symbols = facts.symbols;
Ok(Scan {
all_files,
paths,
deps,
symbols,
suppressions,
uncovered: indexed.uncovered,
indexed: (indexed.ran, indexed.cached),
})
}
fn exclude_set(patterns: &[String]) -> Result<GlobSet> {
let mut builder = GlobSetBuilder::new();
for p in patterns {
let p = p.trim_end_matches('/');
builder.add(Glob::new(p).with_context(|| format!("bad exclude pattern `{p}`"))?);
builder.add(Glob::new(&format!("{p}/**"))?);
}
Ok(builder.build()?)
}
pub fn slash(path: &Path) -> String {
path.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/")
}