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}