use super::*;
impl<'a> ClassAnalyzer<'a> {
pub(super) fn check_circular_class_inheritance(&self, issues: &mut Vec<Issue>) {
let mut globally_done: HashSet<String> = HashSet::default();
let mut class_keys: Vec<Arc<str>> = crate::db::workspace_classes(self.db)
.iter()
.filter(|fqcn| {
let here = crate::db::Fqcn::from_str(self.db, fqcn.as_ref());
crate::db::find_class_like(self.db, here)
.map(|c| c.is_class())
.unwrap_or(false)
})
.cloned()
.collect();
class_keys.sort();
for start_fqcn in &class_keys {
if globally_done.contains(start_fqcn.as_ref()) {
continue;
}
let mut chain: Vec<Arc<str>> = Vec::new();
let mut chain_set: HashSet<String> = HashSet::default();
let mut current: Arc<str> = start_fqcn.clone();
loop {
if globally_done.contains(current.as_ref()) {
for node in &chain {
globally_done.insert(node.to_string());
}
break;
}
if !chain_set.insert(current.to_string()) {
let cycle_start = chain
.iter()
.position(|p| p.as_ref() == current.as_ref())
.unwrap_or(0);
let cycle_nodes = &chain[cycle_start..];
let offender = cycle_nodes
.iter()
.filter(|n| self.class_in_analyzed_files(n))
.max_by(|a, b| a.as_ref().cmp(b.as_ref()));
if let Some(offender) = offender {
let here = crate::db::Fqcn::from_str(self.db, offender.as_ref());
let location: Option<Location> = crate::db::find_class_like(self.db, here)
.and_then(|c| c.location().cloned());
let loc = issue_location(
location.as_ref(),
location
.as_ref()
.and_then(|l| self.sources.get(&l.file).copied()),
);
let mut issue = Issue::new(
IssueKind::CircularInheritance {
class: offender.to_string(),
},
loc,
);
if let Some(snippet) = extract_snippet(location.as_ref(), &self.sources) {
issue = issue.with_snippet(snippet);
}
issues.push(issue);
}
for node in &chain {
globally_done.insert(node.to_string());
}
break;
}
chain.push(current.clone());
let here = crate::db::Fqcn::from_str(self.db, current.as_ref());
let parent: Option<Arc<str>> =
crate::db::find_class_like(self.db, here).and_then(|c| c.parent().cloned());
match parent {
Some(p) => current = p,
None => {
for node in &chain {
globally_done.insert(node.to_string());
}
break;
}
}
}
}
}
pub(super) fn check_circular_interface_inheritance(&self, issues: &mut Vec<Issue>) {
let mut globally_done: HashSet<String> = HashSet::default();
let mut iface_keys: Vec<Arc<str>> = crate::db::workspace_classes(self.db)
.iter()
.filter(|fqcn| {
let here = crate::db::Fqcn::from_str(self.db, fqcn.as_ref());
crate::db::find_class_like(self.db, here)
.map(|c| c.is_interface())
.unwrap_or(false)
})
.cloned()
.collect();
iface_keys.sort();
for start_fqcn in &iface_keys {
if globally_done.contains(start_fqcn.as_ref()) {
continue;
}
let mut in_stack: Vec<Arc<str>> = Vec::new();
let mut stack_set: HashSet<String> = HashSet::default();
self.dfs_interface_cycle(
start_fqcn.clone(),
&mut in_stack,
&mut stack_set,
&mut globally_done,
issues,
);
}
}
fn dfs_interface_cycle(
&self,
fqcn: Arc<str>,
in_stack: &mut Vec<Arc<str>>,
stack_set: &mut HashSet<String>,
globally_done: &mut HashSet<String>,
issues: &mut Vec<Issue>,
) {
if globally_done.contains(fqcn.as_ref()) {
return;
}
if stack_set.contains(fqcn.as_ref()) {
let cycle_start = in_stack
.iter()
.position(|p| p.as_ref() == fqcn.as_ref())
.unwrap_or(0);
let cycle_nodes = &in_stack[cycle_start..];
let offender = cycle_nodes
.iter()
.filter(|n| self.iface_in_analyzed_files(n))
.max_by(|a, b| a.as_ref().cmp(b.as_ref()));
if let Some(offender) = offender {
let here = crate::db::Fqcn::from_str(self.db, offender.as_ref());
let location =
crate::db::find_class_like(self.db, here).and_then(|c| c.location().cloned());
let loc = issue_location(
location.as_ref(),
location
.as_ref()
.and_then(|l| self.sources.get(&l.file).copied()),
);
let mut issue = Issue::new(
IssueKind::CircularInheritance {
class: offender.to_string(),
},
loc,
);
if let Some(snippet) = extract_snippet(location.as_ref(), &self.sources) {
issue = issue.with_snippet(snippet);
}
issues.push(issue);
}
return;
}
stack_set.insert(fqcn.to_string());
in_stack.push(fqcn.clone());
let here = crate::db::Fqcn::from_str(self.db, fqcn.as_ref());
let extends: Vec<Arc<str>> = crate::db::find_class_like(self.db, here)
.map(|c| c.extends().to_vec())
.unwrap_or_default();
for parent in extends {
self.dfs_interface_cycle(parent, in_stack, stack_set, globally_done, issues);
}
in_stack.pop();
stack_set.remove(fqcn.as_ref());
globally_done.insert(fqcn.to_string());
}
fn class_in_analyzed_files(&self, fqcn: &Arc<str>) -> bool {
if self.analyzed_files.is_empty() {
return true;
}
let here = crate::db::Fqcn::from_str(self.db, fqcn.as_ref());
crate::db::find_class_like(self.db, here)
.and_then(|c| c.location().cloned())
.map(|loc| self.analyzed_files.contains(&loc.file))
.unwrap_or(false)
}
fn iface_in_analyzed_files(&self, fqcn: &Arc<str>) -> bool {
self.class_in_analyzed_files(fqcn)
}
pub(super) fn check_missing_constructor(
&self,
fqcn: &Arc<str>,
location: Option<&Location>,
issues: &mut Vec<Issue>,
) {
let here = crate::db::Fqcn::from_str(self.db, fqcn.as_ref());
if crate::db::find_method_in_chain(self.db, here, "__construct").is_some() {
return;
}
let ancestors = crate::db::class_ancestors_by_fqcn(self.db, here);
let has_uninitialized = ancestors.iter().any(|ancestor| {
let anc_here = crate::db::Fqcn::from_str(self.db, ancestor.as_ref());
if let Some(class) = crate::db::find_class_like(self.db, anc_here) {
if let Some(props) = class.own_properties() {
return props.values().any(|p| {
p.has_native_type
&& p.default.is_none()
&& p.ty.as_deref().is_some_and(|ty| !ty.is_nullable())
});
}
}
false
});
if !has_uninitialized {
return;
}
let loc = issue_location(
location,
location.and_then(|l| self.sources.get(&l.file).copied()),
);
let mut issue = Issue::new(
IssueKind::MissingConstructor {
class: fqcn.to_string(),
},
loc,
);
if let Some(snippet) = extract_snippet(location, &self.sources) {
issue = issue.with_snippet(snippet);
}
issues.push(issue);
}
}