Skip to main content

fallow_graph/
project.rs

1//! Centralized project state with file registry and workspace metadata.
2
3use fallow_config::WorkspaceInfo;
4
5use fallow_types::discover::{DiscoveredFile, FileId};
6
7/// Discovered files and workspace packages for one analysis run.
8///
9/// The files are indexed by their dense `FileId`.
10pub struct ProjectState {
11    files: Vec<DiscoveredFile>,
12    workspaces: Vec<WorkspaceInfo>,
13}
14
15impl ProjectState {
16    /// Build a new project state from discovered files and workspaces.
17    #[must_use]
18    pub fn new(files: Vec<DiscoveredFile>, workspaces: Vec<WorkspaceInfo>) -> Self {
19        debug_assert!(
20            files.iter().enumerate().all(|(i, f)| f.id.0 as usize == i),
21            "FileIds must be densely packed starting at 0"
22        );
23        Self { files, workspaces }
24    }
25
26    /// All discovered files, indexed by `FileId`.
27    #[must_use]
28    pub fn files(&self) -> &[DiscoveredFile] {
29        &self.files
30    }
31
32    /// All discovered workspace packages.
33    #[must_use]
34    pub fn workspaces(&self) -> &[WorkspaceInfo] {
35        &self.workspaces
36    }
37
38    /// Look up a file by its `FileId`.
39    #[must_use]
40    pub fn file_by_id(&self, id: FileId) -> Option<&DiscoveredFile> {
41        self.files.get(id.0 as usize)
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use std::path::PathBuf;
48
49    use super::*;
50
51    fn make_file(id: u32, path: &str) -> DiscoveredFile {
52        DiscoveredFile {
53            id: FileId(id),
54            path: PathBuf::from(path),
55            size_bytes: 100,
56        }
57    }
58
59    fn make_workspace(name: &str, root: &str) -> WorkspaceInfo {
60        WorkspaceInfo {
61            root: PathBuf::from(root),
62            name: name.to_string(),
63            is_internal_dependency: false,
64        }
65    }
66
67    #[test]
68    fn file_by_id_valid() {
69        let files = vec![
70            make_file(0, "/project/src/a.ts"),
71            make_file(1, "/project/src/b.ts"),
72        ];
73        let state = ProjectState::new(files, vec![]);
74        let file = state.file_by_id(FileId(0)).unwrap();
75        assert_eq!(file.path, PathBuf::from("/project/src/a.ts"));
76        assert_eq!(file.id, FileId(0));
77    }
78
79    #[test]
80    fn file_by_id_out_of_bounds() {
81        let files = vec![make_file(0, "/project/src/a.ts")];
82        let state = ProjectState::new(files, vec![]);
83        assert!(state.file_by_id(FileId(999)).is_none());
84    }
85
86    #[test]
87    fn empty_state() {
88        let state = ProjectState::new(vec![], vec![]);
89        assert!(state.files().is_empty());
90        assert!(state.workspaces().is_empty());
91        assert!(state.file_by_id(FileId(0)).is_none());
92    }
93
94    #[test]
95    fn files_returns_all_files() {
96        let files = vec![
97            make_file(0, "/project/src/a.ts"),
98            make_file(1, "/project/src/b.ts"),
99        ];
100        let state = ProjectState::new(files, vec![]);
101        assert_eq!(state.files().len(), 2);
102        assert_eq!(state.files()[0].id, FileId(0));
103        assert_eq!(state.files()[1].id, FileId(1));
104    }
105
106    #[test]
107    fn workspaces_returns_all_workspaces() {
108        let workspaces = vec![
109            make_workspace("a", "/project/packages/a"),
110            make_workspace("b", "/project/packages/b"),
111        ];
112        let state = ProjectState::new(vec![], workspaces);
113        assert_eq!(state.workspaces().len(), 2);
114    }
115}