cargo_e/
e_discovery.rs

1// src/e_discovery.rs
2use std::{
3    fs,
4    fs::File,
5    io::{self, BufRead, BufReader},
6    path::{Path, PathBuf},
7};
8
9use crate::e_target::{CargoTarget, TargetKind};
10use anyhow::{anyhow, Context, Result};
11
12pub fn scan_tests_directory(manifest_path: &Path) -> Result<Vec<String>> {
13    // Determine the project root from the manifest's parent directory.
14    let project_root = manifest_path
15        .parent()
16        .ok_or_else(|| anyhow!("Unable to determine project root from manifest"))?;
17
18    // Construct the path to the tests directory.
19    let tests_dir = project_root.join("tests");
20    let mut tests = Vec::new();
21
22    // Only scan if the tests directory exists and is a directory.
23    if tests_dir.exists() && tests_dir.is_dir() {
24        for entry in fs::read_dir(tests_dir)? {
25            let entry = entry?;
26            let path = entry.path();
27            // Only consider files with a `.rs` extension.
28            if path.is_file() {
29                if let Some(ext) = path.extension() {
30                    if ext == "rs" {
31                        if let Some(stem) = path.file_stem() {
32                            tests.push(stem.to_string_lossy().to_string());
33                        }
34                    }
35                }
36            }
37        }
38    }
39
40    Ok(tests)
41}
42
43pub fn scan_examples_directory(
44    manifest_path: &Path,
45    examples_folder: &str,
46) -> Result<Vec<CargoTarget>> {
47    // Determine the project root from the manifest's parent directory.
48    let project_root = manifest_path
49        .parent()
50        .ok_or_else(|| anyhow::anyhow!("Unable to determine project root"))?;
51    let examples_dir = project_root.join(examples_folder);
52    let mut targets = Vec::new();
53
54    if examples_dir.exists() && examples_dir.is_dir() {
55        for entry in fs::read_dir(&examples_dir)
56            .with_context(|| format!("Reading directory {:?}", examples_dir))?
57        {
58            let entry = entry?;
59            let path = entry.path();
60            if path.is_file() {
61                // Assume that any .rs file in examples/ is an example.
62                if let Some(ext) = path.extension() {
63                    if ext == "rs" {
64                        if let Some(stem) = path.file_stem() {
65                            if let Some(target) = CargoTarget::from_source_file(
66                                stem,
67                                &path,
68                                manifest_path,
69                                true,
70                                false,
71                            ) {
72                                targets.push(target);
73                            }
74                        }
75                    }
76                }
77            } else if path.is_dir() {
78                if let Some(target) = CargoTarget::from_folder(&path, &manifest_path, true, true) {
79                    if target.kind == TargetKind::Unknown {
80                        continue;
81                    }
82                    targets.push(target);
83                }
84            }
85        }
86    }
87
88    Ok(targets)
89}
90
91/// Try to detect a “script” kind by reading *one* first line.
92/// Returns Ok(Some(...)) if it matches either marker, Ok(None) otherwise.
93/// Any I/O error is propagated.
94fn detect_script_kind(path: &Path) -> io::Result<Option<TargetKind>> {
95    let file = File::open(path)?;
96    let mut reader = BufReader::new(file);
97    let mut first_line = String::new();
98    reader.read_line(&mut first_line)?;
99
100    // must start with `#`
101    if !first_line.starts_with('#') {
102        return Ok(None);
103    }
104    // now check your two markers
105    if first_line.contains("scriptisto") {
106        return Ok(Some(TargetKind::ScriptScriptisto));
107    }
108    if first_line.contains("rust-script") {
109        return Ok(Some(TargetKind::ScriptRustScript));
110    }
111    Ok(None)
112}
113
114/// Determines the target kind and (optionally) an updated manifest path based on:
115/// - Tauri configuration: If the parent directory of the original manifest contains a
116///   "tauri.conf.json", and also a Cargo.toml exists in that same directory, then update the manifest path
117///   and return ManifestTauri.
118/// - Dioxus markers: If the file contents contain any Dioxus markers, return either ManifestDioxusExample
119///   (if `example` is true) or ManifestDioxus.
120/// - Otherwise, if the file contains "fn main", decide based on the candidate's parent folder name.
121///   If the parent is "examples" (or "bin"), return the corresponding Example/Binary (or extended variant).
122/// - If none of these conditions match, return Example as a fallback.
123///
124/// Returns a tuple of (TargetKind, updated_manifest_path).
125pub fn determine_target_kind_and_manifest(
126    manifest_path: &Path,
127    candidate: &Path,
128    file_contents: &str,
129    example: bool,
130    extended: bool,
131    _toml_specified: bool,
132    incoming_kind: Option<TargetKind>,
133) -> (TargetKind, PathBuf) {
134    // Start with the original manifest path.
135    let mut new_manifest = manifest_path.to_path_buf();
136
137    if let Ok(Some(script_kind)) = detect_script_kind(candidate) {
138        return (script_kind, new_manifest);
139    }
140    // If the incoming kind is already known (Test or Bench), return it.
141    if let Some(kind) = incoming_kind {
142        if kind == TargetKind::Test || kind == TargetKind::Bench {
143            return (kind, new_manifest);
144        }
145    }
146    // Tauri detection: check if the manifest's parent or candidate's parent contains tauri config.
147    let tauri_detected = manifest_path
148        .parent()
149        .and_then(|p| p.file_name())
150        .map(|s| s.to_string_lossy().eq_ignore_ascii_case("src-tauri"))
151        .unwrap_or(false)
152        || manifest_path
153            .parent()
154            .map(|p| p.join("tauri.conf.json"))
155            .map_or(false, |p| p.exists())
156        || manifest_path
157            .parent()
158            .map(|p| p.join("src-tauri"))
159            .map_or(false, |p| p.exists())
160        || candidate
161            .parent()
162            .map(|p| p.join("tauri.conf.json"))
163            .map_or(false, |p| p.exists());
164
165    // println!(
166    //     "{} {} {} {}",
167    //     manifest_path.display(),
168    //     candidate.display(),
169    //     tauri_detected,
170    //     toml_specified
171    // );
172    if tauri_detected {
173        if example {
174            return (TargetKind::ManifestTauriExample, new_manifest);
175        }
176        // If the candidate's parent contains tauri.conf.json, update the manifest path if there's a Cargo.toml there.
177        if let Some(candidate_parent) = candidate.parent() {
178            let candidate_manifest = candidate_parent.join("Cargo.toml");
179            if candidate_manifest.exists() {
180                new_manifest = candidate_manifest;
181            }
182        }
183        return (TargetKind::ManifestTauri, new_manifest);
184    }
185
186    // Dioxus detection
187    if file_contents.contains("dioxus::") {
188        let kind = if example {
189            TargetKind::ManifestDioxusExample
190        } else {
191            TargetKind::ManifestDioxus
192        };
193        return (kind, new_manifest);
194    }
195
196    // leptos detection
197    if file_contents.contains("leptos::") {
198        return (TargetKind::ManifestLeptos, new_manifest);
199    }
200
201    // Check if the file contains "fn main"
202    if file_contents.contains("fn main") {
203        let kind = if example {
204            if extended {
205                TargetKind::ExtendedExample
206            } else {
207                TargetKind::Example
208            }
209        } else if extended {
210            TargetKind::ExtendedBinary
211        } else {
212            TargetKind::Binary
213        };
214        return (kind, new_manifest);
215    }
216    // Check if the file contains a #[test] attribute; if so, mark it as a test.
217    if file_contents.contains("#[test]") {
218        return (TargetKind::Test, new_manifest);
219    }
220
221    let kind = if example {
222        if extended {
223            TargetKind::UnknownExtendedExample
224        } else {
225            TargetKind::UnknownExample
226        }
227    } else if extended {
228        TargetKind::UnknownExtendedBinary
229    } else {
230        TargetKind::UnknownBinary
231    };
232    (kind, new_manifest)
233    // Default fallback.
234    // (TargetKind::Unknown, "errorNOfnMAIN".into())
235}
236
237/// Returns true if the candidate file is not located directly in the project root.
238pub fn is_extended_target(manifest_path: &Path, candidate: &Path) -> bool {
239    if let Some(project_root) = manifest_path.parent() {
240        // If the candidate's parent is not the project root, it's nested (i.e. extended).
241        candidate
242            .parent()
243            .map(|p| p != project_root)
244            .unwrap_or(false)
245    } else {
246        false
247    }
248}
249
250// #[cfg(test)]
251// mod tests {
252//     use super::*;
253//     use std::fs;
254//     use tempfile::tempdir;
255
256//     #[test]
257//     fn test_discover_targets_no_manifest() {
258//         let temp = tempdir().unwrap();
259//         // With no Cargo.toml, we expect an empty list.
260//         let targets = discover_targets(temp.path()).unwrap();
261//         assert!(targets.is_empty());
262//     }
263
264//     #[test]
265//     fn test_discover_targets_with_manifest_and_example() {
266//         let temp = tempdir().unwrap();
267//         // Create a dummy Cargo.toml.
268//         let manifest_path = temp.path().join("Cargo.toml");
269//         fs::write(&manifest_path, "[package]\nname = \"dummy\"\n").unwrap();
270
271//         // Create an examples directory with a dummy example file.
272//         let examples_dir = temp.path().join("examples");
273//         fs::create_dir(&examples_dir).unwrap();
274//         let example_file = examples_dir.join("example1.rs");
275//         fs::write(&example_file, "fn main() {}").unwrap();
276
277//         let targets = discover_targets(temp.path()).unwrap();
278//         // Expect at least two targets: one for the manifest and one for the example.
279//         assert!(targets.len() >= 2);
280
281//         let example_target = targets
282//             .iter()
283//             .find(|t| t.kind == TargetKind::Example && t.name == "example1");
284//         assert!(example_target.is_some());
285//     }
286// }