use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use crate::domain::config::{Agent, Config, RelevanceMode, Rule};
use crate::domain::ignore;
use crate::errors::{Error, Result};
use crate::io::configfs::{self, RuleScope};
use crate::io::files;
pub fn known_rules(config: &Config) -> BTreeSet<&str> {
config.rules.iter().map(|r| r.name.as_str()).collect()
}
pub fn target_files(
cwd: &Path,
config: &Config,
scopes: &BTreeMap<String, RuleScope>,
cli_files: &[PathBuf],
) -> Result<BTreeSet<PathBuf>> {
let mut out: BTreeSet<PathBuf> = BTreeSet::new();
for rule in &config.rules {
if matches!(rule.relevance_mode(), RelevanceMode::Never) {
continue;
}
let agent_name = rule.agent.clone().unwrap_or_else(|| "default".to_string());
let agent = config.agent_or_default(&agent_name);
let fallback;
let scope = match scopes.get(&rule.name) {
Some(s) => s,
None => {
fallback = RuleScope {
dir: cwd.to_path_buf(),
files: config.files.clone(),
};
&fallback
}
};
for f in resolve_files(cwd, rule, &agent, cli_files, scope)? {
out.insert(f);
}
}
Ok(out)
}
pub fn resolve_files(
cwd: &Path,
rule: &Rule,
agent: &Agent,
cli_files: &[PathBuf],
scope: &RuleScope,
) -> Result<Vec<PathBuf>> {
if let Some(f) = &rule.files {
return files::resolve_scoped(&scope.dir, cwd, f);
}
if let Some(f) = &agent.files {
return files::resolve_scoped(&scope.dir, cwd, f);
}
if !cli_files.is_empty() {
return Ok(scope_cli_files(cwd, &scope.dir, cli_files));
}
files::resolve_scoped(&scope.dir, cwd, &scope.files)
}
fn scope_cli_files(cwd: &Path, dir: &Path, cli_files: &[PathBuf]) -> Vec<PathBuf> {
cli_files
.iter()
.filter(|f| {
let abs = if f.is_absolute() {
(*f).clone()
} else {
cwd.join(f)
};
configfs::normalize(&abs).starts_with(dir)
})
.cloned()
.collect()
}
pub fn check(cwd: &Path, targets: &BTreeSet<PathBuf>, known: &BTreeSet<&str>) -> Result<()> {
let mut problems: Vec<String> = Vec::new();
for rel in targets {
let Some(text) = files::read_text(cwd, rel)? else {
continue;
};
for p in ignore::validate(&text, known) {
problems.push(format!(
" {}:{}: {}",
files::to_slash(rel),
p.line,
p.message
));
}
}
if problems.is_empty() {
Ok(())
} else {
Err(Error::IgnoreDirective(problems.join("\n")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn scope_cli_files_bounds_to_the_scope_dir_relative_and_absolute() {
let tmp = tempdir().unwrap();
let cwd = tmp.path();
let dir = cwd.join("backend");
let abs_in = cwd.join("backend/x.rs"); let abs_out = cwd.join("other/y.rs"); let files = vec![
PathBuf::from("backend/svc.rs"), PathBuf::from("app.rs"), abs_in.clone(),
abs_out,
];
let kept = scope_cli_files(cwd, &dir, &files);
assert_eq!(kept, vec![PathBuf::from("backend/svc.rs"), abs_in]);
}
#[test]
fn scope_cli_files_ancestor_scope_keeps_all_files_under_cwd() {
let tmp = tempdir().unwrap();
let cwd = tmp.path();
let files = vec![PathBuf::from("a.rs"), PathBuf::from("sub/b.rs")];
assert_eq!(scope_cli_files(cwd, cwd, &files), files);
}
}