use foldhash::HashMap;
use foldhash::HashSet;
use mago_word::ascii_lowercase_word;
use mago_word::empty_word;
use mago_word::Word;
use mago_word::WordSet;
use crate::context::ScopeContext;
use crate::diff::CodebaseDiff;
use crate::identifier::function_like::FunctionLikeIdentifier;
use crate::identifier::method::MethodIdentifier;
use crate::symbol::SymbolIdentifier;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ReferenceSource {
Symbol(bool, Word),
ClassLikeMember(bool, Word, Word),
File(bool, Word),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ReferenceOrigin {
Symbol(SymbolIdentifier),
File(Word),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SymbolReferenceKind {
Body,
Signature,
OverriddenMember,
FunctionLikeReturn,
PropertyRead,
PropertyWrite,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[allow(clippy::struct_field_names)]
pub struct SymbolReferences {
symbol_references_to_symbols: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
symbol_references_to_symbols_in_signature: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
symbol_references_to_overridden_members: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
functionlike_references_to_functionlike_returns: HashMap<FunctionLikeIdentifier, HashSet<FunctionLikeIdentifier>>,
file_references_to_symbols: HashMap<Word, HashSet<SymbolIdentifier>>,
file_references_to_symbols_in_signature: HashMap<Word, HashSet<SymbolIdentifier>>,
property_write_references: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
property_read_references: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
file_property_write_references: HashMap<Word, HashSet<SymbolIdentifier>>,
file_property_read_references: HashMap<Word, HashSet<SymbolIdentifier>>,
}
impl SymbolReferences {
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.symbol_references_to_symbols.is_empty()
&& self.symbol_references_to_symbols_in_signature.is_empty()
&& self.symbol_references_to_overridden_members.is_empty()
&& self.functionlike_references_to_functionlike_returns.is_empty()
&& self.file_references_to_symbols.is_empty()
&& self.file_references_to_symbols_in_signature.is_empty()
&& self.property_write_references.is_empty()
&& self.property_read_references.is_empty()
&& self.file_property_write_references.is_empty()
&& self.file_property_read_references.is_empty()
}
#[inline]
pub fn count_body_references(&self) -> usize {
self.symbol_references_to_symbols.values().map(std::collections::HashSet::len).sum::<usize>()
+ self.file_references_to_symbols.values().map(std::collections::HashSet::len).sum::<usize>()
}
#[inline]
pub fn count_signature_references(&self) -> usize {
self.symbol_references_to_symbols_in_signature.values().map(std::collections::HashSet::len).sum::<usize>()
+ self.file_references_to_symbols_in_signature.values().map(std::collections::HashSet::len).sum::<usize>()
}
#[inline]
#[must_use]
pub fn total_map_entries(&self) -> usize {
self.symbol_references_to_symbols.len()
+ self.symbol_references_to_symbols_in_signature.len()
+ self.symbol_references_to_overridden_members.len()
+ self.functionlike_references_to_functionlike_returns.len()
+ self.file_references_to_symbols.len()
+ self.file_references_to_symbols_in_signature.len()
+ self.property_write_references.len()
+ self.property_read_references.len()
+ self.file_property_write_references.len()
+ self.file_property_read_references.len()
}
#[inline]
#[must_use]
pub fn count_referencing_symbols(&self, symbol: &SymbolIdentifier, in_signature: bool) -> usize {
let map = if in_signature {
&self.symbol_references_to_symbols_in_signature
} else {
&self.symbol_references_to_symbols
};
let files =
if in_signature { &self.file_references_to_symbols_in_signature } else { &self.file_references_to_symbols };
map.values().filter(|referenced_set| referenced_set.contains(symbol)).count()
+ files.values().filter(|referenced_set| referenced_set.contains(symbol)).count()
}
#[inline]
#[must_use]
pub fn count_property_reads(&self, property: &SymbolIdentifier) -> usize {
self.property_read_references.values().filter(|read_set| read_set.contains(property)).count()
+ self.file_property_read_references.values().filter(|read_set| read_set.contains(property)).count()
}
#[inline]
#[must_use]
pub fn count_property_writes(&self, property: &SymbolIdentifier) -> usize {
self.property_write_references.values().filter(|write_set| write_set.contains(property)).count()
+ self.file_property_write_references.values().filter(|write_set| write_set.contains(property)).count()
}
#[inline]
pub fn add_symbol_reference_to_class_member(
&mut self,
referencing_symbol: Word,
class_member: SymbolIdentifier,
in_signature: bool,
) {
self.add_symbol_reference_to_symbol(referencing_symbol, class_member.0, false);
let key = (referencing_symbol, empty_word());
if in_signature {
self.symbol_references_to_symbols_in_signature.entry(key).or_default().insert(class_member);
} else {
self.symbol_references_to_symbols.entry(key).or_default().insert(class_member);
}
}
#[inline]
pub fn add_symbol_reference_to_symbol(&mut self, referencing_symbol: Word, symbol: Word, in_signature: bool) {
if referencing_symbol == symbol {
return;
}
let referencing_key = (referencing_symbol, empty_word());
let referenced_key = (symbol, empty_word());
if in_signature {
self.symbol_references_to_symbols_in_signature.entry(referencing_key).or_default().insert(referenced_key);
} else {
if let Some(sig_refs) = self.symbol_references_to_symbols_in_signature.get(&referencing_key)
&& sig_refs.contains(&referenced_key)
{
return;
}
self.symbol_references_to_symbols.entry(referencing_key).or_default().insert(referenced_key);
}
}
#[inline]
pub fn add_symbol_reference(
&mut self,
referencing: SymbolIdentifier,
referenced: SymbolIdentifier,
in_signature: bool,
) {
match (referencing.1.is_empty(), referenced.1.is_empty()) {
(true, true) => self.add_symbol_reference_to_symbol(referencing.0, referenced.0, in_signature),
(true, false) => self.add_symbol_reference_to_class_member(referencing.0, referenced, in_signature),
(false, true) => self.add_class_member_reference_to_symbol(referencing, referenced.0, in_signature),
(false, false) => self.add_class_member_reference_to_class_member(referencing, referenced, in_signature),
}
}
#[inline]
pub fn add_reference(&mut self, referencing: ReferenceOrigin, referenced: SymbolIdentifier, in_signature: bool) {
match referencing {
ReferenceOrigin::Symbol(referencing) => {
self.add_symbol_reference(referencing, referenced, in_signature);
}
ReferenceOrigin::File(file) => {
self.add_file_reference_to_class_member(file, referenced, in_signature);
}
}
}
#[inline]
pub fn add_property_read_reference(&mut self, referencing: ReferenceOrigin, property: SymbolIdentifier) {
self.add_reference(referencing, property, false);
match referencing {
ReferenceOrigin::Symbol(symbol) => {
self.property_read_references.entry(symbol).or_default().insert(property);
}
ReferenceOrigin::File(file) => {
self.file_property_read_references.entry(file).or_default().insert(property);
}
}
}
#[inline]
pub fn add_property_write_reference(&mut self, referencing: ReferenceOrigin, property: SymbolIdentifier) {
self.add_reference(referencing, property, false);
match referencing {
ReferenceOrigin::Symbol(symbol) => {
self.property_write_references.entry(symbol).or_default().insert(property);
}
ReferenceOrigin::File(file) => {
self.file_property_write_references.entry(file).or_default().insert(property);
}
}
}
#[inline]
pub fn add_overridden_member_reference(&mut self, referencing: SymbolIdentifier, overridden: SymbolIdentifier) {
self.symbol_references_to_overridden_members.entry(referencing).or_default().insert(overridden);
}
#[inline]
pub fn add_functionlike_return_reference(&mut self, referencing: SymbolIdentifier, referenced: SymbolIdentifier) {
let referencing = if referencing.1.is_empty() {
FunctionLikeIdentifier::Function(referencing.0)
} else {
FunctionLikeIdentifier::Method(referencing.0, referencing.1)
};
let referenced = if referenced.1.is_empty() {
FunctionLikeIdentifier::Function(referenced.0)
} else {
FunctionLikeIdentifier::Method(referenced.0, referenced.1)
};
self.add_reference_to_functionlike_return(referencing, referenced);
}
#[inline]
pub fn add_class_member_reference_to_class_member(
&mut self,
referencing_class_member: SymbolIdentifier,
class_member: SymbolIdentifier,
in_signature: bool,
) {
if referencing_class_member == class_member {
return;
}
self.add_symbol_reference_to_symbol(referencing_class_member.0, class_member.0, false);
self.add_class_member_reference_to_symbol(referencing_class_member, class_member.0, false);
if in_signature {
self.symbol_references_to_symbols_in_signature
.entry(referencing_class_member)
.or_default()
.insert(class_member);
} else {
self.symbol_references_to_symbols.entry(referencing_class_member).or_default().insert(class_member);
}
}
#[inline]
pub fn add_class_member_reference_to_symbol(
&mut self,
referencing_class_member: SymbolIdentifier,
symbol: Word,
in_signature: bool,
) {
if referencing_class_member.0 == symbol {
return;
}
self.add_symbol_reference_to_symbol(referencing_class_member.0, symbol, false);
let referenced_key = (symbol, empty_word());
if in_signature {
self.symbol_references_to_symbols_in_signature
.entry(referencing_class_member)
.or_default()
.insert(referenced_key);
} else {
if let Some(sig_refs) = self.symbol_references_to_symbols_in_signature.get(&referencing_class_member)
&& sig_refs.contains(&referenced_key)
{
return;
}
self.symbol_references_to_symbols.entry(referencing_class_member).or_default().insert(referenced_key);
}
}
#[inline]
pub fn add_file_reference_to_class_member(
&mut self,
file_name: Word,
class_member: SymbolIdentifier,
in_signature: bool,
) {
if !class_member.1.is_empty() {
self.add_file_reference_to_class_member(file_name, (class_member.0, empty_word()), false);
}
if in_signature {
self.file_references_to_symbols_in_signature.entry(file_name).or_default().insert(class_member);
} else {
if let Some(sig_refs) = self.file_references_to_symbols_in_signature.get(&file_name)
&& sig_refs.contains(&class_member)
{
return;
}
self.file_references_to_symbols.entry(file_name).or_default().insert(class_member);
}
}
#[inline]
pub fn add_reference_to_class_member(
&mut self,
scope: &ScopeContext<'_>,
class_member: SymbolIdentifier,
in_signature: bool,
) {
self.add_reference(scope.get_reference_origin(), class_member, in_signature);
}
#[inline]
pub fn add_reference_for_method_call(&mut self, scope: &ScopeContext<'_>, method: &MethodIdentifier) {
self.add_reference_to_class_member(
scope,
(ascii_lowercase_word(method.get_class_name().as_bytes()), method.get_method_name()),
false,
);
}
#[inline]
pub fn add_reference_for_property_read(&mut self, scope: &ScopeContext<'_>, class_name: Word, property_name: Word) {
let normalized_class_name = ascii_lowercase_word(class_name.as_bytes());
let class_member = (normalized_class_name, property_name);
self.add_property_read_reference(scope.get_reference_origin(), class_member);
}
#[inline]
pub fn add_reference_for_property_write(
&mut self,
scope: &ScopeContext<'_>,
class_name: Word,
property_name: Word,
) {
let normalized_class_name = ascii_lowercase_word(class_name.as_bytes());
let class_member = (normalized_class_name, property_name);
self.add_property_write_reference(scope.get_reference_origin(), class_member);
}
#[inline]
pub fn add_reference_to_overridden_class_member(&mut self, scope: &ScopeContext, class_member: SymbolIdentifier) {
let ReferenceOrigin::Symbol(referencing) = scope.get_reference_origin() else {
return;
};
self.symbol_references_to_overridden_members.entry(referencing).or_default().insert(class_member);
}
#[inline]
pub fn add_reference_to_symbol(&mut self, scope: &ScopeContext, symbol: Word, in_signature: bool) {
self.add_reference(scope.get_reference_origin(), (symbol, empty_word()), in_signature);
}
#[inline]
pub fn add_reference_to_functionlike_return(
&mut self,
referencing_functionlike: FunctionLikeIdentifier,
referenced_functionlike: FunctionLikeIdentifier,
) {
if referencing_functionlike == referenced_functionlike {
return;
}
self.functionlike_references_to_functionlike_returns
.entry(referencing_functionlike)
.or_default()
.insert(referenced_functionlike);
}
#[inline]
pub fn extend(&mut self, other: Self) {
for (k, v) in other.symbol_references_to_symbols {
self.symbol_references_to_symbols.entry(k).or_default().extend(v);
}
for (k, v) in other.symbol_references_to_symbols_in_signature {
self.symbol_references_to_symbols_in_signature.entry(k).or_default().extend(v);
}
for (k, v) in other.symbol_references_to_overridden_members {
self.symbol_references_to_overridden_members.entry(k).or_default().extend(v);
}
for (k, v) in other.functionlike_references_to_functionlike_returns {
self.functionlike_references_to_functionlike_returns.entry(k).or_default().extend(v);
}
for (k, v) in other.file_references_to_symbols {
self.file_references_to_symbols.entry(k).or_default().extend(v);
}
for (k, v) in other.file_references_to_symbols_in_signature {
self.file_references_to_symbols_in_signature.entry(k).or_default().extend(v);
}
for (k, v) in other.property_write_references {
self.property_write_references.entry(k).or_default().extend(v);
}
for (k, v) in other.property_read_references {
self.property_read_references.entry(k).or_default().extend(v);
}
for (k, v) in other.file_property_write_references {
self.file_property_write_references.entry(k).or_default().extend(v);
}
for (k, v) in other.file_property_read_references {
self.file_property_read_references.entry(k).or_default().extend(v);
}
}
#[inline]
pub fn for_each_reference(&self, mut visit: impl FnMut(ReferenceOrigin, SymbolIdentifier, SymbolReferenceKind)) {
for (source, targets) in &self.symbol_references_to_symbols {
for target in targets {
visit(ReferenceOrigin::Symbol(*source), *target, SymbolReferenceKind::Body);
}
}
for (source, targets) in &self.symbol_references_to_symbols_in_signature {
for target in targets {
visit(ReferenceOrigin::Symbol(*source), *target, SymbolReferenceKind::Signature);
}
}
for (source, targets) in &self.symbol_references_to_overridden_members {
for target in targets {
visit(ReferenceOrigin::Symbol(*source), *target, SymbolReferenceKind::OverriddenMember);
}
}
for (source, targets) in &self.functionlike_references_to_functionlike_returns {
let Some(source) = function_like_symbol_identifier(source) else {
continue;
};
for target in targets {
if let Some(target) = function_like_symbol_identifier(target) {
visit(ReferenceOrigin::Symbol(source), target, SymbolReferenceKind::FunctionLikeReturn);
}
}
}
for (source, targets) in &self.file_references_to_symbols {
for target in targets {
visit(ReferenceOrigin::File(*source), *target, SymbolReferenceKind::Body);
}
}
for (source, targets) in &self.file_references_to_symbols_in_signature {
for target in targets {
visit(ReferenceOrigin::File(*source), *target, SymbolReferenceKind::Signature);
}
}
for (source, targets) in &self.property_read_references {
for target in targets {
visit(ReferenceOrigin::Symbol(*source), *target, SymbolReferenceKind::PropertyRead);
}
}
for (source, targets) in &self.property_write_references {
for target in targets {
visit(ReferenceOrigin::Symbol(*source), *target, SymbolReferenceKind::PropertyWrite);
}
}
for (source, targets) in &self.file_property_read_references {
for target in targets {
visit(ReferenceOrigin::File(*source), *target, SymbolReferenceKind::PropertyRead);
}
}
for (source, targets) in &self.file_property_write_references {
for target in targets {
visit(ReferenceOrigin::File(*source), *target, SymbolReferenceKind::PropertyWrite);
}
}
}
#[inline]
#[must_use]
pub fn get_references_to_symbol(&self, target_symbol: SymbolIdentifier) -> HashSet<&SymbolIdentifier> {
let mut referencing_items = HashSet::default();
for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
if referenced_items.contains(&target_symbol) {
referencing_items.insert(referencing_item);
}
}
for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
if referenced_items.contains(&target_symbol) {
referencing_items.insert(referencing_item);
}
}
referencing_items
}
#[inline]
#[must_use]
pub fn has_file_reference_to_symbol(&self, target_symbol: SymbolIdentifier) -> bool {
self.file_references_to_symbols.values().any(|references| references.contains(&target_symbol))
|| self
.file_references_to_symbols_in_signature
.values()
.any(|references| references.contains(&target_symbol))
}
#[inline]
#[must_use]
pub fn get_invalid_symbols(
&self,
codebase_diff: &CodebaseDiff,
) -> Option<(HashSet<SymbolIdentifier>, WordSet, WordSet)> {
let mut invalid_signatures = HashSet::default();
let mut partially_invalid_symbols = WordSet::default();
let mut sig_reverse_index: HashMap<SymbolIdentifier, Vec<SymbolIdentifier>> = HashMap::default();
for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
let containing_symbol = (referencing_item.0, empty_word());
if codebase_diff.contains_changed_entry(&containing_symbol) {
invalid_signatures.insert(*referencing_item);
partially_invalid_symbols.insert(referencing_item.0);
}
for referenced in referenced_items {
sig_reverse_index.entry(*referenced).or_default().push(*referencing_item);
}
}
let mut symbols_to_process = codebase_diff.get_changed().iter().copied().collect::<Vec<_>>();
let mut processed_symbols = HashSet::default();
let mut expense_counter = 0;
const EXPENSE_LIMIT: usize = 5000;
while let Some(invalidated_item) = symbols_to_process.pop() {
if processed_symbols.contains(&invalidated_item) {
continue;
}
expense_counter += 1;
if expense_counter > EXPENSE_LIMIT {
return None;
}
invalid_signatures.insert(invalidated_item);
processed_symbols.insert(invalidated_item);
if !invalidated_item.1.is_empty() {
partially_invalid_symbols.insert(invalidated_item.0);
let containing_symbol = (invalidated_item.0, empty_word());
if !processed_symbols.contains(&containing_symbol) {
symbols_to_process.push(containing_symbol);
}
}
if let Some(referencing_items) = sig_reverse_index.get(&invalidated_item) {
for referencing_item in referencing_items {
if !processed_symbols.contains(referencing_item) {
symbols_to_process.push(*referencing_item);
}
invalid_signatures.insert(*referencing_item);
if !referencing_item.1.is_empty() {
partially_invalid_symbols.insert(referencing_item.0);
}
}
}
}
let mut invalid_bodies = HashSet::default();
for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
if referenced_items.iter().any(|r| invalid_signatures.contains(r)) {
invalid_bodies.insert(*referencing_item);
if !referencing_item.1.is_empty() {
partially_invalid_symbols.insert(referencing_item.0);
}
}
}
for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
if referenced_items.iter().any(|r| invalid_signatures.contains(r)) {
invalid_bodies.insert(*referencing_item);
if !referencing_item.1.is_empty() {
partially_invalid_symbols.insert(referencing_item.0);
}
}
}
let mut invalid_files = WordSet::default();
for (file, referenced_items) in
self.file_references_to_symbols.iter().chain(&self.file_references_to_symbols_in_signature)
{
if referenced_items.iter().any(|referenced| invalid_signatures.contains(referenced)) {
invalid_files.insert(*file);
}
}
let mut all_invalid_symbols = invalid_signatures;
all_invalid_symbols.extend(invalid_bodies);
Some((all_invalid_symbols, partially_invalid_symbols, invalid_files))
}
#[inline]
pub fn restore_references_for_safe_symbols(
&mut self,
previous: &SymbolReferences,
safe_symbols: &WordSet,
safe_symbol_members: &HashSet<SymbolIdentifier>,
) {
let is_safe = |key: &SymbolIdentifier| -> bool {
if key.1.is_empty() { safe_symbols.contains(&key.0) } else { safe_symbol_members.contains(key) }
};
for (key, refs) in &previous.symbol_references_to_symbols {
if is_safe(key) && !self.symbol_references_to_symbols.contains_key(key) {
self.symbol_references_to_symbols.insert(*key, refs.clone());
}
}
for (key, refs) in &previous.symbol_references_to_overridden_members {
if is_safe(key) && !self.symbol_references_to_overridden_members.contains_key(key) {
self.symbol_references_to_overridden_members.insert(*key, refs.clone());
}
}
for (key, refs) in &previous.functionlike_references_to_functionlike_returns {
let sym_key = match key {
FunctionLikeIdentifier::Function(name) => (*name, mago_word::empty_word()),
FunctionLikeIdentifier::Method(class, method) => (*class, *method),
_ => continue,
};
if is_safe(&sym_key) && !self.functionlike_references_to_functionlike_returns.contains_key(key) {
self.functionlike_references_to_functionlike_returns.insert(*key, refs.clone());
}
}
for (key, refs) in &previous.property_write_references {
if is_safe(key) && !self.property_write_references.contains_key(key) {
self.property_write_references.insert(*key, refs.clone());
}
}
for (key, refs) in &previous.property_read_references {
if is_safe(key) && !self.property_read_references.contains_key(key) {
self.property_read_references.insert(*key, refs.clone());
}
}
}
#[inline]
pub fn remove_body_references_for_symbols(
&mut self,
symbols_and_members: &HashSet<SymbolIdentifier>,
file_names: &[Word],
) {
for key in symbols_and_members {
self.symbol_references_to_symbols.remove(key);
self.symbol_references_to_overridden_members.remove(key);
self.property_write_references.remove(key);
self.property_read_references.remove(key);
}
self.functionlike_references_to_functionlike_returns.retain(|key, _| {
let sym_key = match key {
FunctionLikeIdentifier::Function(name) => (*name, mago_word::empty_word()),
FunctionLikeIdentifier::Method(class, method) => (*class, *method),
_ => return true,
};
!symbols_and_members.contains(&sym_key)
});
for name in file_names {
self.file_references_to_symbols.remove(name);
self.file_property_write_references.remove(name);
self.file_property_read_references.remove(name);
}
}
#[inline]
pub fn remove_references_from_files(&mut self, file_names: &WordSet) {
for name in file_names {
self.file_references_to_symbols.remove(name);
self.file_references_to_symbols_in_signature.remove(name);
self.file_property_write_references.remove(name);
self.file_property_read_references.remove(name);
}
}
#[inline]
pub fn retain_references_from_files(&mut self, file_names: &WordSet) {
self.file_references_to_symbols.retain(|name, _| file_names.contains(name));
self.file_references_to_symbols_in_signature.retain(|name, _| file_names.contains(name));
self.file_property_write_references.retain(|name, _| file_names.contains(name));
self.file_property_read_references.retain(|name, _| file_names.contains(name));
}
#[inline]
pub fn remove_references_from_invalid_symbols(&mut self, invalid_symbols_and_members: &HashSet<SymbolIdentifier>) {
self.symbol_references_to_symbols
.retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
self.symbol_references_to_symbols_in_signature
.retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
self.symbol_references_to_overridden_members
.retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
self.property_write_references
.retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
self.property_read_references
.retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
}
#[inline]
pub fn retain_safe_symbol_references(
&mut self,
safe_symbols: &WordSet,
safe_symbol_members: &HashSet<SymbolIdentifier>,
) {
let is_safe = |key: &SymbolIdentifier| -> bool {
if key.1.is_empty() { safe_symbols.contains(&key.0) } else { safe_symbol_members.contains(key) }
};
self.symbol_references_to_symbols.retain(|k, _| is_safe(k));
self.symbol_references_to_symbols_in_signature.retain(|k, _| is_safe(k));
self.symbol_references_to_overridden_members.retain(|k, _| is_safe(k));
self.property_write_references.retain(|k, _| is_safe(k));
self.property_read_references.retain(|k, _| is_safe(k));
self.functionlike_references_to_functionlike_returns.retain(|key, _| {
let sym_key = match key {
FunctionLikeIdentifier::Function(name) => (*name, mago_word::empty_word()),
FunctionLikeIdentifier::Method(class, method) => (*class, *method),
_ => return true, };
is_safe(&sym_key)
});
}
pub fn remove_dirty_symbol_references(&mut self, dirty_symbols: &HashSet<SymbolIdentifier>) {
for key in dirty_symbols {
self.symbol_references_to_symbols.remove(key);
self.symbol_references_to_symbols_in_signature.remove(key);
self.symbol_references_to_overridden_members.remove(key);
self.property_write_references.remove(key);
self.property_read_references.remove(key);
let fl_key = if key.1.is_empty() {
FunctionLikeIdentifier::Function(key.0)
} else {
FunctionLikeIdentifier::Method(key.0, key.1)
};
self.functionlike_references_to_functionlike_returns.remove(&fl_key);
}
}
}
fn function_like_symbol_identifier(identifier: &FunctionLikeIdentifier) -> Option<SymbolIdentifier> {
match identifier {
FunctionLikeIdentifier::Function(name) => Some((*name, empty_word())),
FunctionLikeIdentifier::Method(class, method) => Some((*class, *method)),
FunctionLikeIdentifier::Closure(_) => None,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use mago_word::empty_word;
use mago_word::word;
fn make_refs_with_body(entries: Vec<(SymbolIdentifier, Vec<SymbolIdentifier>)>) -> SymbolReferences {
let mut refs = SymbolReferences::new();
for (key, values) in entries {
let set: HashSet<SymbolIdentifier> = values.into_iter().collect();
refs.symbol_references_to_symbols.insert(key, set);
}
refs
}
#[test]
fn test_for_each_reference_visits_every_reference_kind_and_origin() {
let function = (word("consumer"), empty_word());
let signature_function = (word("signature_consumer"), empty_word());
let class = word("service");
let method = (class, word("method"));
let property = (class, word("$property"));
let file_property = (class, word("$file_property"));
let override_source = (word("child"), word("method"));
let return_source = (word("return_consumer"), empty_word());
let file = word("src/file.php");
let mut references = SymbolReferences::new();
assert!(references.is_empty());
references.add_symbol_reference(function, (class, empty_word()), false);
references.add_symbol_reference(signature_function, (class, empty_word()), true);
references.add_property_read_reference(ReferenceOrigin::Symbol(method), property);
references.add_property_write_reference(ReferenceOrigin::Symbol(function), property);
references.add_overridden_member_reference(override_source, method);
references.add_functionlike_return_reference(return_source, method);
references.add_reference(ReferenceOrigin::File(file), method, false);
references.add_file_reference_to_class_member(file, property, true);
references.add_property_read_reference(ReferenceOrigin::File(file), file_property);
references.add_property_write_reference(ReferenceOrigin::File(file), file_property);
assert!(!references.is_empty());
let mut visited = HashSet::default();
references.for_each_reference(|source, target, kind| {
visited.insert((source, target, kind));
});
assert!(visited.contains(&(
ReferenceOrigin::Symbol(function),
(class, empty_word()),
SymbolReferenceKind::Body
)));
assert!(visited.contains(&(
ReferenceOrigin::Symbol(signature_function),
(class, empty_word()),
SymbolReferenceKind::Signature,
)));
assert!(visited.contains(&(ReferenceOrigin::Symbol(method), property, SymbolReferenceKind::PropertyRead)));
assert!(visited.contains(&(ReferenceOrigin::Symbol(function), property, SymbolReferenceKind::PropertyWrite)));
assert!(visited.contains(&(
ReferenceOrigin::Symbol(override_source),
method,
SymbolReferenceKind::OverriddenMember,
)));
assert!(visited.contains(&(
ReferenceOrigin::Symbol(return_source),
method,
SymbolReferenceKind::FunctionLikeReturn,
)));
assert!(visited.contains(&(ReferenceOrigin::File(file), method, SymbolReferenceKind::Body)));
assert!(visited.contains(&(ReferenceOrigin::File(file), property, SymbolReferenceKind::Signature)));
assert!(visited.contains(&(ReferenceOrigin::File(file), file_property, SymbolReferenceKind::PropertyRead)));
assert!(visited.contains(&(ReferenceOrigin::File(file), file_property, SymbolReferenceKind::PropertyWrite)));
assert_eq!(references.count_property_reads(&file_property), 1);
assert_eq!(references.count_property_writes(&file_property), 1);
}
#[test]
fn test_restore_references_for_safe_symbols_restores_missing_body_refs() {
let class_a = word("class_a");
let class_b = word("class_b");
let method_foo = word("foo");
let method_bar = word("bar");
let previous = make_refs_with_body(vec![
((class_a, method_foo), vec![(class_b, empty_word())]),
((class_b, method_bar), vec![(class_a, empty_word())]),
]);
let mut current = make_refs_with_body(vec![((class_b, method_bar), vec![(class_a, empty_word())])]);
let safe_symbols = WordSet::default();
let mut safe_members = HashSet::default();
safe_members.insert((class_a, method_foo));
current.restore_references_for_safe_symbols(&previous, &safe_symbols, &safe_members);
assert!(current.symbol_references_to_symbols.contains_key(&(class_a, method_foo)));
let restored = ¤t.symbol_references_to_symbols[&(class_a, method_foo)];
assert!(restored.contains(&(class_b, empty_word())));
assert!(current.symbol_references_to_symbols.contains_key(&(class_b, method_bar)));
}
#[test]
fn test_restore_references_does_not_overwrite_existing() {
let class_a = word("class_a");
let class_b = word("class_b");
let class_c = word("class_c");
let method_foo = word("foo");
let previous = make_refs_with_body(vec![((class_a, method_foo), vec![(class_b, empty_word())])]);
let mut current = make_refs_with_body(vec![((class_a, method_foo), vec![(class_c, empty_word())])]);
let safe_symbols = WordSet::default();
let mut safe_members = HashSet::default();
safe_members.insert((class_a, method_foo));
current.restore_references_for_safe_symbols(&previous, &safe_symbols, &safe_members);
let refs = ¤t.symbol_references_to_symbols[&(class_a, method_foo)];
assert!(refs.contains(&(class_c, empty_word())));
assert!(!refs.contains(&(class_b, empty_word())));
}
#[test]
fn test_restore_references_for_safe_top_level_symbols() {
let func_a = word("func_a");
let class_b = word("class_b");
let previous = make_refs_with_body(vec![((func_a, empty_word()), vec![(class_b, empty_word())])]);
let mut current = SymbolReferences::new();
let mut safe_symbols = WordSet::default();
safe_symbols.insert(func_a);
let safe_members = HashSet::default();
current.restore_references_for_safe_symbols(&previous, &safe_symbols, &safe_members);
assert!(current.symbol_references_to_symbols.contains_key(&(func_a, empty_word())));
let restored = ¤t.symbol_references_to_symbols[&(func_a, empty_word())];
assert!(restored.contains(&(class_b, empty_word())));
}
#[test]
fn test_restore_skips_non_safe_symbols() {
let func_a = word("func_a");
let class_b = word("class_b");
let previous = make_refs_with_body(vec![((func_a, empty_word()), vec![(class_b, empty_word())])]);
let mut current = SymbolReferences::new();
let safe_symbols = WordSet::default();
let safe_members = HashSet::default();
current.restore_references_for_safe_symbols(&previous, &safe_symbols, &safe_members);
assert!(!current.symbol_references_to_symbols.contains_key(&(func_a, empty_word())));
}
#[test]
fn test_get_invalid_symbols_basic_cascade() {
let class_a = word("class_a");
let class_b = word("class_b");
let method_foo = word("foo");
let mut refs = SymbolReferences::new();
refs.symbol_references_to_symbols_in_signature.insert((class_b, method_foo), {
let mut set = HashSet::default();
set.insert((class_a, empty_word()));
set
});
let mut diff = crate::diff::CodebaseDiff::new();
let mut changed = HashSet::default();
changed.insert((class_a, empty_word()));
diff = diff.with_changed(changed);
let result = refs.get_invalid_symbols(&diff);
assert!(result.is_some());
let (invalid, partially_invalid, invalid_files) = result.unwrap();
assert!(invalid.contains(&(class_a, empty_word())));
assert!(invalid.contains(&(class_b, method_foo)));
assert!(partially_invalid.contains(&class_b));
assert!(invalid_files.is_empty());
}
#[test]
fn test_get_invalid_symbols_tracks_file_origins() {
let changed_class = word("changed_class");
let file = word("src/bootstrap.php");
let mut references = SymbolReferences::new();
references.add_reference(ReferenceOrigin::File(file), (changed_class, empty_word()), false);
let mut diff = crate::diff::CodebaseDiff::new();
let mut changed = HashSet::default();
changed.insert((changed_class, empty_word()));
diff = diff.with_changed(changed);
let (_, _, invalid_files) = references.get_invalid_symbols(&diff).expect("invalidation should complete");
assert_eq!(invalid_files.len(), 1);
assert!(invalid_files.contains(&file));
}
}