Skip to main content

stern4rust/
package_tree.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use std::collections::BTreeMap;
6use std::path::Path;
7use std::path::PathBuf;
8
9use crate::source_file::SourceFile;
10
11// The walked files arranged as the directories they sit in.
12//
13// A registry declares the files beside it and the folders directly beneath it,
14// so the question "is this file reached" can only be asked of a directory as a
15// whole. This is that shape: every directory the run saw, the files in it, and
16// which of those files is a registry.
17//
18// `main.rs` counts as a registry alongside `lib.rs`. It is an entry point
19// rather than an index and legitimately holds code, but it may still declare
20// modules -- and a file declared only from `main.rs` is reached. Treating the
21// registries of a directory as one set is what keeps that from being reported
22// as an orphan.
23pub struct PackageTree {
24    directories: BTreeMap<PathBuf, Vec<PathBuf>>,
25}
26
27impl PackageTree {
28    pub const REGISTRY_NAMES: [&'static str; 4] = ["all_tests.rs", "lib.rs", "main.rs", "mod.rs"];
29
30    pub fn of(files: &[SourceFile]) -> Self {
31        let mut directories: BTreeMap<PathBuf, Vec<PathBuf>> = BTreeMap::new();
32        for file in files {
33            let path = PathBuf::from(file.relative_path().replace('\\', "/"));
34            let parent = path.parent().unwrap_or(Path::new("")).to_path_buf();
35            directories.entry(parent).or_default().push(path);
36        }
37        Self { directories }
38    }
39
40    pub fn directories(&self) -> Vec<&Path> {
41        self.directories.keys().map(PathBuf::as_path).collect()
42    }
43
44    // The registries of one directory, in REGISTRY_NAMES order, so the offence
45    // lands on `lib.rs` rather than `main.rs` when a package has both.
46    pub fn registries_in(&self, directory: &Path) -> Vec<&Path> {
47        let mut found: Vec<&Path> = self
48            .files_in(directory)
49            .into_iter()
50            .filter(|path| Self::is_registry(path))
51            .collect();
52        found.sort_by_key(|path| Self::registry_rank(path));
53        found
54    }
55
56    // What the registries of this directory have to declare for everything
57    // beside them to be compiled: each sibling module file, and each subfolder
58    // that is itself a module.
59    pub fn expected_modules_in(&self, directory: &Path) -> Vec<String> {
60        let mut expected: Vec<String> = self
61            .files_in(directory)
62            .into_iter()
63            .filter(|path| !Self::is_registry(path))
64            .filter_map(Self::module_name)
65            .collect();
66        expected.extend(self.submodules_of(directory));
67        expected.sort();
68        expected
69    }
70
71    fn files_in(&self, directory: &Path) -> Vec<&Path> {
72        self.directories
73            .get(directory)
74            .map(|paths| paths.iter().map(PathBuf::as_path).collect())
75            .unwrap_or_default()
76    }
77
78    // A subfolder is a module only if it has a registry of its own. One without
79    // is tests-layout's finding, and reporting it here as undeclared would
80    // instruct the reader to declare a folder that cannot be declared yet.
81    fn submodules_of(&self, directory: &Path) -> Vec<String> {
82        self.directories
83            .keys()
84            .filter(|candidate| candidate.parent() == Some(directory))
85            .filter(|candidate| !self.registries_in(candidate).is_empty())
86            .filter_map(|candidate| Self::directory_name(candidate))
87            .collect()
88    }
89
90    fn directory_name(path: &Path) -> Option<String> {
91        path.file_name()
92            .and_then(|name| name.to_str())
93            .map(str::to_string)
94    }
95
96    fn is_registry(path: &Path) -> bool {
97        path.file_name()
98            .and_then(|name| name.to_str())
99            .is_some_and(|name| Self::REGISTRY_NAMES.contains(&name))
100    }
101
102    fn module_name(path: &Path) -> Option<String> {
103        path.file_stem()
104            .and_then(|stem| stem.to_str())
105            .map(str::to_string)
106    }
107
108    fn registry_rank(path: &Path) -> usize {
109        path.file_name()
110            .and_then(|name| name.to_str())
111            .and_then(|name| Self::REGISTRY_NAMES.iter().position(|known| *known == name))
112            .unwrap_or(usize::MAX)
113    }
114}