Skip to main content

codehelion_core/discovery/
source_unit.rs

1//! The unit of discovery: one physical source file.
2
3use std::fmt;
4use std::path::PathBuf;
5use std::sync::Arc;
6
7use super::language::Language;
8
9/// A stable content fingerprint of a file.
10///
11/// This hashes bytes only; it does not depend on line numbers, AST node ids or
12/// any other position-derived value, so it is stable across reformatting that
13/// leaves bytes unchanged and identical for two files with identical content.
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub struct ContentHash(String);
16
17impl ContentHash {
18    /// Hash the given bytes.
19    #[must_use]
20    pub fn of(bytes: &[u8]) -> Self {
21        Self(blake3::hash(bytes).to_hex().to_string())
22    }
23
24    /// The hash as a lowercase hex string.
25    #[must_use]
26    pub fn as_str(&self) -> &str {
27        &self.0
28    }
29
30    /// Take back a hash an earlier scan recorded.
31    ///
32    /// Nothing is re-derived, so this cannot tell whether the string is the
33    /// hash of anything. It exists so a stored fingerprint can be compared
34    /// with a freshly computed one, which is the only thing either is for.
35    #[must_use]
36    pub const fn from_recorded(hex: String) -> Self {
37        Self(hex)
38    }
39}
40
41impl fmt::Display for ContentHash {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.write_str(&self.0)
44    }
45}
46
47/// The role a source file plays in its package.
48///
49/// For Rust this is derived from the Cargo manifest and layout conventions; it
50/// lets later stages down-weight test and example code without discarding it.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum TargetKind {
53    /// Library sources (`src/lib.rs` and the module tree below it).
54    Library,
55    /// Binary sources (`src/main.rs`, `src/bin/`, or an explicit `[[bin]]`).
56    Binary,
57    /// Integration tests (`tests/`).
58    Test,
59    /// Benchmarks (`benches/`).
60    Bench,
61    /// Examples (`examples/`).
62    Example,
63    /// A Cargo build script (`build.rs`).
64    BuildScript,
65    /// Role could not be determined (for example, a C/C++ file outside any
66    /// recognised package layout).
67    Unknown,
68}
69
70impl TargetKind {
71    /// Stable lowercase identifier used in reports.
72    #[must_use]
73    pub const fn name(self) -> &'static str {
74        match self {
75            Self::Library => "library",
76            Self::Binary => "binary",
77            Self::Test => "test",
78            Self::Bench => "bench",
79            Self::Example => "example",
80            Self::BuildScript => "build-script",
81            Self::Unknown => "unknown",
82        }
83    }
84}
85
86/// One physical source file selected for analysis.
87///
88/// A file appears at most once: uniqueness is keyed on its normalized path, so
89/// a header shared by several translation units is registered a single time.
90/// [`content_hash`](Self::content_hash) additionally identifies byte-identical
91/// copies at different paths.
92#[derive(Debug, Clone)]
93pub struct SourceUnit {
94    /// Path relative to the scan root, used for display and stable ordering.
95    pub relative_path: PathBuf,
96    /// Absolute path, used to read the file.
97    pub absolute_path: PathBuf,
98    /// Detected language.
99    pub language: Language,
100    /// Whether the file is a header rather than a translation unit.
101    pub is_header: bool,
102    /// Content fingerprint.
103    pub content_hash: ContentHash,
104    /// Exact bytes whose hash was recorded and which frontends must analyse.
105    ///
106    /// Keeping this shared allocation prevents a second filesystem read after
107    /// discovery, so a source edit cannot make the stored hash describe
108    /// different bytes from the parsed program.
109    pub source_bytes: Arc<[u8]>,
110    /// File size in bytes.
111    pub byte_len: u64,
112    /// Owning Cargo package, when the file sits inside one.
113    pub package: Option<String>,
114    /// The crate a compiler knows this file by, when the package layout says
115    /// which crate that is.
116    ///
117    /// Not the package: one package holds a library, its binaries and a crate
118    /// per test file, and a compiler is asked about one of those rather than
119    /// about the package they share.
120    pub crate_name: Option<String>,
121    /// Role of the file in its package.
122    pub target_kind: TargetKind,
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn identical_bytes_hash_equal_and_differ_from_others() {
131        let a = ContentHash::of(b"fn main() {}");
132        let b = ContentHash::of(b"fn main() {}");
133        let c = ContentHash::of(b"fn main() { }");
134        assert_eq!(a, b);
135        assert_ne!(a, c);
136        assert_eq!(a.as_str().len(), 64);
137    }
138
139    #[test]
140    fn target_kind_names_are_stable() {
141        assert_eq!(TargetKind::Library.name(), "library");
142        assert_eq!(TargetKind::BuildScript.name(), "build-script");
143        assert_eq!(TargetKind::Unknown.name(), "unknown");
144    }
145}