Skip to main content

clapdocs/scanner/
find.rs

1use ignore::WalkBuilder;
2use std::fs;
3use std::path::Path;
4
5use super::datamodel::CargoToml;
6pub use super::datamodel::ScanOptions;
7pub use super::datamodel::Target;
8
9pub fn find_projects(options: &ScanOptions) -> Result<Vec<Target>, String> {
10    match options.readme_name {
11        Some(readme_name) => find_by_readme(options.root_dir, options.recursive, readme_name),
12        None => find_by_cargo(options.root_dir, options.recursive),
13    }
14}
15
16fn find_by_readme(
17    root_dir: &Path,
18    recursive: bool,
19    readme_name: &str,
20) -> Result<Vec<Target>, String> {
21    let readme_name_lower = readme_name.to_lowercase();
22
23    let walker = build_walker(root_dir, recursive);
24    let mut targets = Vec::new();
25
26    for entry in walker.flatten() {
27        let path = entry.path();
28
29        if !path.is_file() {
30            continue;
31        }
32
33        let file_name = path
34            .file_name()
35            .and_then(|n| n.to_str())
36            .unwrap_or_default();
37
38        if file_name.to_lowercase() != readme_name_lower {
39            continue;
40        }
41
42        let readme_path = path.to_path_buf();
43
44        match resolve_clap_project_from_readme(&readme_path) {
45            Some(target) => targets.push(target),
46            None => {
47                eprintln!(
48                    "  (!) {} has no parent clap project, skipping",
49                    readme_path.display()
50                );
51            }
52        }
53    }
54
55    if targets.is_empty() {
56        return Err(format!(
57            "No files named '{readme_name}' with a parent clap project found"
58        ));
59    }
60
61    Ok(targets)
62}
63
64fn find_by_cargo(root_dir: &Path, recursive: bool) -> Result<Vec<Target>, String> {
65    let walker = build_walker(root_dir, recursive);
66    let mut targets = Vec::new();
67
68    for entry in walker.flatten() {
69        let path = entry.path();
70
71        if !path.is_file() {
72            continue;
73        }
74
75        let file_name = path
76            .file_name()
77            .and_then(|n| n.to_str())
78            .unwrap_or_default();
79
80        if file_name != "Cargo.toml" {
81            continue;
82        }
83
84        let project_dir = match path.parent() {
85            Some(dir) => dir,
86            None => continue,
87        };
88
89        if let Some(target) = try_as_clap_project(project_dir, None) {
90            targets.push(target);
91        }
92    }
93
94    if targets.is_empty() {
95        return Err("No clap projects found".to_string());
96    }
97
98    Ok(targets)
99}
100
101fn build_walker(root_dir: &Path, recursive: bool) -> ignore::Walk {
102    let mut builder = WalkBuilder::new(root_dir);
103    builder.standard_filters(true).follow_links(true);
104
105    if !recursive {
106        builder.max_depth(Some(1));
107    }
108
109    builder.build()
110}
111
112fn resolve_clap_project_from_readme(readme_path: &Path) -> Option<Target> {
113    let mut search_dir = readme_path.parent()?;
114
115    loop {
116        let cargo_path = search_dir.join("Cargo.toml");
117
118        if cargo_path.is_file()
119            && let Some(target) = try_as_clap_project(search_dir, Some(readme_path))
120        {
121            return Some(target);
122        }
123
124        search_dir = search_dir.parent()?;
125    }
126}
127
128fn try_as_clap_project(project_dir: &Path, readme_path: Option<&Path>) -> Option<Target> {
129    let cargo_path = project_dir.join("Cargo.toml");
130    let content = fs::read_to_string(&cargo_path).ok()?;
131    let cargo_toml: CargoToml = toml::from_str(&content).ok()?;
132
133    let package_name = cargo_toml.package.as_ref()?.name.clone();
134
135    let has_clap = cargo_toml
136        .dependencies
137        .as_ref()
138        .and_then(|deps| deps.as_table())
139        .is_some_and(|deps| deps.contains_key("clap"));
140
141    if !has_clap {
142        return None;
143    }
144
145    Some(Target {
146        readme_path: readme_path.map(Path::to_path_buf),
147        project_path: project_dir.to_path_buf(),
148        name: package_name,
149    })
150}