use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use crate::docblock;
use crate::inheritance;
use crate::inheritance::ClassRef;
use crate::php_type::PhpType;
use crate::types::{
ClassInfo, ConstantInfo, MAX_INHERITANCE_DEPTH, MAX_MIXIN_DEPTH, MethodInfo, PropertyInfo,
Visibility,
};
use crate::util::short_name;
thread_local! {
static MIXIN_CACHE: RefCell<HashMap<String, Arc<ClassInfo>>> =
RefCell::new(HashMap::new());
}
pub fn clear_mixin_cache() {
MIXIN_CACHE.with(|cache| cache.borrow_mut().clear());
}
struct MixinCollector {
methods: Vec<MethodInfo>,
properties: Vec<PropertyInfo>,
constants: Vec<ConstantInfo>,
dedup: MixinDedup,
}
struct MixinDedup {
methods: HashSet<String>,
properties: HashSet<String>,
constants: HashSet<String>,
}
use super::{VirtualMemberProvider, VirtualMembers};
pub struct PHPDocProvider;
impl VirtualMemberProvider for PHPDocProvider {
fn applies_to(
&self,
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> bool {
if class.class_docblock.as_ref().is_some_and(|d| !d.is_empty()) {
return true;
}
for trait_name in &class.used_traits {
if let Some(trait_info) = class_loader(trait_name)
&& trait_info
.class_docblock
.as_ref()
.is_some_and(|d| !d.is_empty())
{
return true;
}
}
if !class.mixins.is_empty() {
return true;
}
let mut current_parent = class.parent_class.clone();
let mut depth = 0u32;
while let Some(ref parent_name) = current_parent {
depth += 1;
if depth > MAX_INHERITANCE_DEPTH {
break;
}
let parent = if let Some(p) = class_loader(parent_name) {
p
} else {
break;
};
if !parent.mixins.is_empty() {
return true;
}
if parent
.class_docblock
.as_ref()
.is_some_and(|d| !d.is_empty())
{
return true;
}
current_parent = parent.parent_class.clone();
}
false
}
fn provide(
&self,
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
_cache: Option<&super::ResolvedClassCache>,
) -> VirtualMembers {
let mut methods = Vec::new();
let mut properties = Vec::new();
let constants = Vec::new();
let mut seen_methods: HashSet<String> =
class.methods.iter().map(|m| m.name.clone()).collect();
let mut seen_props: HashSet<String> = HashSet::new();
let seen_consts: HashSet<String> = class.constants.iter().map(|c| c.name.clone()).collect();
if let Some(doc_text) = class.class_docblock.as_deref()
&& !doc_text.is_empty()
{
for m in docblock::extract_method_tags(doc_text) {
seen_methods.insert(m.name.clone());
methods.push(m);
}
for (name, type_hint) in docblock::extract_property_tags(doc_text) {
seen_props.insert(name.clone());
properties.push(PropertyInfo {
name,
name_offset: 0,
type_hint,
native_type_hint: None,
description: None,
is_static: false,
visibility: Visibility::Public,
deprecation_message: None,
deprecated_replacement: None,
see_refs: Vec::new(),
is_virtual: true,
});
}
}
for trait_name in &class.used_traits {
let trait_info = if let Some(t) = class_loader(trait_name) {
t
} else {
continue;
};
if let Some(doc_text) = trait_info.class_docblock.as_deref()
&& !doc_text.is_empty()
{
for m in docblock::extract_method_tags(doc_text) {
if seen_methods.insert(m.name.clone()) {
methods.push(m);
}
}
for (name, type_hint) in docblock::extract_property_tags(doc_text) {
if seen_props.insert(name.clone()) {
properties.push(PropertyInfo {
name,
name_offset: 0,
type_hint,
native_type_hint: None,
description: None,
is_static: false,
visibility: Visibility::Public,
deprecation_message: None,
deprecated_replacement: None,
see_refs: Vec::new(),
is_virtual: true,
});
}
}
}
}
{
let mut current_parent = class.parent_class.clone();
let mut depth = 0u32;
while let Some(ref parent_name) = current_parent {
depth += 1;
if depth > MAX_INHERITANCE_DEPTH {
break;
}
let parent = if let Some(p) = class_loader(parent_name) {
p
} else {
break;
};
if let Some(doc_text) = parent.class_docblock.as_deref()
&& !doc_text.is_empty()
{
for m in docblock::extract_method_tags(doc_text) {
if seen_methods.insert(m.name.clone()) {
methods.push(m);
}
}
for (name, type_hint) in docblock::extract_property_tags(doc_text) {
if seen_props.insert(name.clone()) {
properties.push(PropertyInfo {
name,
name_offset: 0,
type_hint,
native_type_hint: None,
description: None,
is_static: false,
visibility: Visibility::Public,
deprecation_message: None,
deprecated_replacement: None,
see_refs: Vec::new(),
is_virtual: true,
});
}
}
}
current_parent = parent.parent_class.clone();
}
}
let mixin_dedup = MixinDedup {
methods: seen_methods,
properties: seen_props,
constants: seen_consts,
};
let mut collector = MixinCollector {
methods,
properties,
constants,
dedup: mixin_dedup,
};
collect_mixin_members(
&class.mixins,
&class.mixin_generics,
class_loader,
&mut collector,
&HashMap::new(),
0,
);
let mut current_ancestor: ClassRef<'_> = ClassRef::Borrowed(class);
let mut active_subs: HashMap<String, PhpType> = HashMap::new();
let mut depth = 0u32;
while let Some(ref parent_name) = current_ancestor.parent_class.clone() {
depth += 1;
if depth > MAX_INHERITANCE_DEPTH {
break;
}
let parent = if let Some(p) = class_loader(parent_name) {
p
} else {
break;
};
let level_subs = build_mixin_substitution_map(¤t_ancestor, &parent, &active_subs);
if !parent.mixins.is_empty() {
let resolved_mixin_generics: Vec<(String, Vec<PhpType>)> = if level_subs.is_empty()
{
parent.mixin_generics.clone()
} else {
parent
.mixin_generics
.iter()
.map(|(name, args)| {
let resolved_args: Vec<PhpType> =
args.iter().map(|arg| arg.substitute(&level_subs)).collect();
(name.clone(), resolved_args)
})
.collect()
};
collect_mixin_members(
&parent.mixins,
&resolved_mixin_generics,
class_loader,
&mut collector,
&level_subs,
0,
);
}
active_subs = level_subs;
current_ancestor = ClassRef::Owned(parent);
}
VirtualMembers {
methods: collector.methods,
properties: collector.properties,
constants: collector.constants,
}
}
}
fn collect_mixin_members(
mixin_names: &[String],
mixin_generics: &[(String, Vec<PhpType>)],
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
collector: &mut MixinCollector,
template_subs: &HashMap<String, PhpType>,
depth: u32,
) {
if depth > MAX_MIXIN_DEPTH {
return;
}
for mixin_name in mixin_names {
let resolved_mixin_name = if let Some(concrete) = template_subs.get(mixin_name.as_str()) {
if let Some(base) = concrete.base_name() {
base.to_string()
} else {
continue;
}
} else {
mixin_name.clone()
};
let mixin_class = if let Some(c) = class_loader(&resolved_mixin_name) {
c
} else {
continue;
};
let mixin_short = short_name(&resolved_mixin_name);
let generic_args: Option<&[PhpType]> = mixin_generics
.iter()
.find(|(name, _)| {
name == mixin_name
|| short_name(name) == mixin_short
|| name == &resolved_mixin_name
})
.map(|(_, args)| args.as_slice());
let resolved_mixin = MIXIN_CACHE.with(|cache| {
let mut map = cache.borrow_mut();
Arc::clone(map.entry(resolved_mixin_name.clone()).or_insert_with(|| {
Arc::new(crate::inheritance::resolve_class_with_inheritance(
&mixin_class,
class_loader,
))
}))
});
let subs: HashMap<String, PhpType> = if let Some(args) = generic_args {
let mut map = HashMap::new();
for (i, param_name) in mixin_class.template_params.iter().enumerate() {
if let Some(arg) = args.get(i) {
map.insert(param_name.clone(), arg.clone());
}
}
map
} else {
HashMap::new()
};
for method in &resolved_mixin.methods {
if method.visibility != Visibility::Public {
continue;
}
if !collector.dedup.methods.insert(method.name.clone()) {
continue;
}
let mut method = method.clone();
if !subs.is_empty() {
inheritance::apply_substitution_to_method(&mut method, &subs);
}
method.is_virtual = true;
collector.methods.push(method);
}
for property in &resolved_mixin.properties {
if property.visibility != Visibility::Public {
continue;
}
if !collector.dedup.properties.insert(property.name.clone()) {
continue;
}
let mut property = property.clone();
if !subs.is_empty() {
inheritance::apply_substitution_to_property(&mut property, &subs);
}
property.is_virtual = true;
collector.properties.push(property);
}
for constant in &resolved_mixin.constants {
if constant.visibility != Visibility::Public {
continue;
}
if !collector.dedup.constants.insert(constant.name.clone()) {
continue;
}
collector.constants.push(constant.clone());
}
if !mixin_class.mixins.is_empty() {
collect_mixin_members(
&mixin_class.mixins,
&mixin_class.mixin_generics,
class_loader,
collector,
&HashMap::new(),
depth + 1,
);
}
}
}
pub fn resolve_template_param_mixins(
original_class: &ClassInfo,
template_subs: &HashMap<String, PhpType>,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> super::VirtualMembers {
if template_subs.is_empty() || original_class.mixins.is_empty() {
return super::VirtualMembers {
methods: Vec::new(),
properties: Vec::new(),
constants: Vec::new(),
};
}
let template_mixins: Vec<String> = original_class
.mixins
.iter()
.filter(|m| original_class.template_params.contains(m))
.cloned()
.collect();
if template_mixins.is_empty() {
return super::VirtualMembers {
methods: Vec::new(),
properties: Vec::new(),
constants: Vec::new(),
};
}
let dedup = MixinDedup {
methods: HashSet::new(),
properties: HashSet::new(),
constants: HashSet::new(),
};
let mut collector = MixinCollector {
methods: Vec::new(),
properties: Vec::new(),
constants: Vec::new(),
dedup,
};
collect_mixin_members(
&template_mixins,
&original_class.mixin_generics,
class_loader,
&mut collector,
template_subs,
0,
);
super::VirtualMembers {
methods: collector.methods,
properties: collector.properties,
constants: collector.constants,
}
}
fn build_mixin_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
}
#[cfg(test)]
#[path = "phpdoc_tests.rs"]
mod tests;