pub mod laravel;
pub mod phpdoc;
use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use parking_lot::Mutex;
use crate::inheritance::{
ClassRef, apply_substitution_to_method, apply_substitution_to_property,
enrich_method_from_ancestor, enrich_property_from_ancestor, resolve_class_with_inheritance,
};
use crate::php_type::PhpType;
use crate::types::{ClassInfo, ConstantInfo, MethodInfo, PropertyInfo};
use crate::util::short_name;
use crate::virtual_members::laravel::patches::apply_laravel_patches;
pub type ResolvedClassCacheKey = (String, Vec<String>);
pub type ResolvedClassCache = Arc<Mutex<HashMap<ResolvedClassCacheKey, Arc<ClassInfo>>>>;
pub fn new_resolved_class_cache() -> ResolvedClassCache {
Arc::new(Mutex::new(HashMap::new()))
}
thread_local! {
static ACTIVE_RESOLVED_CACHE: Cell<*const ResolvedClassCache> = const { Cell::new(std::ptr::null()) };
}
pub struct ResolvedCacheGuard {
prev: *const ResolvedClassCache,
}
impl Drop for ResolvedCacheGuard {
fn drop(&mut self) {
ACTIVE_RESOLVED_CACHE.with(|c| c.set(self.prev));
}
}
pub fn with_active_resolved_class_cache(cache: &ResolvedClassCache) -> ResolvedCacheGuard {
let prev = ACTIVE_RESOLVED_CACHE.with(|c| c.get());
ACTIVE_RESOLVED_CACHE.with(|c| c.set(cache as *const _));
ResolvedCacheGuard { prev }
}
pub fn active_resolved_class_cache() -> Option<&'static ResolvedClassCache> {
let ptr = ACTIVE_RESOLVED_CACHE.with(|c| c.get());
if ptr.is_null() {
None
} else {
Some(unsafe { &*ptr })
}
}
pub fn evict_fqn(cache: &mut HashMap<ResolvedClassCacheKey, Arc<ClassInfo>>, fqn: &str) {
if cache.is_empty() {
return;
}
let before = cache.len();
cache.retain(|(k, _), _| k != fqn);
let removed_direct = cache.len() < before;
if !removed_direct {
let seed = [fqn.to_string()];
let has_dependent = cache.values().any(|cls| depends_on_any(cls, &seed));
if !has_dependent {
return;
}
}
let mut evicted: Vec<String> = vec![fqn.to_string()];
loop {
let mut newly_evicted: Vec<String> = Vec::new();
for ((cached_fqn, _), cls) in cache.iter() {
if depends_on_any(cls, &evicted) && !evicted.contains(cached_fqn) {
newly_evicted.push(cached_fqn.clone());
}
}
if newly_evicted.is_empty() {
break;
}
for dep_fqn in &newly_evicted {
cache.retain(|(k, _), _| k != dep_fqn);
evicted.push(dep_fqn.clone());
}
}
}
fn depends_on_any(cls: &ClassInfo, fqns: &[String]) -> bool {
for fqn in fqns {
let short = crate::util::short_name(fqn);
if let Some(ref parent) = cls.parent_class
&& (parent == fqn || parent == short)
{
return true;
}
if cls.used_traits.iter().any(|t| t == fqn || t == short) {
return true;
}
if cls.interfaces.iter().any(|i| i == fqn || i == short) {
return true;
}
if cls.mixins.iter().any(|m| m == fqn || m == short) {
return true;
}
if let Some(laravel) = cls.laravel()
&& laravel.casts_definitions.iter().any(|(_, cast_type)| {
let class_part = cast_type.split(':').next().unwrap_or(cast_type);
class_part == fqn || class_part == short
})
{
return true;
}
}
false
}
pub struct VirtualMembers {
pub methods: Vec<MethodInfo>,
pub properties: Vec<PropertyInfo>,
pub constants: Vec<ConstantInfo>,
}
impl VirtualMembers {
pub fn is_empty(&self) -> bool {
self.methods.is_empty() && self.properties.is_empty() && self.constants.is_empty()
}
}
pub trait VirtualMemberProvider {
fn applies_to(
&self,
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> bool;
fn provide(
&self,
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
cache: Option<&ResolvedClassCache>,
) -> VirtualMembers;
}
pub fn merge_virtual_members(class: &mut ClassInfo, virtual_members: VirtualMembers) {
let mut method_index: HashMap<(String, bool), usize> = class
.methods
.iter()
.enumerate()
.map(|(i, m)| ((m.name.clone(), m.is_static), i))
.collect();
for method in virtual_members.methods {
let key = (method.name.clone(), method.is_static);
if let Some(&idx) = method_index.get(&key) {
if class.methods[idx].has_scope_attribute {
class.methods.make_mut()[idx] = method;
}
} else {
let new_idx = class.methods.len();
class.methods.push(method);
method_index.insert(key, new_idx);
}
}
let mut prop_index: HashMap<String, usize> = class
.properties
.iter()
.enumerate()
.map(|(i, p)| (p.name.clone(), i))
.collect();
for property in virtual_members.properties {
if let Some(&idx) = prop_index.get(&property.name) {
if property_type_specificity(&property)
> property_type_specificity(&class.properties[idx])
{
class.properties.make_mut()[idx] = property;
}
} else {
let new_idx = class.properties.len();
prop_index.insert(property.name.clone(), new_idx);
class.properties.push(property);
}
}
let mut const_names: HashSet<String> = class.constants.iter().map(|c| c.name.clone()).collect();
for constant in virtual_members.constants {
if const_names.insert(constant.name.clone()) {
class.constants.push(constant);
}
}
}
fn type_specificity(hint: &Option<PhpType>) -> u8 {
match hint {
None => 0,
Some(t) if t.is_mixed() => 0,
Some(PhpType::Raw(s)) if s.trim().is_empty() => 0,
Some(t) if t.has_type_structure() => 2,
Some(_) => 1,
}
}
fn property_type_specificity(property: &PropertyInfo) -> u8 {
let effective_score = type_specificity(&property.type_hint);
if effective_score > 0 {
return effective_score;
}
type_specificity(&property.native_type_hint)
}
pub fn apply_virtual_members(
class: &mut ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
providers: &[Box<dyn VirtualMemberProvider>],
cache: Option<&ResolvedClassCache>,
) {
for provider in providers {
if provider.applies_to(class, class_loader) {
let virtual_members = provider.provide(class, class_loader, cache);
if !virtual_members.is_empty() {
merge_virtual_members(class, virtual_members);
}
}
}
}
pub fn default_providers() -> Vec<Box<dyn VirtualMemberProvider>> {
vec![
Box::new(laravel::LaravelModelProvider),
Box::new(laravel::LaravelFactoryProvider),
Box::new(phpdoc::PHPDocProvider),
]
}
pub fn resolve_class_fully(
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> Arc<ClassInfo> {
resolve_class_fully_inner(class, class_loader, None, &[])
}
pub fn resolve_class_fully_cached(
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
cache: &ResolvedClassCache,
) -> Arc<ClassInfo> {
resolve_class_fully_inner(class, class_loader, Some(cache), &[])
}
pub fn resolve_class_fully_maybe_cached(
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
cache: Option<&ResolvedClassCache>,
) -> Arc<ClassInfo> {
resolve_class_fully_inner(class, class_loader, cache, &[])
}
fn resolve_class_fully_inner(
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
cache: Option<&ResolvedClassCache>,
generic_args: &[String],
) -> Arc<ClassInfo> {
let fqn = class.fqn();
let cache_key: ResolvedClassCacheKey = (fqn.clone(), generic_args.to_vec());
if let Some(cache) = cache {
let map = cache.lock();
if let Some(cached) = map.get(&cache_key) {
return Arc::clone(cached);
}
}
let mut merged = resolve_class_with_inheritance(class, class_loader);
if fqn == "Illuminate\\Redis\\Connections\\Connection" {
let mixin = "Redis".to_string();
if !merged.mixins.contains(&mixin) {
merged.mixins.push(mixin);
}
}
let providers = default_providers();
if !providers.is_empty() {
apply_virtual_members(&mut merged, class_loader, &providers, cache);
}
let mut all_iface_names: Vec<String> = class.interfaces.clone();
let mut all_implements_generics: Vec<(String, Vec<PhpType>)> =
class.implements_generics.clone();
{
let mut current: ClassRef<'_> = ClassRef::Borrowed(class);
let mut depth = 0u32;
let mut active_subs: HashMap<String, PhpType> = HashMap::new();
while let Some(ref parent_name) = current.parent_class {
depth += 1;
if depth > 20 {
break;
}
if let Some(parent) = class_loader(parent_name) {
let parent_short = short_name(&parent.name);
let type_args = current
.extends_generics
.iter()
.chain(current.implements_generics.iter())
.find(|(name, _)| short_name(name) == parent_short)
.map(|(_, args)| args);
let mut level_subs = if let Some(args) = type_args {
let mut map = HashMap::new();
for (i, param_name) in parent.template_params.iter().enumerate() {
if let Some(arg) = args.get(i) {
let resolved = if active_subs.is_empty() {
arg.clone()
} else {
arg.substitute(&active_subs)
};
map.insert(param_name.clone(), resolved);
}
}
map
} else {
active_subs.clone()
};
if level_subs.is_empty() && !active_subs.is_empty() {
level_subs = active_subs.clone();
}
for iface in &parent.interfaces {
if !all_iface_names.contains(iface) {
all_iface_names.push(iface.clone());
}
}
for (iface_name, args) in &parent.implements_generics {
let resolved_args: Vec<PhpType> = if level_subs.is_empty() {
args.clone()
} else {
args.iter().map(|a| a.substitute(&level_subs)).collect()
};
all_implements_generics.push((iface_name.clone(), resolved_args));
}
active_subs = level_subs;
current = ClassRef::Owned(parent);
} else {
break;
}
}
}
let mut iface_idx = 0;
while iface_idx < all_iface_names.len() {
let iface_name = all_iface_names[iface_idx].clone();
iface_idx += 1;
if let Some(iface) = class_loader(&iface_name) {
for child_iface in &iface.interfaces {
if !all_iface_names.contains(child_iface) {
all_iface_names.push(child_iface.clone());
}
}
let iface_subs =
build_implements_substitution_map(&iface_name, &iface, &all_implements_generics);
for (name, args) in &iface.extends_generics {
if !all_implements_generics.iter().any(|(n, _)| n == name) {
let resolved_args: Vec<PhpType> = if iface_subs.is_empty() {
args.clone()
} else {
args.iter().map(|a| a.substitute(&iface_subs)).collect()
};
all_implements_generics.push((name.clone(), resolved_args));
}
}
for (name, args) in &iface.implements_generics {
if !all_implements_generics.iter().any(|(n, _)| n == name) {
let resolved_args: Vec<PhpType> = if iface_subs.is_empty() {
args.clone()
} else {
args.iter().map(|a| a.substitute(&iface_subs)).collect()
};
all_implements_generics.push((name.clone(), resolved_args));
}
}
if iface_subs.is_empty() {
let iface_key: ResolvedClassCacheKey = (iface.fqn(), Vec::new());
if let Some(c) = cache {
let map = c.lock();
if let Some(cached) = map.get(&iface_key) {
let resolved_iface = ClassInfo::clone(cached);
drop(map);
merge_interface_members_into(&mut merged, resolved_iface, &iface_subs);
continue;
}
}
}
let mut resolved_iface = resolve_class_with_inheritance(&iface, class_loader);
if !providers.is_empty() {
apply_virtual_members(&mut resolved_iface, class_loader, &providers, cache);
}
merge_interface_members_into(&mut merged, resolved_iface, &iface_subs);
}
}
for (name, args) in &all_implements_generics {
if !merged
.implements_generics
.iter()
.any(|(n, _)| short_name(n) == short_name(name))
{
merged
.implements_generics
.push((name.clone(), args.clone()));
}
}
apply_laravel_patches(&mut merged, &fqn);
let result = Arc::new(merged);
if let Some(cache) = cache {
cache.lock().insert(cache_key, Arc::clone(&result));
}
result
}
fn merge_interface_members_into(
merged: &mut ClassInfo,
mut resolved_iface: ClassInfo,
iface_subs: &HashMap<String, PhpType>,
) {
if !iface_subs.is_empty() {
for method in resolved_iface.methods.make_mut().iter_mut() {
apply_substitution_to_method(method, iface_subs);
}
for property in resolved_iface.properties.make_mut().iter_mut() {
apply_substitution_to_property(property, iface_subs);
}
}
const HASHMAP_THRESHOLD: usize = 32;
let method_index: Option<HashMap<String, usize>> = if merged.methods.len() >= HASHMAP_THRESHOLD
{
Some(
merged
.methods
.iter()
.enumerate()
.map(|(i, m)| (m.name.clone(), i))
.collect(),
)
} else {
None
};
for iface_method in resolved_iface.methods.into_vec() {
let existing_idx = if let Some(ref index) = method_index {
index.get(&iface_method.name).copied()
} else {
merged
.methods
.iter()
.position(|m| m.name == iface_method.name)
};
if let Some(idx) = existing_idx {
let existing = &mut merged.methods.make_mut()[idx];
enrich_method_from_ancestor(existing, &iface_method);
} else {
merged.methods.push(iface_method);
}
}
for property in resolved_iface.properties.into_vec() {
if let Some(existing) = merged
.properties
.make_mut()
.iter_mut()
.find(|p| p.name == property.name)
{
enrich_property_from_ancestor(existing, &property);
} else {
merged.properties.push(property);
}
}
let existing_consts: HashSet<String> =
merged.constants.iter().map(|c| c.name.clone()).collect();
for constant in resolved_iface.constants.into_vec() {
if !existing_consts.contains(&constant.name) {
merged.constants.push(constant);
}
}
}
fn build_implements_substitution_map(
iface_name: &str,
iface: &ClassInfo,
all_implements_generics: &[(String, Vec<PhpType>)],
) -> HashMap<String, PhpType> {
if iface.template_params.is_empty() || all_implements_generics.is_empty() {
return HashMap::new();
}
let iface_short = short_name(iface_name);
let type_args = all_implements_generics
.iter()
.find(|(name, _)| short_name(name) == iface_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 iface.template_params.iter().enumerate() {
if let Some(arg) = type_args.get(i) {
map.insert(param_name.clone(), arg.clone());
}
}
map
}
#[cfg(test)]
mod tests;