code_moniker_workspace/linkage/change/
delta.rs1use std::path::PathBuf;
2
3use code_moniker_core::lang::build_manifest::Manifest;
4
5use crate::source::CodeIndexMaterial;
6
7use crate::code::CodeIndexGraphDiff;
8use crate::snapshot::{ReferenceId, SourceId, SymbolId};
9
10#[derive(Clone, Debug, Default, Eq, PartialEq)]
11pub struct LinkageGraphDelta {
12 references: ReferenceDelta,
13 symbols: SymbolDelta,
14}
15
16#[derive(Clone, Debug, Default, Eq, PartialEq)]
17pub struct LinkageRefreshImpact {
18 scope: RefreshScope,
19 references: ReferenceDelta,
20 symbols: SymbolDelta,
21 precision: LinkageDiffPrecision,
22}
23
24#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
25enum LinkageDiffPrecision {
26 #[default]
27 SourceLevel,
28 Precise,
29}
30
31#[derive(Clone, Debug, Default, Eq, PartialEq)]
32pub(in crate::linkage) struct RefreshScope {
33 changed_sources: Vec<SourceId>,
34 changed_paths: Vec<PathBuf>,
35}
36
37#[derive(Clone, Debug, Default, Eq, PartialEq)]
38pub(in crate::linkage) enum ReferenceDelta {
39 #[default]
40 Unchanged,
41 Changed {
42 changed: Vec<ReferenceId>,
43 removed: Vec<ReferenceId>,
44 removed_binding: bool,
45 removed_semantic_fact: bool,
46 remapped: Vec<(ReferenceId, ReferenceId)>,
47 },
48}
49
50#[derive(Clone, Debug, Default, Eq, PartialEq)]
51pub(in crate::linkage) enum SymbolDelta {
52 #[default]
53 Unchanged,
54 AdditiveOnly {
55 added: Vec<SymbolId>,
56 },
57 RemovedOnly {
58 removed: Vec<SymbolId>,
59 retargeted_identities: Vec<String>,
60 },
61 Mixed {
62 candidate_changed: Vec<SymbolId>,
63 changed: Vec<SymbolId>,
64 retargeted_identities: Vec<String>,
65 },
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub(in crate::linkage) enum LinkageRefreshShape<'a> {
70 Empty,
71 SourceLevel,
72 ManifestPolicy,
73 AdditiveSymbolsOnly(&'a [SymbolId]),
74 RemovedSymbolsOnly(&'a [SymbolId]),
75 LinkageRelevant,
76}
77
78impl LinkageRefreshImpact {
79 pub fn new(changed_sources: Vec<SourceId>, changed_paths: Vec<PathBuf>) -> Self {
80 Self {
81 scope: RefreshScope::new(changed_sources, changed_paths),
82 references: ReferenceDelta::Unchanged,
83 symbols: SymbolDelta::Unchanged,
84 precision: LinkageDiffPrecision::SourceLevel,
85 }
86 }
87
88 pub fn with_graph_delta(
89 changed_sources: Vec<SourceId>,
90 changed_paths: Vec<PathBuf>,
91 graph_delta: LinkageGraphDelta,
92 ) -> Self {
93 Self {
94 scope: RefreshScope::new(changed_sources, changed_paths),
95 references: graph_delta.references,
96 symbols: graph_delta.symbols,
97 precision: LinkageDiffPrecision::Precise,
98 }
99 }
100
101 pub fn is_empty(&self) -> bool {
102 self.scope.is_empty()
103 && self.references.is_empty()
104 && symbol_delta_is_unchanged(&self.symbols)
105 }
106
107 pub(in crate::linkage) fn shape(&self) -> LinkageRefreshShape<'_> {
108 classify_refresh_shape(self)
109 }
110
111 pub(in crate::linkage) fn changed_sources(&self) -> &[SourceId] {
112 self.scope.changed_sources()
113 }
114
115 pub(in crate::linkage) fn changed_paths(&self) -> &[PathBuf] {
116 self.scope.changed_paths()
117 }
118
119 pub(in crate::linkage) fn has_precise_graph_diff(&self) -> bool {
120 self.precision == LinkageDiffPrecision::Precise
121 }
122
123 pub(in crate::linkage) fn references(&self) -> &ReferenceDelta {
124 &self.references
125 }
126
127 pub(in crate::linkage) fn definitions(&self) -> &SymbolDelta {
128 &self.symbols
129 }
130}
131
132pub(in crate::linkage) fn changes_c_include_topology(
133 impact: &LinkageRefreshImpact,
134 material: &CodeIndexMaterial,
135) -> bool {
136 let changed_c_path = impact
137 .scope
138 .changed_paths
139 .iter()
140 .any(|path| is_c_family_path(path));
141 let ReferenceDelta::Changed {
142 changed,
143 removed_binding,
144 ..
145 } = &impact.references
146 else {
147 return false;
148 };
149 if *removed_binding && changed_c_path {
150 return true;
151 }
152 changed.iter().any(|reference| {
153 let Some((source_file, local_reference)) = material.identity.reference_location(reference)
154 else {
155 return false;
156 };
157 material.files.get(source_file).is_some_and(|file| {
158 file.lang == code_moniker_core::lang::Lang::C
159 && file.graph.ref_at(local_reference).kind
160 == code_moniker_core::lang::kinds::IMPORTS_MODULE
161 })
162 })
163}
164
165fn is_c_family_path(path: &std::path::Path) -> bool {
166 path.extension()
167 .and_then(|extension| extension.to_str())
168 .is_some_and(|extension| {
169 matches!(
170 extension.to_ascii_lowercase().as_str(),
171 "c" | "h" | "cc" | "cpp" | "cxx" | "c++"
172 )
173 })
174}
175
176impl LinkageGraphDelta {
177 pub fn from_code_index(graph_diff: CodeIndexGraphDiff) -> Self {
178 Self {
179 references: ReferenceDelta::from_code_index(&graph_diff),
180 symbols: SymbolDelta::from_code_index(graph_diff),
181 }
182 }
183}
184
185impl From<CodeIndexGraphDiff> for LinkageGraphDelta {
186 fn from(graph_diff: CodeIndexGraphDiff) -> Self {
187 Self::from_code_index(graph_diff)
188 }
189}
190
191fn classify_refresh_shape(impact: &LinkageRefreshImpact) -> LinkageRefreshShape<'_> {
192 if impact.is_empty() {
193 return LinkageRefreshShape::Empty;
194 }
195 if !impact.has_precise_graph_diff() {
196 return LinkageRefreshShape::SourceLevel;
197 }
198 if impact.scope.has_manifest_path_change() {
199 return LinkageRefreshShape::ManifestPolicy;
200 }
201 if !impact.references.is_empty() {
202 return LinkageRefreshShape::LinkageRelevant;
203 }
204 match &impact.symbols {
205 SymbolDelta::AdditiveOnly { added } => LinkageRefreshShape::AdditiveSymbolsOnly(added),
206 SymbolDelta::RemovedOnly { removed, .. } => {
207 LinkageRefreshShape::RemovedSymbolsOnly(removed)
208 }
209 SymbolDelta::Unchanged | SymbolDelta::Mixed { .. } => LinkageRefreshShape::LinkageRelevant,
210 }
211}
212
213impl RefreshScope {
214 fn new(changed_sources: Vec<SourceId>, changed_paths: Vec<PathBuf>) -> Self {
215 Self {
216 changed_sources,
217 changed_paths,
218 }
219 }
220
221 fn is_empty(&self) -> bool {
222 self.changed_sources.is_empty() && self.changed_paths.is_empty()
223 }
224
225 fn changed_sources(&self) -> &[SourceId] {
226 &self.changed_sources
227 }
228
229 fn changed_paths(&self) -> &[PathBuf] {
230 &self.changed_paths
231 }
232
233 fn has_manifest_path_change(&self) -> bool {
234 self.changed_paths
235 .iter()
236 .any(|path| Manifest::for_filename(path).is_some())
237 }
238}
239
240impl ReferenceDelta {
241 fn from_code_index(graph_diff: &CodeIndexGraphDiff) -> Self {
242 if graph_diff.changed_references.is_empty()
243 && graph_diff.removed_references.is_empty()
244 && graph_diff.reference_id_remaps.is_empty()
245 {
246 return Self::Unchanged;
247 }
248 Self::Changed {
249 changed: graph_diff.changed_references.clone(),
250 removed: graph_diff.removed_references.clone(),
251 removed_binding: graph_diff.removed_reference_kinds.iter().any(|kind| {
252 matches!(
253 kind.as_bytes(),
254 code_moniker_core::lang::kinds::IMPORTS_MODULE
255 | code_moniker_core::lang::kinds::IMPORTS_SYMBOL
256 | code_moniker_core::core::kinds::REF_REEXPORTS
257 )
258 }),
259 removed_semantic_fact: graph_diff.removed_reference_kinds.iter().any(|kind| {
260 matches!(
261 kind.as_bytes(),
262 code_moniker_core::lang::kinds::TYPED_AS
263 | code_moniker_core::lang::kinds::RETURNS_TYPE
264 )
265 }),
266 remapped: graph_diff.reference_id_remaps.clone(),
267 }
268 }
269
270 pub(in crate::linkage) fn is_empty(&self) -> bool {
271 matches!(self, Self::Unchanged)
272 }
273
274 pub(in crate::linkage) fn changed_ids(&self) -> &[ReferenceId] {
275 match self {
276 Self::Unchanged => &[],
277 Self::Changed { changed, .. } => changed,
278 }
279 }
280
281 pub(in crate::linkage) fn id_remaps(&self) -> &[(ReferenceId, ReferenceId)] {
282 match self {
283 Self::Unchanged => &[],
284 Self::Changed { remapped, .. } => remapped,
285 }
286 }
287
288 pub(in crate::linkage) fn removed_ids(&self) -> &[ReferenceId] {
289 match self {
290 Self::Unchanged => &[],
291 Self::Changed { removed, .. } => removed,
292 }
293 }
294
295 pub(in crate::linkage) fn removed_binding(&self) -> bool {
296 matches!(
297 self,
298 Self::Changed {
299 removed_binding: true,
300 ..
301 }
302 )
303 }
304
305 pub(in crate::linkage) fn removed_semantic_fact(&self) -> bool {
306 matches!(
307 self,
308 Self::Changed {
309 removed_semantic_fact: true,
310 ..
311 }
312 )
313 }
314}
315
316impl SymbolDelta {
317 fn from_code_index(graph_diff: CodeIndexGraphDiff) -> Self {
318 if symbol_delta_is_empty(&graph_diff) {
319 return Self::Unchanged;
320 }
321 if is_additive_symbol_delta(&graph_diff) {
322 return Self::AdditiveOnly {
323 added: graph_diff.added_symbols,
324 };
325 }
326 if is_removed_symbol_delta(&graph_diff) {
327 return Self::RemovedOnly {
328 removed: graph_diff.removed_symbols,
329 retargeted_identities: graph_diff.removed_symbol_identities,
330 };
331 }
332 let retargeted_identities = retargeted_symbol_identities_from_diff(&graph_diff);
333 Self::Mixed {
334 candidate_changed: candidate_changed_symbols(&graph_diff),
335 changed: graph_diff.changed_symbols,
336 retargeted_identities,
337 }
338 }
339
340 pub(in crate::linkage) fn candidate_ids(&self) -> &[SymbolId] {
341 match self {
342 Self::AdditiveOnly { added } => added,
343 Self::Mixed {
344 candidate_changed, ..
345 } => candidate_changed,
346 Self::Unchanged | Self::RemovedOnly { .. } => &[],
347 }
348 }
349
350 pub(in crate::linkage) fn changed_ids(&self) -> &[SymbolId] {
351 match self {
352 Self::AdditiveOnly { added } => added,
353 Self::Mixed { changed, .. } => changed,
354 Self::Unchanged | Self::RemovedOnly { .. } => &[],
355 }
356 }
357
358 pub(in crate::linkage) fn retargeted_identities(&self) -> &[String] {
359 match self {
360 Self::RemovedOnly {
361 retargeted_identities,
362 ..
363 }
364 | Self::Mixed {
365 retargeted_identities,
366 ..
367 } => retargeted_identities,
368 Self::Unchanged | Self::AdditiveOnly { .. } => &[],
369 }
370 }
371}
372
373fn symbol_delta_is_unchanged(symbols: &SymbolDelta) -> bool {
374 matches!(symbols, SymbolDelta::Unchanged)
375}
376
377fn symbol_delta_is_empty(graph_diff: &CodeIndexGraphDiff) -> bool {
378 graph_diff.added_symbols.is_empty()
379 && graph_diff.modified_symbols.is_empty()
380 && graph_diff.changed_symbols.is_empty()
381 && graph_diff.removed_symbols.is_empty()
382 && graph_diff.modified_symbol_identities.is_empty()
383 && graph_diff.removed_symbol_identities.is_empty()
384 && graph_diff.symbol_id_remaps.is_empty()
385}
386
387fn is_additive_symbol_delta(graph_diff: &CodeIndexGraphDiff) -> bool {
388 !graph_diff.added_symbols.is_empty()
389 && graph_diff.modified_symbols.is_empty()
390 && graph_diff.removed_symbols.is_empty()
391 && graph_diff.symbol_id_remaps.is_empty()
392 && graph_diff
393 .changed_symbols
394 .iter()
395 .all(|symbol| graph_diff.added_symbols.contains(symbol))
396}
397
398fn is_removed_symbol_delta(graph_diff: &CodeIndexGraphDiff) -> bool {
399 !graph_diff.removed_symbols.is_empty()
400 && graph_diff.added_symbols.is_empty()
401 && graph_diff.modified_symbols.is_empty()
402 && graph_diff.changed_symbols.is_empty()
403 && graph_diff.symbol_id_remaps.is_empty()
404}
405
406fn candidate_changed_symbols(graph_diff: &CodeIndexGraphDiff) -> Vec<SymbolId> {
407 graph_diff
408 .added_symbols
409 .iter()
410 .chain(graph_diff.modified_symbols.iter())
411 .cloned()
412 .collect()
413}
414
415fn retargeted_symbol_identities_from_diff(graph_diff: &CodeIndexGraphDiff) -> Vec<String> {
416 graph_diff
417 .modified_symbol_identities
418 .iter()
419 .chain(graph_diff.removed_symbol_identities.iter())
420 .cloned()
421 .collect()
422}