use std::collections::{HashMap, HashSet};
use std::sync::Arc;
#[cfg(test)]
use std::borrow::Cow;
pub(crate) enum ClassRef<'a> {
Borrowed(&'a ClassInfo),
Owned(Arc<ClassInfo>),
}
impl std::ops::Deref for ClassRef<'_> {
type Target = ClassInfo;
#[inline]
fn deref(&self) -> &ClassInfo {
match self {
ClassRef::Borrowed(r) => r,
ClassRef::Owned(a) => a,
}
}
}
pub(crate) struct TraitContext<'a> {
pub use_generics: &'a [(String, Vec<PhpType>)],
pub precedences: &'a [TraitPrecedence],
pub aliases: &'a [TraitAlias],
}
pub(crate) struct MergeDedup {
pub methods: HashSet<String>,
pub properties: HashSet<String>,
pub constants: HashSet<String>,
}
impl MergeDedup {
fn from_class(class: &ClassInfo) -> Self {
Self {
methods: class.methods.iter().map(|m| m.name.clone()).collect(),
properties: class.properties.iter().map(|p| p.name.clone()).collect(),
constants: class.constants.iter().map(|c| c.name.clone()).collect(),
}
}
}
use crate::php_type::PhpType;
use crate::types::{
ClassInfo, MAX_INHERITANCE_DEPTH, MAX_TRAIT_DEPTH, MethodInfo, ParameterInfo, PropertyInfo,
TraitAlias, TraitPrecedence, Visibility,
};
use crate::util::short_name;
use crate::virtual_members::laravel::{
extends_eloquent_model, factory_to_model_fqn, model_to_factory_fqn,
};
fn lacks_docblock_override(effective: &Option<PhpType>, native: &Option<PhpType>) -> bool {
match (effective, native) {
(None, _) => true,
(Some(_), None) => false,
(Some(eff), Some(nat)) => eff.equivalent(nat),
}
}
fn ancestor_has_richer_type(effective: &Option<PhpType>, native: &Option<PhpType>) -> bool {
match (effective, native) {
(Some(_), None) => true,
(Some(eff), Some(nat)) => !eff.equivalent(nat),
_ => false,
}
}
pub(crate) fn enrich_method_from_ancestor(existing: &mut MethodInfo, ancestor: &MethodInfo) {
if existing.return_type.is_none() && ancestor.return_type.is_some()
|| lacks_docblock_override(&existing.return_type, &existing.native_return_type)
&& ancestor_has_richer_type(&ancestor.return_type, &ancestor.native_return_type)
{
existing.return_type = ancestor.return_type.clone();
}
if existing.template_params.is_empty() && !ancestor.template_params.is_empty() {
existing.template_params = ancestor.template_params.clone();
existing.template_param_bounds = ancestor.template_param_bounds.clone();
existing.template_bindings = ancestor.template_bindings.clone();
if existing.return_type.is_none() {
existing.return_type = ancestor.return_type.clone();
}
}
if existing.conditional_return.is_none() && ancestor.conditional_return.is_some() {
existing.conditional_return = ancestor.conditional_return.clone();
}
if existing.type_assertions.is_empty() && !ancestor.type_assertions.is_empty() {
existing.type_assertions = ancestor.type_assertions.clone();
}
enrich_parameters_from_ancestor(&mut existing.parameters, &ancestor.parameters);
if existing.description.is_none() && ancestor.description.is_some() {
existing.description = ancestor.description.clone();
}
if existing.return_description.is_none() && ancestor.return_description.is_some() {
existing.return_description = ancestor.return_description.clone();
}
}
fn enrich_parameters_from_ancestor(
existing_params: &mut [ParameterInfo],
ancestor_params: &[ParameterInfo],
) {
for (existing_param, ancestor_param) in existing_params.iter_mut().zip(ancestor_params) {
if lacks_docblock_override(&existing_param.type_hint, &existing_param.native_type_hint)
&& ancestor_param.type_hint.is_some()
{
existing_param.type_hint = ancestor_param.type_hint.clone();
}
if existing_param.description.is_none() && ancestor_param.description.is_some() {
existing_param.description = ancestor_param.description.clone();
}
}
}
pub(crate) fn enrich_property_from_ancestor(existing: &mut PropertyInfo, ancestor: &PropertyInfo) {
if existing.type_hint.is_none() && ancestor.type_hint.is_some()
|| lacks_docblock_override(&existing.type_hint, &existing.native_type_hint)
&& ancestor_has_richer_type(&ancestor.type_hint, &ancestor.native_type_hint)
{
existing.type_hint = ancestor.type_hint.clone();
}
if existing.description.is_none() && ancestor.description.is_some() {
existing.description = ancestor.description.clone();
}
}
pub(crate) fn resolve_class_with_inheritance(
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> ClassInfo {
let mut merged = class.clone();
let mut dedup = MergeDedup::from_class(&merged);
merge_traits_into(
&mut merged,
&class.used_traits,
&TraitContext {
use_generics: &class.use_generics,
precedences: &class.trait_precedences,
aliases: &class.trait_aliases,
},
class_loader,
0,
&mut dedup,
);
let mut current: ClassRef<'_> = ClassRef::Borrowed(class);
let mut depth = 0;
let mut active_subs: HashMap<String, PhpType> = HashMap::new();
while let Some(ref parent_name) = current.parent_class {
depth += 1;
if depth > MAX_INHERITANCE_DEPTH {
break;
}
let parent = if let Some(p) = class_loader(parent_name) {
p
} else {
break;
};
let mut level_subs = build_substitution_map(¤t, &parent, &active_subs);
if level_subs.is_empty()
&& !parent.template_params.is_empty()
&& is_factory_class(parent_name)
{
let factory_fqn = current.fqn();
if let Some(model_fqn) = factory_to_model_fqn(&factory_fqn)
&& class_loader(&model_fqn).is_some()
{
for param in &parent.template_params {
level_subs.insert(param.clone(), PhpType::Named(model_fqn.clone()));
}
}
}
let substituted_use_generics: Vec<(String, Vec<PhpType>)> = if level_subs.is_empty() {
parent.use_generics.clone()
} else {
parent
.use_generics
.iter()
.map(|(name, args)| {
let substituted_args: Vec<PhpType> =
args.iter().map(|arg| arg.substitute(&level_subs)).collect();
(name.clone(), substituted_args)
})
.collect()
};
merge_traits_into(
&mut merged,
&parent.used_traits,
&TraitContext {
use_generics: &substituted_use_generics,
precedences: &parent.trait_precedences,
aliases: &parent.trait_aliases,
},
class_loader,
0,
&mut dedup,
);
for method in &parent.methods {
if method.visibility == Visibility::Private {
continue;
}
let mut ancestor_method = method.clone();
if !level_subs.is_empty() {
apply_substitution_to_method(&mut ancestor_method, &level_subs);
}
if !dedup.methods.insert(method.name.clone()) {
if let Some(existing) = merged
.methods
.make_mut()
.iter_mut()
.find(|m| m.name == method.name)
{
enrich_method_from_ancestor(existing, &ancestor_method);
}
continue;
}
merged.methods.push(ancestor_method);
}
for property in &parent.properties {
if property.visibility == Visibility::Private {
continue;
}
let mut ancestor_property = property.clone();
if !level_subs.is_empty() {
apply_substitution_to_property(&mut ancestor_property, &level_subs);
}
if !dedup.properties.insert(property.name.clone()) {
if let Some(existing) = merged
.properties
.make_mut()
.iter_mut()
.find(|p| p.name == property.name)
{
enrich_property_from_ancestor(existing, &ancestor_property);
}
continue;
}
merged.properties.push(ancestor_property);
}
for constant in &parent.constants {
if constant.visibility == Visibility::Private {
continue;
}
if !dedup.constants.insert(constant.name.clone()) {
continue;
}
merged.constants.push(constant.clone());
}
active_subs = level_subs;
current = ClassRef::Owned(parent);
}
merged
}
pub(crate) fn resolve_method_return_type(
class: &ClassInfo,
method_name: &str,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> Option<PhpType> {
let merged = crate::virtual_members::resolve_class_fully(class, class_loader);
merged
.methods
.iter()
.find(|m| m.name == method_name)
.and_then(|m| m.return_type.clone())
}
pub(crate) fn resolve_property_type_hint(
class: &ClassInfo,
prop_name: &str,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> Option<PhpType> {
let merged = crate::virtual_members::resolve_class_fully(class, class_loader);
merged
.properties
.iter()
.find(|p| p.name == prop_name)
.and_then(|p| p.type_hint.clone())
}
fn merge_traits_into(
merged: &mut ClassInfo,
trait_names: &[String],
ctx: &TraitContext<'_>,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
depth: u32,
dedup: &mut MergeDedup,
) {
if depth > MAX_TRAIT_DEPTH {
return;
}
for trait_name in trait_names {
let trait_info = if let Some(t) = class_loader(trait_name) {
t
} else {
continue;
};
let mut trait_subs =
build_trait_substitution_map(trait_name, &trait_info, ctx.use_generics);
if trait_subs.is_empty()
&& !trait_info.template_params.is_empty()
&& is_has_factory_trait(trait_name)
&& extends_eloquent_model(merged, class_loader)
{
let model_fqn = merged.fqn();
let factory_fqn = model_to_factory_fqn(&model_fqn);
if class_loader(&factory_fqn).is_some() {
for param in &trait_info.template_params {
trait_subs.insert(param.clone(), PhpType::Named(factory_fqn.clone()));
}
}
}
if !trait_info.used_traits.is_empty() {
merge_traits_into(
merged,
&trait_info.used_traits,
&TraitContext {
use_generics: &trait_info.use_generics,
precedences: &trait_info.trait_precedences,
aliases: &trait_info.trait_aliases,
},
class_loader,
depth + 1,
dedup,
);
}
let mut current = trait_info.clone();
let mut parent_depth = depth;
while let Some(ref parent_name) = current.parent_class {
parent_depth += 1;
if parent_depth > MAX_TRAIT_DEPTH {
break;
}
let parent = if let Some(p) = class_loader(parent_name) {
p
} else {
break;
};
if !parent.used_traits.is_empty() {
merge_traits_into(
merged,
&parent.used_traits,
&TraitContext {
use_generics: &parent.use_generics,
precedences: &parent.trait_precedences,
aliases: &parent.trait_aliases,
},
class_loader,
parent_depth + 1,
dedup,
);
}
for method in &parent.methods {
if method.visibility == Visibility::Private {
continue;
}
if !dedup.methods.insert(method.name.clone()) {
continue;
}
merged.methods.push(method.clone());
}
for property in &parent.properties {
if property.visibility == Visibility::Private {
continue;
}
if !dedup.properties.insert(property.name.clone()) {
continue;
}
merged.properties.push(property.clone());
}
for constant in &parent.constants {
if constant.visibility == Visibility::Private {
continue;
}
if !dedup.constants.insert(constant.name.clone()) {
continue;
}
merged.constants.push(constant.clone());
}
current = parent;
}
for method in &trait_info.methods {
let excluded = ctx.precedences.iter().any(|p| {
p.method_name == method.name
&& p.insteadof.iter().any(|excluded_trait| {
excluded_trait == trait_name
|| short_name(excluded_trait) == short_name(trait_name)
})
});
if excluded {
continue;
}
if !dedup.methods.insert(method.name.clone()) {
continue;
}
let mut method = method.clone();
for alias in ctx.aliases {
if alias.method_name == method.name
&& alias.alias.is_none()
&& let Some(vis) = alias.visibility
{
let name_matches = alias
.trait_name
.as_ref()
.is_none_or(|t| t == trait_name || short_name(t) == short_name(trait_name));
if name_matches {
method.visibility = vis;
}
}
}
if !trait_subs.is_empty() {
apply_substitution_to_method(&mut method, &trait_subs);
}
merged.methods.push(method);
}
for property in &trait_info.properties {
if !dedup.properties.insert(property.name.clone()) {
continue;
}
let mut property = property.clone();
if !trait_subs.is_empty() {
apply_substitution_to_property(&mut property, &trait_subs);
}
merged.properties.push(property);
}
for constant in &trait_info.constants {
if !dedup.constants.insert(constant.name.clone()) {
continue;
}
merged.constants.push(constant.clone());
}
for alias in ctx.aliases {
let alias_name = match &alias.alias {
Some(name) => name,
None => continue,
};
let name_matches = alias
.trait_name
.as_ref()
.is_none_or(|t| t == trait_name || short_name(t) == short_name(trait_name));
if !name_matches {
continue;
}
let source_method = trait_info
.methods
.iter()
.find(|m| m.name == alias.method_name);
let source_method = match source_method {
Some(m) => m,
None => continue,
};
if !dedup.methods.insert(alias_name.clone()) {
continue;
}
let mut aliased = source_method.clone();
aliased.name = alias_name.clone();
if let Some(vis) = alias.visibility {
aliased.visibility = vis;
}
if !trait_subs.is_empty() {
apply_substitution_to_method(&mut aliased, &trait_subs);
}
merged.methods.push(aliased);
}
}
}
fn is_has_factory_trait(trait_name: &str) -> bool {
trait_name == "Illuminate\\Database\\Eloquent\\Factories\\HasFactory"
|| trait_name == "HasFactory"
}
fn is_factory_class(class_name: &str) -> bool {
class_name == "Illuminate\\Database\\Eloquent\\Factories\\Factory" || class_name == "Factory"
}
fn build_trait_substitution_map(
trait_name: &str,
trait_info: &ClassInfo,
use_generics: &[(String, Vec<PhpType>)],
) -> HashMap<String, PhpType> {
if trait_info.template_params.is_empty() || use_generics.is_empty() {
return HashMap::new();
}
let trait_short = short_name(trait_name);
let type_args = use_generics
.iter()
.find(|(name, _)| {
let name_short = short_name(name);
name_short == trait_short
})
.map(|(_, args)| args);
let type_args = match type_args {
Some(args) => args,
None => return HashMap::new(),
};
let mut map = HashMap::new();
for (i, param_name) in trait_info.template_params.iter().enumerate() {
if let Some(arg) = type_args.get(i) {
map.insert(param_name.clone(), arg.clone());
}
}
map
}
fn build_substitution_map(
current: &ClassInfo,
parent: &ClassInfo,
active_subs: &HashMap<String, PhpType>,
) -> HashMap<String, PhpType> {
if parent.template_params.is_empty() {
return active_subs.clone();
}
let parent_short = short_name(&parent.name);
let type_args = current
.extends_generics
.iter()
.chain(current.implements_generics.iter())
.find(|(name, _)| {
let name_short = short_name(name);
name_short == parent_short
})
.map(|(_, args)| args);
let type_args = match type_args {
Some(args) => args,
None => {
return active_subs.clone();
}
};
let mut map = HashMap::new();
for (i, param_name) in parent.template_params.iter().enumerate() {
if let Some(arg) = type_args.get(i) {
let resolved = if active_subs.is_empty() {
arg.clone()
} else {
arg.substitute(active_subs)
};
map.insert(param_name.clone(), resolved);
}
}
map
}
pub(crate) fn apply_substitution_to_method(
method: &mut MethodInfo,
subs: &HashMap<String, PhpType>,
) {
if let Some(ref mut ret) = method.return_type {
*ret = ret.substitute(subs);
}
if let Some(ref mut cond) = method.conditional_return {
apply_substitution_to_conditional(cond, subs);
}
for param in &mut method.parameters {
if let Some(ref mut hint) = param.type_hint {
*hint = hint.substitute(subs);
}
}
}
pub(crate) fn apply_substitution_to_conditional(
cond: &mut PhpType,
subs: &HashMap<String, PhpType>,
) {
*cond = cond.substitute(subs);
}
pub(crate) fn apply_substitution_to_property(
property: &mut PropertyInfo,
subs: &HashMap<String, PhpType>,
) {
if let Some(ref mut hint) = property.type_hint {
*hint = hint.substitute(subs);
}
}
#[cfg(test)]
pub(crate) fn apply_substitution<'a>(
type_str: &'a str,
subs: &HashMap<String, PhpType>,
) -> Cow<'a, str> {
let s = type_str.trim();
if s.is_empty() || subs.is_empty() {
return Cow::Borrowed(s);
}
if !subs.keys().any(|key| s.contains(key.as_str())) {
return Cow::Borrowed(s);
}
let parsed = PhpType::parse(s);
let substituted = parsed.substitute(subs);
let result = substituted.to_string();
if result == s {
Cow::Borrowed(s)
} else {
Cow::Owned(result)
}
}
pub(crate) fn build_generic_subs(
class: &ClassInfo,
type_args: &[PhpType],
) -> HashMap<String, PhpType> {
if class.template_params.is_empty() || type_args.is_empty() {
return HashMap::new();
}
let offset = if type_args.len() < class.template_params.len() {
let skip = class.template_params.len() - type_args.len();
let all_skipped_are_key_like = class.template_params[..skip].iter().all(|param| {
class
.template_param_bounds
.get(param)
.is_some_and(is_key_like_bound)
});
if all_skipped_are_key_like { skip } else { 0 }
} else {
0
};
let mut subs = HashMap::new();
for (i, param_name) in class.template_params.iter().enumerate() {
if i < offset {
continue;
}
if let Some(arg) = type_args.get(i - offset) {
subs.insert(param_name.clone(), arg.clone());
}
}
subs
}
pub(crate) fn apply_generic_args(class: &ClassInfo, type_args: &[PhpType]) -> ClassInfo {
let subs = build_generic_subs(class, type_args);
if subs.is_empty() {
return class.clone();
}
let mut result = class.clone();
for method in result.methods.make_mut() {
apply_substitution_to_method(method, &subs);
}
for property in result.properties.make_mut() {
apply_substitution_to_property(property, &subs);
}
apply_substitution_to_generics(&mut result.implements_generics, &subs);
apply_substitution_to_generics(&mut result.extends_generics, &subs);
apply_substitution_to_generics(&mut result.use_generics, &subs);
result
}
fn is_key_like_bound(bound: &PhpType) -> bool {
match bound {
PhpType::Named(_) => bound.is_array_key() || bound.is_int() || bound.is_string_type(),
PhpType::Union(members) => {
!members.is_empty() && members.iter().all(|m| m.is_int() || m.is_string_type())
}
_ => false,
}
}
fn apply_substitution_to_generics(
generics: &mut [(String, Vec<PhpType>)],
subs: &HashMap<String, PhpType>,
) {
for (_class_name, type_args) in generics.iter_mut() {
for arg in type_args.iter_mut() {
let substituted = arg.substitute(subs);
if substituted != *arg {
*arg = substituted;
}
}
}
}
#[cfg(test)]
#[path = "inheritance_tests.rs"]
mod tests;