use foldhash::HashMap;
use foldhash::HashSet;
use mago_allocator::Arena;
use mago_codex::metadata::CodebaseMetadata;
use mago_codex::metadata::class_like::ClassLikeMetadata;
use mago_codex::reference::ReferenceOrigin;
use mago_codex::reference::SymbolReferenceKind;
use mago_codex::reference::SymbolReferences;
use mago_codex::symbol::SymbolIdentifier;
use mago_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasSpan;
use mago_span::Span;
use mago_text_edit::Safety;
use mago_text_edit::TextEdit;
use mago_word::Word;
use crate::code::IssueCode;
use crate::context::Context;
#[derive(Clone)]
struct CheckableMember {
symbol_id: SymbolIdentifier,
display_name: Word,
span: Span,
is_property: bool,
}
#[derive(Debug)]
pub(crate) struct UnusedMemberSpans {
pub unused_methods: HashSet<Span>,
pub unused_properties: HashSet<Span>,
pub read_properties: HashSet<Span>,
}
pub fn check_unused_members_with_transitivity<A>(
class_name: Word,
class_span: Span,
class_like_metadata: &ClassLikeMetadata,
symbol_references: &SymbolReferences,
additional_symbol_references: Option<&SymbolReferences>,
context: &mut Context<'_, '_, A>,
) -> HashSet<SymbolIdentifier>
where
A: Arena,
{
let checkable_members = collect_checkable_members(class_name, class_like_metadata, context.codebase);
let unused_members = find_unused_members(&checkable_members, symbol_references, additional_symbol_references);
for member in &checkable_members {
if unused_members.contains(&member.symbol_id) {
if member.is_property {
report_unused_property(context, class_span, member.display_name, member.span);
} else {
report_unused_method(context, class_span, member.display_name, member.span);
}
}
}
unused_members
}
fn collect_checkable_members(
class_name: Word,
class_like_metadata: &ClassLikeMetadata,
codebase: &CodebaseMetadata,
) -> Vec<CheckableMember> {
if class_like_metadata.kind.is_trait() || class_like_metadata.kind.is_interface() {
return Vec::new();
}
let is_final = class_like_metadata.flags.is_final() || class_like_metadata.kind.is_enum();
let mut checkable_members = Vec::new();
for (property_name, property) in &class_like_metadata.properties {
if let Some(declaring_class) = class_like_metadata.declaring_property_ids.get(property_name)
&& *declaring_class != class_name
{
continue;
}
if property.read_visibility.is_public() {
continue;
}
if property.read_visibility.is_protected() && !is_final {
continue;
}
if property_name.as_bytes().starts_with(b"$_") {
continue;
}
if !property.hooks.is_empty() {
continue;
}
if class_like_metadata.magic_property_ids.contains_key(property_name) {
continue;
}
if !property.read_visibility.is_private()
&& class_like_metadata.overridden_property_ids.contains_key(property_name)
{
continue;
}
if class_like_metadata
.used_traits
.iter()
.any(|trait_name| codebase.property_exists(trait_name.as_bytes(), property_name.as_bytes()))
{
continue;
}
if let Some(property_span) = property.name_span.or(property.span) {
checkable_members.push(CheckableMember {
symbol_id: (class_name, *property_name),
display_name: *property_name,
span: property_span,
is_property: true,
});
}
}
for method_name in &class_like_metadata.methods {
if let Some(declaring_method_id) = class_like_metadata.declaring_method_ids.get(method_name)
&& declaring_method_id.get_class_name() != class_name
{
continue;
}
let Some(method_metadata) = codebase.function_likes.get(&(class_name, *method_name)) else {
continue;
};
let Some(method_meta) = &method_metadata.method_metadata else {
continue;
};
if method_meta.visibility.is_public() {
continue;
}
if method_meta.visibility.is_protected() && !is_final {
continue;
}
if method_name.as_bytes().starts_with(b"_") {
continue;
}
if method_meta.is_abstract {
continue;
}
if class_like_metadata.overridden_method_ids.contains_key(method_name) {
continue;
}
if class_like_metadata
.used_traits
.iter()
.any(|trait_name| codebase.method_exists(trait_name.as_bytes(), method_name.as_bytes()))
{
continue;
}
let method_span = method_metadata.name_span.unwrap_or(method_metadata.span);
checkable_members.push(CheckableMember {
symbol_id: (class_name, *method_name),
display_name: method_metadata.original_name,
span: method_span,
is_property: false,
});
}
checkable_members
}
fn find_unused_members(
checkable_members: &[CheckableMember],
symbol_references: &SymbolReferences,
additional_symbol_references: Option<&SymbolReferences>,
) -> HashSet<SymbolIdentifier> {
let mut unused_members: HashSet<SymbolIdentifier> = HashSet::default();
for member in checkable_members {
if !is_member_referenced(symbol_references, additional_symbol_references, &member.symbol_id) {
unused_members.insert(member.symbol_id);
}
}
let checkable_set: HashSet<SymbolIdentifier> = checkable_members.iter().map(|member| member.symbol_id).collect();
loop {
let mut newly_unused: HashSet<SymbolIdentifier> = HashSet::default();
for member in checkable_members {
if unused_members.contains(&member.symbol_id) {
continue;
}
if all_references_from_unused(
symbol_references,
additional_symbol_references,
&member.symbol_id,
&unused_members,
&checkable_set,
) {
newly_unused.insert(member.symbol_id);
}
}
if newly_unused.is_empty() {
break;
}
unused_members.extend(newly_unused);
}
unused_members
}
pub(crate) fn find_unused_member_spans(
codebase: &CodebaseMetadata,
symbol_references: &SymbolReferences,
) -> UnusedMemberSpans {
let mut referencing_symbols = HashMap::<SymbolIdentifier, HashSet<SymbolIdentifier>>::default();
let mut externally_referenced = HashSet::<SymbolIdentifier>::default();
let mut read_properties = HashSet::<SymbolIdentifier>::default();
symbol_references.for_each_reference(|source, target, kind| {
if matches!(kind, SymbolReferenceKind::Body | SymbolReferenceKind::Signature) {
match source {
ReferenceOrigin::Symbol(source) => {
referencing_symbols.entry(target).or_default().insert(source);
}
ReferenceOrigin::File(_) => {
externally_referenced.insert(target);
}
}
} else if kind == SymbolReferenceKind::PropertyRead {
read_properties.insert(target);
}
});
let mut spans = UnusedMemberSpans {
unused_methods: HashSet::default(),
unused_properties: HashSet::default(),
read_properties: HashSet::default(),
};
for (class_name, class_like_metadata) in &codebase.class_likes {
let checkable_members = collect_checkable_members(*class_name, class_like_metadata, codebase);
let unused_members =
find_unused_members_from_index(&checkable_members, &referencing_symbols, &externally_referenced);
for member in checkable_members {
if unused_members.contains(&member.symbol_id) {
if member.is_property {
spans.unused_properties.insert(member.span);
} else {
spans.unused_methods.insert(member.span);
}
}
}
for (property_name, property) in &class_like_metadata.properties {
let symbol_id = (*class_name, *property_name);
if read_properties.contains(&symbol_id)
&& let Some(property_span) = property.name_span.or(property.span)
{
spans.read_properties.insert(property_span);
}
}
}
spans
}
fn find_unused_members_from_index(
checkable_members: &[CheckableMember],
referencing_symbols: &HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
externally_referenced: &HashSet<SymbolIdentifier>,
) -> HashSet<SymbolIdentifier> {
let checkable_set = checkable_members.iter().map(|member| member.symbol_id).collect::<HashSet<_>>();
let mut unused_members = checkable_members
.iter()
.filter(|member| {
!externally_referenced.contains(&member.symbol_id)
&& referencing_symbols.get(&member.symbol_id).is_none_or(HashSet::is_empty)
})
.map(|member| member.symbol_id)
.collect::<HashSet<_>>();
loop {
let mut newly_unused = HashSet::default();
for member in checkable_members {
if unused_members.contains(&member.symbol_id) || externally_referenced.contains(&member.symbol_id) {
continue;
}
let all_references_are_unused = referencing_symbols.get(&member.symbol_id).is_none_or(|references| {
references.iter().all(|source| checkable_set.contains(source) && unused_members.contains(source))
});
if all_references_are_unused {
newly_unused.insert(member.symbol_id);
}
}
if newly_unused.is_empty() {
break;
}
unused_members.extend(newly_unused);
}
unused_members
}
fn all_references_from_unused(
symbol_references: &SymbolReferences,
additional_symbol_references: Option<&SymbolReferences>,
symbol_id: &SymbolIdentifier,
unused_members: &HashSet<SymbolIdentifier>,
checkable_set: &HashSet<SymbolIdentifier>,
) -> bool {
all_references_from_unused_in(symbol_references, symbol_id, unused_members, checkable_set)
&& additional_symbol_references.is_none_or(|references| {
all_references_from_unused_in(references, symbol_id, unused_members, checkable_set)
})
}
fn all_references_from_unused_in(
symbol_references: &SymbolReferences,
symbol_id: &SymbolIdentifier,
unused_members: &HashSet<SymbolIdentifier>,
checkable_set: &HashSet<SymbolIdentifier>,
) -> bool {
if symbol_references.has_file_reference_to_symbol(*symbol_id) {
return false;
}
let referencing_symbols = symbol_references.get_references_to_symbol(*symbol_id);
if referencing_symbols.is_empty() {
return true;
}
for referencing in referencing_symbols {
if checkable_set.contains(referencing) {
if !unused_members.contains(referencing) {
return false;
}
} else {
return false;
}
}
true
}
pub fn check_write_only_properties<A>(
class_name: Word,
class_span: Span,
class_like_metadata: &ClassLikeMetadata,
symbol_references: &SymbolReferences,
additional_symbol_references: Option<&SymbolReferences>,
unused_members: &HashSet<SymbolIdentifier>,
context: &mut Context<'_, '_, A>,
) where
A: Arena,
{
if class_like_metadata.kind.is_trait() {
return;
}
if class_like_metadata.kind.is_interface() {
return;
}
let is_final = class_like_metadata.flags.is_final() || class_like_metadata.kind.is_enum();
for (property_name, property) in &class_like_metadata.properties {
if let Some(declaring_class) = class_like_metadata.declaring_property_ids.get(property_name)
&& *declaring_class != class_name
{
continue;
}
if property.read_visibility.is_public() {
continue;
}
if property.read_visibility.is_protected() && !is_final {
continue;
}
if property_name.as_bytes().starts_with(b"$_") {
continue;
}
if !property.hooks.is_empty() {
continue;
}
if class_like_metadata.magic_property_ids.contains_key(property_name) {
continue;
}
let symbol_id: SymbolIdentifier = (class_name, *property_name);
if unused_members.contains(&symbol_id) {
continue;
}
if !is_member_referenced(symbol_references, additional_symbol_references, &symbol_id) {
continue;
}
let has_reads = symbol_references.count_property_reads(&symbol_id) > 0
|| additional_symbol_references.is_some_and(|references| references.count_property_reads(&symbol_id) > 0);
let has_writes = symbol_references.count_property_writes(&symbol_id) > 0
|| additional_symbol_references.is_some_and(|references| references.count_property_writes(&symbol_id) > 0);
if has_writes
&& !has_reads
&& let Some(property_span) = property.name_span.or(property.span)
{
report_write_only_property(context, class_span, *property_name, property_span);
}
}
}
#[inline(always)]
fn is_member_referenced(
symbol_references: &SymbolReferences,
additional_symbol_references: Option<&SymbolReferences>,
symbol_id: &SymbolIdentifier,
) -> bool {
is_member_referenced_in(symbol_references, symbol_id)
|| additional_symbol_references.is_some_and(|references| is_member_referenced_in(references, symbol_id))
}
#[inline(always)]
fn is_member_referenced_in(symbol_references: &SymbolReferences, symbol_id: &SymbolIdentifier) -> bool {
symbol_references.count_referencing_symbols(symbol_id, false) > 0
|| symbol_references.count_referencing_symbols(symbol_id, true) > 0
}
fn report_unused_property<A>(
context: &mut Context<'_, '_, A>,
class_span: Span,
property_name: Word,
property_span: Span,
) where
A: Arena,
{
let issue = Issue::help(format!("Property `{property_name}` is never used."))
.with_code(IssueCode::UnusedProperty)
.with_annotations([
Annotation::primary(property_span).with_message(format!("Property `{property_name}` is declared here.")),
Annotation::secondary(class_span),
])
.with_note("This property is declared but never read or written within the class.")
.with_help(
"Consider prefixing the property with an underscore (`$_`) to indicate that it is intentionally unused, or remove it if it is not needed.",
);
context.collector.propose(issue, |edits| {
edits.push(TextEdit::insert(property_span.start_offset() + 1, "_").with_safety(Safety::PotentiallyUnsafe));
});
}
fn report_write_only_property<A>(
context: &mut Context<'_, '_, A>,
class_span: Span,
property_name: Word,
property_span: Span,
) where
A: Arena,
{
let issue = Issue::help(format!("Property `{property_name}` is written to but never read."))
.with_code(IssueCode::WriteOnlyProperty)
.with_annotations([
Annotation::primary(property_span).with_message(format!("Property `{property_name}` is declared here.")),
Annotation::secondary(class_span),
])
.with_note("This property is assigned values but its value is never read within the class.")
.with_help(
"Consider removing the property if its value is not needed, or read the value somewhere if it should be used.",
);
context.collector.report(issue);
}
fn report_unused_method<A>(context: &mut Context<'_, '_, A>, class_span: Span, method_name: Word, method_span: Span)
where
A: Arena,
{
let issue = Issue::help(format!("Method `{method_name}()` is never used."))
.with_code(IssueCode::UnusedMethod)
.with_annotations([
Annotation::primary(method_span).with_message(format!("Method `{method_name}()` is declared here.")),
Annotation::secondary(class_span),
])
.with_note("This method is declared but never called within the class.")
.with_help(
"Consider prefixing the method with an underscore (`_`) to indicate that it is intentionally unused, or remove it if it is not needed.",
);
context.collector.propose(issue, |edits| {
edits.push(TextEdit::insert(method_span.start_offset(), "_").with_safety(Safety::PotentiallyUnsafe));
});
}