code-moniker-workspace 0.6.1

Workspace model, ports, snapshots, linkage, and change analysis for code-moniker.
Documentation
use super::*;

mod chains;
mod fields;
mod methods;

pub(in crate::linkage) use chains::pending_receiver_chains;
use chains::{
	ChainContext, build_receiver_call_index, callable_owner, collect_return_types,
	decision_reference, decision_target, external_origin, external_target_shape, method_target,
	reference_status, reference_statuses, resolve_receiver_chain,
};
use fields::{MonikerTypeSet, typed_receiver_decision, typed_receiver_types_decision};
pub(in crate::linkage) use fields::{ReceiverFieldTables, resolve_method_through_supers};
pub(in crate::linkage) use fields::{build_receiver_field_tables, refine_receiver_fields};
use methods::ReceiverCallIndex;
pub(in crate::linkage) use methods::{MethodCallReference, MethodTable};

impl LinkageRefiner<'_> {
	fn resolved_method_targets(
		&self,
		owner: &Moniker,
		call_name: &str,
		call_arity: Option<usize>,
	) -> Option<SymbolSet> {
		let target = method_target(owner, call_name, call_arity);
		if let Some(symbol) = self.candidates.indexes().symbol_by_moniker(&target) {
			return Some(SymbolSet::from_symbol(symbol));
		}
		self.methods.resolve_by_name(owner, call_name, call_arity)
	}

	fn resolved_return_types<'b>(
		&self,
		symbol: SymbolOrdinal,
		return_types: &'b FxHashMap<Moniker, MonikerTypeSet>,
	) -> Option<&'b MonikerTypeSet> {
		let callable = self.candidates.candidate(symbol)?.moniker;
		return_types.get(callable)
	}

	fn manifest_declares_target(
		&self,
		method_call: MethodCallReference<'_>,
		target: &Moniker,
	) -> bool {
		let Some(location) = self.locations.get(method_call.reference_idx) else {
			return false;
		};
		let Some(query) = LinkageQuery::at(method_call.reference, self.material, location) else {
			return false;
		};
		self.manifests
			.declares_external_target(&query.with_target(target))
	}
}

pub(super) fn refine_structural_receivers(
	linkage: &LinkageRefiner<'_>,
	decisions: &mut [ReferenceLinkageDecision],
	references: &RecordTable<ReferenceRecord>,
	changed_references: Option<&FxHashSet<ReferenceId>>,
) {
	let mut owners_by_binding: FxHashMap<(crate::snapshot::SymbolId, String), SymbolSet> =
		FxHashMap::default();
	if let Some(changed) = changed_references {
		let mut affected = FxHashMap::default();
		for reference_id in changed {
			let Some((source_file, local_reference)) =
				linkage.material.identity.reference_location(reference_id)
			else {
				continue;
			};
			let Some(reference_idx) = linkage
				.locations
				.reference_idx(source_file, local_reference)
			else {
				continue;
			};
			let reference = &references[reference_idx];
			let Some(receiver) = structural_receiver_name(reference) else {
				continue;
			};
			affected.insert((reference.source_symbol, receiver.to_owned()), source_file);
		}
		for (binding, source_file) in affected {
			let Some(file) = linkage.material.files.get(source_file) else {
				continue;
			};
			for local_reference in 0..file.graph.ref_count() {
				let Some(reference_idx) = linkage
					.locations
					.reference_idx(source_file, local_reference)
				else {
					continue;
				};
				let reference = &references[reference_idx];
				if reference.source_symbol != binding.0
					|| structural_receiver_name(reference) != Some(binding.1.as_str())
				{
					continue;
				}
				accumulate_structural_owner(linkage, &mut owners_by_binding, reference);
			}
		}
	} else {
		for reference in references.iter() {
			accumulate_structural_owner(linkage, &mut owners_by_binding, reference);
		}
	}

	const MAX_STRUCTURAL_OWNERS: usize = 32;
	for decision in decisions {
		let Some(reference_idx) = decision.refinement_pending_reference_idx() else {
			continue;
		};
		if changed_references.is_some_and(|changed| !changed.contains(decision.reference())) {
			continue;
		}
		let reference = &references[reference_idx];
		let Some(receiver) = structural_receiver_name(reference) else {
			continue;
		};
		let Some(owners) = owners_by_binding.get(&(reference.source_symbol, receiver.to_owned()))
		else {
			continue;
		};
		if owners.is_empty() || owners.len() > MAX_STRUCTURAL_OWNERS {
			continue;
		}
		let Some(call_name) = reference.call_name.as_deref() else {
			continue;
		};
		let Some(call_arity) = reference.call_arity else {
			continue;
		};
		let targets =
			linkage
				.methods
				.methods_for_owners(linkage.candidates, owners, call_name, call_arity);
		if targets.is_empty() {
			continue;
		}
		*decision = ReferenceLinkageDecision::dynamic(
			crate::snapshot::DynamicReason::DuckTypedCandidateSet,
			reference_idx,
			reference.id,
			targets,
		);
	}
}

