Skip to main content

code_moniker_workspace/linkage/change/
delta.rs

1use 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}