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::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}