heddle_object_model/object/
staleness_core.rs1use std::path::Path;
5
6use super::{Annotation, AnnotationScope, ContentHash};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum StalenessStatus {
11 Fresh,
13 SourceChanged {
15 old_hash: ContentHash,
16 new_hash: ContentHash,
17 },
18 FileMissing,
20 SymbolMissing { symbol: String },
22 SymbolAmbiguous {
26 symbol: String,
27 candidate_lines: Vec<(u32, u32)>,
28 },
29 AmbiguousFileMove { candidate_paths: Vec<String> },
31 Unknown,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum SymbolResolution {
38 Resolved { start: u32, end: u32 },
40 Missing,
42 Ambiguous { candidate_lines: Vec<(u32, u32)> },
44}
45
46pub fn annotation_status_for_source(
48 annotation: &Annotation,
49 scope: &AnnotationScope,
50 source: &[u8],
51 file_path: &Path,
52) -> StalenessStatus {
53 annotation_status_for_source_with_symbol_resolver(
54 annotation,
55 scope,
56 source,
57 file_path,
58 resolve_current_symbol,
59 )
60}
61
62pub fn annotation_status_for_source_with_symbol_resolver(
64 annotation: &Annotation,
65 scope: &AnnotationScope,
66 source: &[u8],
67 file_path: &Path,
68 mut resolve_symbol: impl FnMut(&[u8], &Path, &str, Option<(u32, u32)>) -> SymbolResolution,
69) -> StalenessStatus {
70 let Some(revision) = annotation.current_revision() else {
71 return StalenessStatus::Unknown;
72 };
73 let expected_hash = match &revision.source_hash {
74 Some(h) => h,
75 None => return StalenessStatus::Unknown,
76 };
77
78 let scoped_bytes = match scope {
79 AnnotationScope::File => source.to_vec(),
80 AnnotationScope::Lines(start, end) => extract_line_range(source, *start, *end),
81 AnnotationScope::Symbol {
82 name,
83 resolved_lines,
84 } => match resolve_symbol(source, file_path, name, *resolved_lines) {
85 SymbolResolution::Resolved { start, end } => extract_line_range(source, start, end),
86 SymbolResolution::Missing => {
87 return StalenessStatus::SymbolMissing {
88 symbol: name.clone(),
89 };
90 }
91 SymbolResolution::Ambiguous { candidate_lines } => {
92 return StalenessStatus::SymbolAmbiguous {
93 symbol: name.clone(),
94 candidate_lines,
95 };
96 }
97 },
98 };
99
100 let current_hash = ContentHash::compute(&scoped_bytes);
101 if current_hash == *expected_hash {
102 StalenessStatus::Fresh
103 } else {
104 StalenessStatus::SourceChanged {
105 old_hash: *expected_hash,
106 new_hash: current_hash,
107 }
108 }
109}
110
111pub fn extract_line_range(source: &[u8], start: u32, end: u32) -> Vec<u8> {
116 let text = std::str::from_utf8(source).unwrap_or("");
117 let lines: Vec<&str> = text.lines().collect();
118 let start_idx = (start as usize).saturating_sub(1);
119 let end_idx = (end as usize).min(lines.len());
120 if start_idx >= end_idx {
121 return Vec::new();
122 }
123 lines[start_idx..end_idx].join("\n").into_bytes()
124}
125
126pub fn resolve_current_symbol(
131 _source: &[u8],
132 _file_path: &Path,
133 _symbol: &str,
134 stored: Option<(u32, u32)>,
135) -> SymbolResolution {
136 match stored {
137 Some((start, end)) => SymbolResolution::Resolved { start, end },
138 None => SymbolResolution::Missing,
139 }
140}