code_moniker_workspace/linkage/change/
delta.rs1use std::path::PathBuf;
2
3use code_moniker_core::lang::build_manifest::Manifest;
4
5use crate::code::CodeIndexGraphDiff;
6use crate::snapshot::{ReferenceId, SourceId, SymbolId};
7
8#[derive(Clone, Debug, Default, Eq, PartialEq)]
9pub struct LinkageGraphDelta {
10 references: ReferenceDelta,
11 symbols: SymbolDelta,
12}
13
14#[derive(Clone, Debug, Default, Eq, PartialEq)]
15pub struct LinkageRefreshImpact {
16 scope: RefreshScope,
17 references: ReferenceDelta,
18 symbols: SymbolDelta,
19 precision: LinkageDiffPrecision,
20}
21
22#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
23enum LinkageDiffPrecision {
24 #[default]
25 SourceLevel,
26 Precise,
27}
28
29#[derive(Clone, Debug, Default, Eq, PartialEq)]
30pub(in crate::linkage) struct RefreshScope {
31 changed_sources: Vec<SourceId>,
32 changed_paths: Vec<PathBuf>,
33}
34
35#[derive(Clone, Debug, Default, Eq, PartialEq)]
36pub(in crate::linkage) enum ReferenceDelta {
37 #[default]
38 Unchanged,
39 Changed {
40 changed: Vec<ReferenceId>,
41 removed: Vec<ReferenceId>,
42 remapped: Vec<(ReferenceId, ReferenceId)>,
43 },
44}
45
46#[derive(Clone, Debug, Default, Eq, PartialEq)]
47pub(in crate::linkage) enum SymbolDelta {
48 #[default]
49 Unchanged,
50 AdditiveOnly {
51 added: Vec<SymbolId>,
52 },
53 RemovedOnly {
54 removed: Vec<SymbolId>,
55 retargeted_identities: Vec<String>,
56 },
57 Mixed {
58 candidate_changed: Vec<SymbolId>,
59 changed: Vec<SymbolId>,
60 retargeted_identities: Vec<String>,
61 },
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub(in crate::linkage) enum LinkageRefreshShape<'a> {
66 Empty,
67 SourceLevel,
68 ManifestPolicy,
69 AdditiveSymbolsOnly(&'a [SymbolId]),
70 RemovedSymbolsOnly(&'a [SymbolId]),
71 LinkageRelevant,
72}
73
74impl LinkageRefreshImpact {
75 pub fn new(changed_sources: Vec<SourceId>, changed_paths: Vec<PathBuf>) -> Self {
76 Self {
77 scope: RefreshScope::new(changed_sources, changed_paths),
78 references: ReferenceDelta::Unchanged,
79 symbols: SymbolDelta::Unchanged,
80 precision: LinkageDiffPrecision::SourceLevel,
81 }
82 }
83
84 pub fn with_graph_delta(
85 changed_sources: Vec<SourceId>,
86 changed_paths: Vec<PathBuf>,
87 graph_delta: LinkageGraphDelta,
88 ) -> Self {
89 Self {
90 scope: RefreshScope::new(changed_sources, changed_paths),
91 references: graph_delta.references,
92 symbols: graph_delta.symbols,
93 precision: LinkageDiffPrecision::Precise,
94 }
95 }
96
97 pub fn is_empty(&self) -> bool {
98 self.scope.is_empty()
99 && self.references.is_empty()
100 && symbol_delta_is_unchanged(&self.symbols)
101 }
102
103 pub(in crate::linkage) fn shape(&self) -> LinkageRefreshShape<'_> {
104 classify_refresh_shape(self)
105 }
106
107 pub(in crate::linkage) fn changed_sources(&self) -> &[SourceId] {
108 self.scope.changed_sources()
109 }
110
111 pub(in crate::linkage) fn changed_paths(&self) -> &[PathBuf] {
112 self.scope.changed_paths()
113 }
114
115 pub(in crate::linkage) fn has_precise_graph_diff(&self) -> bool {
116 self.precision == LinkageDiffPrecision::Precise
117 }
118
119 pub(in crate::linkage) fn references(&self) -> &ReferenceDelta {
120 &self.references
121 }
122
123 pub(in crate::linkage) fn definitions(&self) -> &SymbolDelta {
124 &self.symbols
125 }
126}
127
128impl LinkageGraphDelta {
129 pub fn from_code_index(graph_diff: CodeIndexGraphDiff) -> Self {
130 Self {
131 references: ReferenceDelta::from_code_index(&graph_diff),
132 symbols: SymbolDelta::from_code_index(graph_diff),
133 }
134 }
135}
136
137impl From<CodeIndexGraphDiff> for LinkageGraphDelta {
138 fn from(graph_diff: CodeIndexGraphDiff) -> Self {
139 Self::from_code_index(graph_diff)
140 }
141}
142
143fn classify_refresh_shape(impact: &LinkageRefreshImpact) -> LinkageRefreshShape<'_> {
144 if impact.is_empty() {
145 return LinkageRefreshShape::Empty;
146 }
147 if !impact.has_precise_graph_diff() {
148 return LinkageRefreshShape::SourceLevel;
149 }
150 if impact.scope.has_manifest_path_change() {
151 return LinkageRefreshShape::ManifestPolicy;
152 }
153 if !impact.references.is_empty() {
154 return LinkageRefreshShape::LinkageRelevant;
155 }
156 match &impact.symbols {
157 SymbolDelta::AdditiveOnly { added } => LinkageRefreshShape::AdditiveSymbolsOnly(added),
158 SymbolDelta::RemovedOnly { removed, .. } => {
159 LinkageRefreshShape::RemovedSymbolsOnly(removed)
160 }
161 SymbolDelta::Unchanged | SymbolDelta::Mixed { .. } => LinkageRefreshShape::LinkageRelevant,
162 }
163}
164
165impl RefreshScope {
166 fn new(changed_sources: Vec<SourceId>, changed_paths: Vec<PathBuf>) -> Self {
167 Self {
168 changed_sources,
169 changed_paths,
170 }
171 }
172
173 fn is_empty(&self) -> bool {
174 self.changed_sources.is_empty() && self.changed_paths.is_empty()
175 }
176
177 fn changed_sources(&self) -> &[SourceId] {
178 &self.changed_sources
179 }
180
181 fn changed_paths(&self) -> &[PathBuf] {
182 &self.changed_paths
183 }
184
185 fn has_manifest_path_change(&self) -> bool {
186 self.changed_paths
187 .iter()
188 .any(|path| Manifest::for_filename(path).is_some())
189 }
190}
191
192impl ReferenceDelta {
193 fn from_code_index(graph_diff: &CodeIndexGraphDiff) -> Self {
194 if graph_diff.changed_references.is_empty()
195 && graph_diff.removed_references.is_empty()
196 && graph_diff.reference_id_remaps.is_empty()
197 {
198 return Self::Unchanged;
199 }
200 Self::Changed {
201 changed: graph_diff.changed_references.clone(),
202 removed: graph_diff.removed_references.clone(),
203 remapped: graph_diff.reference_id_remaps.clone(),
204 }
205 }
206
207 pub(in crate::linkage) fn is_empty(&self) -> bool {
208 matches!(self, Self::Unchanged)
209 }
210
211 pub(in crate::linkage) fn changed_ids(&self) -> &[ReferenceId] {
212 match self {
213 Self::Unchanged => &[],
214 Self::Changed { changed, .. } => changed,
215 }
216 }
217
218 pub(in crate::linkage) fn id_remaps(&self) -> &[(ReferenceId, ReferenceId)] {
219 match self {
220 Self::Unchanged => &[],
221 Self::Changed { remapped, .. } => remapped,
222 }
223 }
224
225 pub(in crate::linkage) fn removed_ids(&self) -> &[ReferenceId] {
226 match self {
227 Self::Unchanged => &[],
228 Self::Changed { removed, .. } => removed,
229 }
230 }
231}
232
233impl SymbolDelta {
234 fn from_code_index(graph_diff: CodeIndexGraphDiff) -> Self {
235 if symbol_delta_is_empty(&graph_diff) {
236 return Self::Unchanged;
237 }
238 if is_additive_symbol_delta(&graph_diff) {
239 return Self::AdditiveOnly {
240 added: graph_diff.added_symbols,
241 };
242 }
243 if is_removed_symbol_delta(&graph_diff) {
244 return Self::RemovedOnly {
245 removed: graph_diff.removed_symbols,
246 retargeted_identities: graph_diff.removed_symbol_identities,
247 };
248 }
249 let retargeted_identities = retargeted_symbol_identities_from_diff(&graph_diff);
250 Self::Mixed {
251 candidate_changed: candidate_changed_symbols(&graph_diff),
252 changed: graph_diff.changed_symbols,
253 retargeted_identities,
254 }
255 }
256
257 pub(in crate::linkage) fn candidate_ids(&self) -> &[SymbolId] {
258 match self {
259 Self::AdditiveOnly { added } => added,
260 Self::Mixed {
261 candidate_changed, ..
262 } => candidate_changed,
263 Self::Unchanged | Self::RemovedOnly { .. } => &[],
264 }
265 }
266
267 pub(in crate::linkage) fn changed_ids(&self) -> &[SymbolId] {
268 match self {
269 Self::AdditiveOnly { added } => added,
270 Self::Mixed { changed, .. } => changed,
271 Self::Unchanged | Self::RemovedOnly { .. } => &[],
272 }
273 }
274
275 pub(in crate::linkage) fn retargeted_identities(&self) -> &[String] {
276 match self {
277 Self::RemovedOnly {
278 retargeted_identities,
279 ..
280 }
281 | Self::Mixed {
282 retargeted_identities,
283 ..
284 } => retargeted_identities,
285 Self::Unchanged | Self::AdditiveOnly { .. } => &[],
286 }
287 }
288}
289
290fn symbol_delta_is_unchanged(symbols: &SymbolDelta) -> bool {
291 matches!(symbols, SymbolDelta::Unchanged)
292}
293
294fn symbol_delta_is_empty(graph_diff: &CodeIndexGraphDiff) -> bool {
295 graph_diff.added_symbols.is_empty()
296 && graph_diff.modified_symbols.is_empty()
297 && graph_diff.changed_symbols.is_empty()
298 && graph_diff.removed_symbols.is_empty()
299 && graph_diff.modified_symbol_identities.is_empty()
300 && graph_diff.removed_symbol_identities.is_empty()
301 && graph_diff.symbol_id_remaps.is_empty()
302}
303
304fn is_additive_symbol_delta(graph_diff: &CodeIndexGraphDiff) -> bool {
305 !graph_diff.added_symbols.is_empty()
306 && graph_diff.modified_symbols.is_empty()
307 && graph_diff.removed_symbols.is_empty()
308 && graph_diff.symbol_id_remaps.is_empty()
309 && graph_diff
310 .changed_symbols
311 .iter()
312 .all(|symbol| graph_diff.added_symbols.contains(symbol))
313}
314
315fn is_removed_symbol_delta(graph_diff: &CodeIndexGraphDiff) -> bool {
316 !graph_diff.removed_symbols.is_empty()
317 && graph_diff.added_symbols.is_empty()
318 && graph_diff.modified_symbols.is_empty()
319 && graph_diff.changed_symbols.is_empty()
320 && graph_diff.symbol_id_remaps.is_empty()
321}
322
323fn candidate_changed_symbols(graph_diff: &CodeIndexGraphDiff) -> Vec<SymbolId> {
324 graph_diff
325 .added_symbols
326 .iter()
327 .chain(graph_diff.modified_symbols.iter())
328 .cloned()
329 .collect()
330}
331
332fn retargeted_symbol_identities_from_diff(graph_diff: &CodeIndexGraphDiff) -> Vec<String> {
333 graph_diff
334 .modified_symbol_identities
335 .iter()
336 .chain(graph_diff.removed_symbol_identities.iter())
337 .cloned()
338 .collect()
339}