Skip to main content

code_moniker_workspace/linkage/change/
delta.rs

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