use crate::walk::{SKIP_DIRS, SOURCE_EXT};
#[must_use]
pub fn is_within_scope(rel: &str, scope: &[String]) -> bool {
scope
.iter()
.any(|d| d == "." || rel == d || rel.starts_with(&format!("{d}/")))
}
fn dir_of(rel: &str) -> &str {
match rel.rfind('/') {
Some(i) => &rel[..i],
None => "",
}
}
fn name_of(rel: &str) -> &str {
match rel.rfind('/') {
Some(i) => &rel[i + 1..],
None => rel,
}
}
fn has_skipped_component(dir: &str) -> bool {
dir.split('/').any(|part| SKIP_DIRS.contains(&part))
}
fn extension_of(name: &str) -> &str {
match name.rfind('.') {
Some(i) => &name[i..],
None => "",
}
}
#[must_use]
pub fn is_indexed_path(rel: &str, scope: &[String]) -> bool {
if rel.is_empty() || !is_within_scope(rel, scope) {
return false;
}
if has_skipped_component(dir_of(rel)) {
return false;
}
SOURCE_EXT.contains(&extension_of(name_of(rel)))
}
#[must_use]
pub fn ancestor_dirs_of_scope(scope: &[String]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for d in scope {
if d == "." {
continue;
}
let mut cur = d.as_str();
while let Some(i) = cur.rfind('/') {
let parent = &cur[..i];
out.push(parent.to_string());
cur = parent;
}
out.push(String::new());
}
out.sort();
out.dedup();
out
}
#[must_use]
pub fn is_config_input(rel: &str, scope: &[String]) -> bool {
let name = name_of(rel);
let dir = dir_of(rel);
let in_scope = is_within_scope(rel, scope) && !has_skipped_component(dir);
if name == "Cargo.toml" {
return in_scope;
}
if !(name.ends_with(".csproj") || name == "Directory.Build.props") {
return false;
}
in_scope || ancestor_dirs_of_scope(scope).iter().any(|a| a == dir)
}
#[must_use]
pub fn is_relevant(rel: &str, scope: &[String]) -> bool {
is_indexed_path(rel, scope) || is_config_input(rel, scope)
}
#[must_use]
pub fn config_pathspecs(scope: &[String]) -> Vec<String> {
let mut out = Vec::new();
for dir in ancestor_dirs_of_scope(scope) {
let prefix = if dir.is_empty() {
String::new()
} else {
format!("{dir}/")
};
out.push(format!(":(literal){prefix}Directory.Build.props"));
out.push(format!(":(glob){prefix}*.csproj"));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn scope(v: &[&str]) -> Vec<String> {
v.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn indexed_paths_follow_scope_skip_dirs_and_extensions() {
let s = scope(&["src"]);
assert!(is_indexed_path("src/a.cs", &s));
assert!(is_indexed_path("src/deep/a.ts", &s));
assert!(!is_indexed_path("other/a.cs", &s), "outside the scope");
assert!(
!is_indexed_path("src/notes.txt", &s),
"not a source extension"
);
assert!(
!is_indexed_path("src/bin/a.cs", &s),
"a skipped directory is never indexed"
);
assert!(is_indexed_path("anything/a.cs", &scope(&["."])));
}
#[test]
fn angular_templates_ac_8_a_template_path_is_indexed() {
assert!(is_indexed_path(
"src/app/card.component.html",
&scope(&["src"])
));
assert!(!is_indexed_path(
"src/app/card.component.htm",
&scope(&["src"])
));
}
#[test]
fn a_file_with_no_extension_is_never_indexed() {
assert!(!is_indexed_path(".gitignore", &scope(&["."])));
}
#[test]
fn config_inputs_cover_scope_and_ancestors() {
let s = scope(&["src"]);
assert!(is_config_input("src/App.csproj", &s));
assert!(is_config_input("App.csproj", &s), "root is an ancestor dir");
assert!(is_config_input("Directory.Build.props", &s));
assert!(
!is_config_input("vendor/App.csproj", &s),
"a sibling directory is neither in scope nor an ancestor"
);
assert!(
!is_config_input("vendor/nested/App.csproj", &s),
"the ancestor scan is non-recursive: only a direct child counts"
);
assert!(
!is_config_input("src/App.props", &s),
"only that exact props file"
);
}
#[test]
fn cargo_manifests_count_in_scope_only() {
let s = scope(&["src"]);
assert!(is_config_input("src/Cargo.toml", &s));
assert!(is_config_input("src/member/Cargo.toml", &s));
assert!(
!is_config_input("Cargo.toml", &s),
"crate discovery never scans ancestors"
);
}
#[test]
fn a_root_scope_covers_the_whole_repo_with_no_ancestor_specs() {
let s = scope(&["."]);
assert!(is_config_input("Cargo.toml", &s));
assert!(is_config_input("deep/App.csproj", &s));
assert!(config_pathspecs(&s).is_empty());
}
#[test]
fn config_pathspecs_name_each_ancestor_directory() {
let specs = config_pathspecs(&scope(&["a/b"]));
assert!(specs.contains(&":(literal)a/Directory.Build.props".to_string()));
assert!(specs.contains(&":(glob)a/*.csproj".to_string()));
assert!(specs.contains(&":(literal)Directory.Build.props".to_string()));
assert!(specs.contains(&":(glob)*.csproj".to_string()));
}
}