fn accumulate_structural_owner(
	linkage: &LinkageRefiner<'_>,
	owners_by_binding: &mut FxHashMap<(crate::snapshot::SymbolId, String), SymbolSet>,
	reference: &ReferenceRecord,
) {
	let Some(receiver) = structural_receiver_name(reference) else {
		return;
	};
	let Some(call_name) = reference.call_name.as_deref() else {
		return;
	};
	let Some(call_arity) = reference.call_arity else {
		return;
	};
	let owners = linkage
		.methods
		.structural_owners(call_name, call_arity)
		.cloned()
		.unwrap_or_else(SymbolSet::new);
	match owners_by_binding.entry((reference.source_symbol, receiver.to_owned())) {
		std::collections::hash_map::Entry::Vacant(entry) => {
			entry.insert(owners);
		}
		std::collections::hash_map::Entry::Occupied(mut entry) => {
			entry.get_mut().intersect_with(&owners);
		}
	}
}

fn structural_receiver_name(reference: &ReferenceRecord) -> Option<&str> {
	if reference.kind != "method_call" {
		return None;
	}
	let receiver = reference.receiver.as_deref()?;
	if receiver.is_empty()
		|| matches!(
			receiver,
			"self" | "cls" | "call" | "member" | "subscript" | "python_conditional_import"
		) || !receiver
		.bytes()
		.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
	{
		return None;
	}
	Some(receiver)
}

pub(super) fn refine_receiver_chains(
	linkage: &LinkageRefiner<'_>,
	tables: &ReceiverFieldTables,
	decisions: &mut [ReferenceLinkageDecision],
	references: &RecordTable<ReferenceRecord>,
	mut pending: Vec<usize>,
) {
	if pending.is_empty() {
		return;
	}
	let receiver_calls = build_receiver_call_index(linkage, decisions, &pending);
	let wanted = receiver_calls
		.by_reference
		.values()
		.copied()
		.collect::<FxHashSet<_>>();
	let mut statuses = reference_statuses(linkage.material, decisions, references, &wanted);
	let return_types =
		collect_return_types(linkage.material, linkage.candidates, decisions, references);
	loop {
		let context = ChainContext {
			statuses: &statuses,
			receiver_calls: &receiver_calls,
			return_types: &return_types,
		};
		let replacements = pending
			.par_iter()
			.filter_map(|idx| {
				let reference_idx = decisions[*idx].refinement_pending_reference_idx()?;
				resolve_receiver_chain(
					linkage,
					tables,
					&context,
					reference_idx,
					&references[reference_idx],
				)
				.map(|replacement| (*idx, replacement))
			})
			.collect::<Vec<_>>();
		if replacements.is_empty() {
			break;
		}
		for (idx, replacement) in replacements {
			if let Some(status) = reference_status(linkage.material, &replacement, references) {
				statuses.insert(replacement.reference_idx(), status);
			}
			decisions[idx] = replacement;
		}
		pending.retain(|idx| decisions[*idx].refinement_pending_reference_idx().is_some());
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn return_type_sets_preserve_distinct_candidates() {
		let first = MonikerBuilder::new()
			.project(b"app")
			.segment(b"class", b"First")
			.build();
		let second = MonikerBuilder::new()
			.project(b"app")
			.segment(b"class", b"Second")
			.build();
		let mut types = MonikerTypeSet::default();
		types.insert(first.clone());
		types.insert(second.clone());
		types.insert(first.clone());

		assert_eq!(
			types.iter().cloned().collect::<Vec<_>>(),
			vec![first, second]
		);
	}
}