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