Skip to main content

fallow_types/
discover.rs

1//! File discovery types: discovered files, file IDs, and entry points.
2
3use std::path::PathBuf;
4
5/// A discovered source file on disk.
6///
7/// # Examples
8///
9/// ```
10/// use fallow_types::discover::{DiscoveredFile, FileId};
11/// use std::path::PathBuf;
12///
13/// let file = DiscoveredFile {
14///     id: FileId(0),
15///     path: PathBuf::from("/project/src/index.ts"),
16///     size_bytes: 2048,
17/// };
18/// assert_eq!(file.id, FileId(0));
19/// assert_eq!(file.size_bytes, 2048);
20/// ```
21#[derive(Debug, Clone)]
22pub struct DiscoveredFile {
23    /// Unique file index.
24    pub id: FileId,
25    /// Absolute path.
26    pub path: PathBuf,
27    /// File size in bytes (for sorting largest-first).
28    pub size_bytes: u64,
29}
30
31/// Compact file identifier.
32///
33/// A newtype wrapper around `u32` used as a stable index into file arrays.
34/// `FileId`s are path-sorted (not insertion order) for stable cross-run identity.
35///
36/// # Examples
37///
38/// ```
39/// use fallow_types::discover::FileId;
40///
41/// let id = FileId(42);
42/// assert_eq!(id.0, 42);
43///
44/// // Implements Copy
45/// let copy = id;
46/// assert_eq!(id, copy);
47/// ```
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct FileId(pub u32);
50
51// Size assertions to prevent memory regressions in hot-path types.
52// These types are stored in large Vecs (one per project file) and iterated
53// in tight loops during discovery, parsing, and graph construction.
54const _: () = assert!(std::mem::size_of::<FileId>() == 4);
55#[cfg(all(target_pointer_width = "64", unix))]
56const _: () = assert!(std::mem::size_of::<DiscoveredFile>() == 40);
57
58/// An entry point into the module graph.
59#[derive(Debug, Clone)]
60pub struct EntryPoint {
61    /// Absolute path to the entry point file.
62    pub path: PathBuf,
63    /// How this entry point was discovered.
64    pub source: EntryPointSource,
65}
66
67/// Where an entry point was discovered from.
68#[derive(Debug, Clone)]
69pub enum EntryPointSource {
70    /// The `main` field in package.json.
71    PackageJsonMain,
72    /// The `module` field in package.json.
73    PackageJsonModule,
74    /// The `exports` field in package.json.
75    PackageJsonExports,
76    /// The `bin` field in package.json.
77    PackageJsonBin,
78    /// A script command in package.json.
79    PackageJsonScript,
80    /// Detected by a framework plugin.
81    Plugin {
82        /// Name of the plugin that detected this entry point.
83        name: String,
84    },
85    /// A test file (e.g., `*.test.ts`, `*.spec.ts`).
86    TestFile,
87    /// A default index file (e.g., `src/index.ts`).
88    DefaultIndex,
89    /// Manually configured in fallow config.
90    ManualEntry,
91    /// Discovered from infrastructure config files (Dockerfile, Procfile, fly.toml).
92    InfrastructureConfig,
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use std::collections::hash_map::DefaultHasher;
99    use std::hash::{Hash, Hasher};
100
101    // ── FileId ──────────────────────────────────────────────────────
102
103    #[test]
104    fn file_id_equality() {
105        assert_eq!(FileId(0), FileId(0));
106        assert_eq!(FileId(42), FileId(42));
107        assert_ne!(FileId(0), FileId(1));
108    }
109
110    #[test]
111    fn file_id_copy_semantics() {
112        let a = FileId(5);
113        let b = a; // Copy, not move
114        assert_eq!(a, b);
115    }
116
117    #[test]
118    fn file_id_hash_consistent() {
119        let id = FileId(99);
120        let hash1 = {
121            let mut h = DefaultHasher::new();
122            id.hash(&mut h);
123            h.finish()
124        };
125        let hash2 = {
126            let mut h = DefaultHasher::new();
127            id.hash(&mut h);
128            h.finish()
129        };
130        assert_eq!(hash1, hash2);
131    }
132
133    #[test]
134    fn file_id_equal_values_same_hash() {
135        let a = FileId(7);
136        let b = FileId(7);
137        let hash_a = {
138            let mut h = DefaultHasher::new();
139            a.hash(&mut h);
140            h.finish()
141        };
142        let hash_b = {
143            let mut h = DefaultHasher::new();
144            b.hash(&mut h);
145            h.finish()
146        };
147        assert_eq!(hash_a, hash_b);
148    }
149
150    #[test]
151    fn file_id_inner_value_accessible() {
152        let id = FileId(123);
153        assert_eq!(id.0, 123);
154    }
155
156    #[test]
157    fn file_id_debug_format() {
158        let id = FileId(42);
159        let debug = format!("{id:?}");
160        assert!(
161            debug.contains("42"),
162            "Debug should show inner value: {debug}"
163        );
164    }
165
166    // ── DiscoveredFile ──────────────────────────────────────────────
167
168    #[test]
169    fn discovered_file_clone() {
170        let original = DiscoveredFile {
171            id: FileId(0),
172            path: PathBuf::from("/project/src/index.ts"),
173            size_bytes: 1024,
174        };
175        let cloned = original.clone();
176        assert_eq!(cloned.id, original.id);
177        assert_eq!(cloned.path, original.path);
178        assert_eq!(cloned.size_bytes, original.size_bytes);
179    }
180
181    #[test]
182    fn discovered_file_zero_size() {
183        let file = DiscoveredFile {
184            id: FileId(0),
185            path: PathBuf::from("/empty.ts"),
186            size_bytes: 0,
187        };
188        assert_eq!(file.size_bytes, 0);
189    }
190
191    #[test]
192    fn discovered_file_large_size() {
193        let file = DiscoveredFile {
194            id: FileId(0),
195            path: PathBuf::from("/large.ts"),
196            size_bytes: u64::MAX,
197        };
198        assert_eq!(file.size_bytes, u64::MAX);
199    }
200
201    // ── EntryPoint ──────────────────────────────────────────────────
202
203    #[test]
204    fn entry_point_clone() {
205        let ep = EntryPoint {
206            path: PathBuf::from("/project/src/main.ts"),
207            source: EntryPointSource::PackageJsonMain,
208        };
209        let cloned = ep.clone();
210        assert_eq!(cloned.path, ep.path);
211        assert!(matches!(cloned.source, EntryPointSource::PackageJsonMain));
212    }
213
214    // ── EntryPointSource ────────────────────────────────────────────
215
216    #[test]
217    fn entry_point_source_all_variants_constructible() {
218        // Verify all variants can be constructed (compile-time coverage)
219        let _ = EntryPointSource::PackageJsonMain;
220        let _ = EntryPointSource::PackageJsonModule;
221        let _ = EntryPointSource::PackageJsonExports;
222        let _ = EntryPointSource::PackageJsonBin;
223        let _ = EntryPointSource::PackageJsonScript;
224        let _ = EntryPointSource::Plugin {
225            name: "next".to_string(),
226        };
227        let _ = EntryPointSource::TestFile;
228        let _ = EntryPointSource::DefaultIndex;
229        let _ = EntryPointSource::ManualEntry;
230        let _ = EntryPointSource::InfrastructureConfig;
231    }
232
233    #[test]
234    fn entry_point_source_plugin_preserves_name() {
235        let source = EntryPointSource::Plugin {
236            name: "vitest".to_string(),
237        };
238        match source {
239            EntryPointSource::Plugin { name } => assert_eq!(name, "vitest"),
240            _ => panic!("expected Plugin variant"),
241        }
242    }
243
244    #[test]
245    fn entry_point_source_plugin_clone_preserves_name() {
246        let source = EntryPointSource::Plugin {
247            name: "storybook".to_string(),
248        };
249        // Use source after clone to verify both copies are valid
250        let cloned = source.clone();
251        // Verify original is still usable
252        assert!(matches!(&source, EntryPointSource::Plugin { name } if name == "storybook"));
253        // Verify clone has the same data
254        match cloned {
255            EntryPointSource::Plugin { name } => assert_eq!(name, "storybook"),
256            _ => panic!("expected Plugin variant after clone"),
257        }
258    }
259
260    #[test]
261    fn entry_point_source_debug_format() {
262        let source = EntryPointSource::PackageJsonMain;
263        let debug = format!("{source:?}");
264        assert!(
265            debug.contains("PackageJsonMain"),
266            "Debug should name the variant: {debug}"
267        );
268
269        let plugin = EntryPointSource::Plugin {
270            name: "remix".to_string(),
271        };
272        let debug = format!("{plugin:?}");
273        assert!(
274            debug.contains("remix"),
275            "Debug should show plugin name: {debug}"
276        );
277    }
278}