code-moniker-workspace 0.4.0

Workspace model, ports, snapshots, linkage, and change analysis for code-moniker.
Documentation
use std::path::PathBuf;

use code_moniker_core::lang::build_manifest::Manifest;

use crate::code::CodeIndexGraphDiff;
use crate::snapshot::{ReferenceId, SourceId, SymbolId};

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LinkageGraphDelta {
	references: ReferenceDelta,
	symbols: SymbolDelta,
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LinkageRefreshImpact {
	scope: RefreshScope,
	references: ReferenceDelta,
	symbols: SymbolDelta,
	precision: LinkageDiffPrecision,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum LinkageDiffPrecision {
	#[default]
	SourceLevel,
	Precise,
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(in crate::linkage) struct RefreshScope {
	changed_sources: Vec<SourceId>,
	changed_paths: Vec<PathBuf>,
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(in crate::linkage) enum ReferenceDelta {
	#[default]
	Unchanged,
	Changed {
		changed: Vec<ReferenceId>,
		removed: Vec<ReferenceId>,
		remapped: Vec<(ReferenceId, ReferenceId)>,
	},
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(in crate::linkage) enum SymbolDelta {
	#[default]
	Unchanged,
	AdditiveOnly {
		added: Vec<SymbolId>,
	},
	RemovedOnly {
		removed: Vec<SymbolId>,
		retargeted_identities: Vec<String>,
	},
	Mixed {
		candidate_changed: Vec<SymbolId>,
		changed: Vec<SymbolId>,
		retargeted_identities: Vec<String>,
	},
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::linkage) enum LinkageRefreshShape<'a> {
	Empty,
	SourceLevel,
	ManifestPolicy,
	AdditiveSymbolsOnly(&'a [SymbolId]),
	RemovedSymbolsOnly(&'a [SymbolId]),
	LinkageRelevant,
}

impl LinkageRefreshImpact {
	pub fn new(changed_sources: Vec<SourceId>, changed_paths: Vec<PathBuf>) -> Self {
		Self {
			scope: RefreshScope::new(changed_sources, changed_paths),
			references: ReferenceDelta::Unchanged,
			symbols: SymbolDelta::Unchanged,
			precision: LinkageDiffPrecision::SourceLevel,
		}
	}

	pub fn with_graph_delta(
		changed_sources: Vec<SourceId>,
		changed_paths: Vec<PathBuf>,
		graph_delta: LinkageGraphDelta,
	) -> Self {
		Self {
			scope: RefreshScope::new(changed_sources, changed_paths),
			references: graph_delta.references,
			symbols: graph_delta.symbols,
			precision: LinkageDiffPrecision::Precise,
		}
	}

	pub fn is_empty(&self) -> bool {
		self.scope.is_empty()
			&& self.references.is_empty()
			&& symbol_delta_is_unchanged(&self.symbols)
	}

	pub(in crate::linkage) fn shape(&self) -> LinkageRefreshShape<'_> {
		classify_refresh_shape(self)
	}

	pub(in crate::linkage) fn changed_sources(&self) -> &[SourceId] {
		self.scope.changed_sources()
	}

	pub(in crate::linkage) fn changed_paths(&self) -> &[PathBuf] {
		self.scope.changed_paths()
	}

	pub(in crate::linkage) fn has_precise_graph_diff(&self) -> bool {
		self.precision == LinkageDiffPrecision::Precise
	}

	pub(in crate::linkage) fn references(&self) -> &ReferenceDelta {
		&self.references
	}

	pub(in crate::linkage) fn definitions(&self) -> &SymbolDelta {
		&self.symbols
	}
}

impl LinkageGraphDelta {
	pub fn from_code_index(graph_diff: CodeIndexGraphDiff) -> Self {
		Self {
			references: ReferenceDelta::from_code_index(&graph_diff),
			symbols: SymbolDelta::from_code_index(graph_diff),
		}
	}
}

impl From<CodeIndexGraphDiff> for LinkageGraphDelta {
	fn from(graph_diff: CodeIndexGraphDiff) -> Self {
		Self::from_code_index(graph_diff)
	}
}

fn classify_refresh_shape(impact: &LinkageRefreshImpact) -> LinkageRefreshShape<'_> {
	if impact.is_empty() {
		return LinkageRefreshShape::Empty;
	}
	if !impact.has_precise_graph_diff() {
		return LinkageRefreshShape::SourceLevel;
	}
	if impact.scope.has_manifest_path_change() {
		return LinkageRefreshShape::ManifestPolicy;
	}
	if !impact.references.is_empty() {
		return LinkageRefreshShape::LinkageRelevant;
	}
	match &impact.symbols {
		SymbolDelta::AdditiveOnly { added } => LinkageRefreshShape::AdditiveSymbolsOnly(added),
		SymbolDelta::RemovedOnly { removed, .. } => {
			LinkageRefreshShape::RemovedSymbolsOnly(removed)
		}
		SymbolDelta::Unchanged | SymbolDelta::Mixed { .. } => LinkageRefreshShape::LinkageRelevant,
	}
}

impl RefreshScope {
	fn new(changed_sources: Vec<SourceId>, changed_paths: Vec<PathBuf>) -> Self {
		Self {
			changed_sources,
			changed_paths,
		}
	}

	fn is_empty(&self) -> bool {
		self.changed_sources.is_empty() && self.changed_paths.is_empty()
	}

	fn changed_sources(&self) -> &[SourceId] {
		&self.changed_sources
	}

	fn changed_paths(&self) -> &[PathBuf] {
		&self.changed_paths
	}

	fn has_manifest_path_change(&self) -> bool {
		self.changed_paths
			.iter()
			.any(|path| Manifest::for_filename(path).is_some())
	}
}

impl ReferenceDelta {
	fn from_code_index(graph_diff: &CodeIndexGraphDiff) -> Self {
		if graph_diff.changed_references.is_empty()
			&& graph_diff.removed_references.is_empty()
			&& graph_diff.reference_id_remaps.is_empty()
		{
			return Self::Unchanged;
		}
		Self::Changed {
			changed: graph_diff.changed_references.clone(),
			removed: graph_diff.removed_references.clone(),
			remapped: graph_diff.reference_id_remaps.clone(),
		}
	}

	pub(in crate::linkage) fn is_empty(&self) -> bool {
		matches!(self, Self::Unchanged)
	}

	pub(in crate::linkage) fn changed_ids(&self) -> &[ReferenceId] {
		match self {
			Self::Unchanged => &[],
			Self::Changed { changed, .. } => changed,
		}
	}

	pub(in crate::linkage) fn id_remaps(&self) -> &[(ReferenceId, ReferenceId)] {
		match self {
			Self::Unchanged => &[],
			Self::Changed { remapped, .. } => remapped,
		}
	}

	pub(in crate::linkage) fn removed_ids(&self) -> &[ReferenceId] {
		match self {
			Self::Unchanged => &[],
			Self::Changed { removed, .. } => removed,
		}
	}
}

impl SymbolDelta {
	fn from_code_index(graph_diff: CodeIndexGraphDiff) -> Self {
		if symbol_delta_is_empty(&graph_diff) {
			return Self::Unchanged;
		}
		if is_additive_symbol_delta(&graph_diff) {
			return Self::AdditiveOnly {
				added: graph_diff.added_symbols,
			};
		}
		if is_removed_symbol_delta(&graph_diff) {
			return Self::RemovedOnly {
				removed: graph_diff.removed_symbols,
				retargeted_identities: graph_diff.removed_symbol_identities,
			};
		}
		let retargeted_identities = retargeted_symbol_identities_from_diff(&graph_diff);
		Self::Mixed {
			candidate_changed: candidate_changed_symbols(&graph_diff),
			changed: graph_diff.changed_symbols,
			retargeted_identities,
		}
	}

	pub(in crate::linkage) fn candidate_ids(&self) -> &[SymbolId] {
		match self {
			Self::AdditiveOnly { added } => added,
			Self::Mixed {
				candidate_changed, ..
			} => candidate_changed,
			Self::Unchanged | Self::RemovedOnly { .. } => &[],
		}
	}

	pub(in crate::linkage) fn changed_ids(&self) -> &[SymbolId] {
		match self {
			Self::AdditiveOnly { added } => added,
			Self::Mixed { changed, .. } => changed,
			Self::Unchanged | Self::RemovedOnly { .. } => &[],
		}
	}

	pub(in crate::linkage) fn retargeted_identities(&self) -> &[String] {
		match self {
			Self::RemovedOnly {
				retargeted_identities,
				..
			}
			| Self::Mixed {
				retargeted_identities,
				..
			} => retargeted_identities,
			Self::Unchanged | Self::AdditiveOnly { .. } => &[],
		}
	}
}

fn symbol_delta_is_unchanged(symbols: &SymbolDelta) -> bool {
	matches!(symbols, SymbolDelta::Unchanged)
}

fn symbol_delta_is_empty(graph_diff: &CodeIndexGraphDiff) -> bool {
	graph_diff.added_symbols.is_empty()
		&& graph_diff.modified_symbols.is_empty()
		&& graph_diff.changed_symbols.is_empty()
		&& graph_diff.removed_symbols.is_empty()
		&& graph_diff.modified_symbol_identities.is_empty()
		&& graph_diff.removed_symbol_identities.is_empty()
		&& graph_diff.symbol_id_remaps.is_empty()
}

fn is_additive_symbol_delta(graph_diff: &CodeIndexGraphDiff) -> bool {
	!graph_diff.added_symbols.is_empty()
		&& graph_diff.modified_symbols.is_empty()
		&& graph_diff.removed_symbols.is_empty()
		&& graph_diff.symbol_id_remaps.is_empty()
		&& graph_diff
			.changed_symbols
			.iter()
			.all(|symbol| graph_diff.added_symbols.contains(symbol))
}

fn is_removed_symbol_delta(graph_diff: &CodeIndexGraphDiff) -> bool {
	!graph_diff.removed_symbols.is_empty()
		&& graph_diff.added_symbols.is_empty()
		&& graph_diff.modified_symbols.is_empty()
		&& graph_diff.changed_symbols.is_empty()
		&& graph_diff.symbol_id_remaps.is_empty()
}

fn candidate_changed_symbols(graph_diff: &CodeIndexGraphDiff) -> Vec<SymbolId> {
	graph_diff
		.added_symbols
		.iter()
		.chain(graph_diff.modified_symbols.iter())
		.cloned()
		.collect()
}

fn retargeted_symbol_identities_from_diff(graph_diff: &CodeIndexGraphDiff) -> Vec<String> {
	graph_diff
		.modified_symbol_identities
		.iter()
		.chain(graph_diff.removed_symbol_identities.iter())
		.cloned()
		.collect()
}