use std::collections::HashMap;
use std::sync::Arc;
use mago_span::HasSpan;
use mago_syntax::ast::*;
use crate::Backend;
use crate::docblock;
use crate::parser::extract_hint_type;
use crate::php_type::PhpType;
use crate::types::{ClassInfo, ResolvedType};
use super::resolution::build_var_resolver_from_ctx;
use crate::completion::call_resolution::MethodReturnCtx;
use crate::completion::conditional_resolution::resolve_conditional_with_args;
use crate::completion::resolver::{Loaders, VarResolutionCtx};
use crate::util::strip_fqn_prefix;
fn resolved_type_with_lookup(
ty: PhpType,
_current_class_name: &str,
all_classes: &[Arc<ClassInfo>],
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> ResolvedType {
if let Some(base) = ty.base_name() {
let base = base.strip_prefix('\\').unwrap_or(base);
if !crate::php_type::is_keyword_type(base) {
let cls = crate::util::find_class_by_name(all_classes, base)
.map(|arc| arc.as_ref().clone())
.or_else(|| class_loader(base).map(Arc::unwrap_or_clone));
if let Some(class) = cls {
return ResolvedType::from_both(ty, class);
}
}
}
ResolvedType::from_type_string(ty)
}
pub(in crate::completion) fn resolve_rhs_expression<'b>(
expr: &'b Expression<'b>,
ctx: &VarResolutionCtx<'_>,
) -> Vec<ResolvedType> {
match expr {
Expression::Literal(Literal::Integer(_)) => {
vec![ResolvedType::from_type_string(PhpType::int())]
}
Expression::Literal(Literal::Float(_)) => {
vec![ResolvedType::from_type_string(PhpType::float())]
}
Expression::Literal(Literal::String(_)) => {
vec![ResolvedType::from_type_string(PhpType::string())]
}
Expression::Literal(Literal::True(_) | Literal::False(_)) => {
vec![ResolvedType::from_type_string(PhpType::bool())]
}
Expression::Literal(Literal::Null(_)) => {
vec![ResolvedType::from_type_string(PhpType::null())]
}
Expression::Array(arr) => {
let pt =
super::raw_type_inference::infer_array_literal_raw_type(arr.elements.iter(), ctx)
.unwrap_or_else(PhpType::array);
vec![ResolvedType::from_type_string(pt)]
}
Expression::LegacyArray(arr) => {
let pt =
super::raw_type_inference::infer_array_literal_raw_type(arr.elements.iter(), ctx)
.unwrap_or_else(PhpType::array);
vec![ResolvedType::from_type_string(pt)]
}
Expression::Instantiation(inst) => resolve_rhs_instantiation(inst, ctx),
Expression::AnonymousClass(anon) => {
let start = anon.left_brace.start.offset;
let name = format!("__anonymous@{}", start);
if let Some(cls) = ctx.all_classes.iter().find(|c| c.name == name) {
return ResolvedType::from_classes(vec![(**cls).clone()]);
}
vec![]
}
Expression::ArrayAccess(array_access) => resolve_rhs_array_access(array_access, expr, ctx),
Expression::Call(call) => resolve_rhs_call(call, expr, ctx),
Expression::Access(access) => resolve_rhs_property_access(access, ctx),
Expression::Parenthesized(p) => resolve_rhs_expression(p.expression, ctx),
Expression::Match(match_expr) => {
let mut combined = Vec::new();
for arm in match_expr.arms.iter() {
let arm_results = resolve_rhs_expression(arm.expression(), ctx);
ResolvedType::extend_unique(&mut combined, arm_results);
}
combined
}
Expression::Conditional(cond_expr) => {
let mut combined = Vec::new();
let then_expr = cond_expr.then.unwrap_or(cond_expr.condition);
ResolvedType::extend_unique(&mut combined, resolve_rhs_expression(then_expr, ctx));
ResolvedType::extend_unique(
&mut combined,
resolve_rhs_expression(cond_expr.r#else, ctx),
);
combined
}
Expression::Binary(binary) if binary.operator.is_null_coalesce() => {
let lhs_non_nullable = matches!(
binary.lhs,
Expression::Instantiation(_)
| Expression::Literal(_)
| Expression::Array(_)
| Expression::LegacyArray(_)
| Expression::Clone(_)
);
let lhs_results = resolve_rhs_expression(binary.lhs, ctx);
if !lhs_results.is_empty() && lhs_non_nullable {
lhs_results
} else if !lhs_results.is_empty() {
let mut combined: Vec<ResolvedType> = lhs_results
.into_iter()
.filter_map(|mut rt| {
let parsed = rt.type_string.clone();
match parsed.non_null_type() {
Some(non_null) => {
rt.type_string = non_null;
Some(rt)
}
None if rt.type_string == PhpType::null() => None,
None => Some(rt),
}
})
.collect();
ResolvedType::extend_unique(&mut combined, resolve_rhs_expression(binary.rhs, ctx));
combined
} else {
let mut combined = lhs_results;
ResolvedType::extend_unique(&mut combined, resolve_rhs_expression(binary.rhs, ctx));
combined
}
}
Expression::Clone(clone_expr) => resolve_rhs_clone(clone_expr, ctx),
Expression::Pipe(pipe) => resolve_rhs_pipe(pipe, ctx),
Expression::PartialApplication(_)
| Expression::Closure(_)
| Expression::ArrowFunction(_) => {
let closure_ty = PhpType::closure();
ResolvedType::from_classes_with_hint(
crate::completion::type_resolution::type_hint_to_classes_typed(
&closure_ty,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
),
closure_ty,
)
}
Expression::Yield(_) => {
if let Some(ref ret_type) = ctx.enclosing_return_type
&& let Some(send_php_type) = ret_type.generator_send_type(true)
{
return ResolvedType::from_classes_with_hint(
crate::completion::type_resolution::type_hint_to_classes_typed(
send_php_type,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
),
send_php_type.clone(),
);
}
vec![]
}
Expression::Variable(Variable::Direct(dv)) => {
let rhs_var = dv.name.to_string();
if rhs_var == ctx.var_name {
return vec![];
}
super::resolution::resolve_variable_types(
&rhs_var,
ctx.current_class,
ctx.all_classes,
ctx.content,
ctx.cursor_offset,
ctx.class_loader,
Loaders::with_function(ctx.function_loader()),
)
}
Expression::Binary(binary) if binary.operator.is_concatenation() => {
vec![ResolvedType::from_type_string(PhpType::string())]
}
Expression::ConstantAccess(ca) => {
let name = ca.name.value().to_string();
let name_clean = strip_fqn_prefix(&name);
match name_clean.to_lowercase().as_str() {
"true" | "false" => {
return vec![ResolvedType::from_type_string(PhpType::bool())];
}
"null" => {
return vec![ResolvedType::from_type_string(PhpType::null())];
}
_ => {}
}
if let Some(loader) = ctx.constant_loader()
&& let Some(maybe_value) = loader(name_clean)
&& let Some(ref value) = maybe_value
&& let Some(ts) = infer_type_from_constant_value(value)
{
return vec![ResolvedType::from_type_string(ts)];
}
vec![]
}
_ => vec![],
}
}
fn infer_type_from_constant_value(value: &str) -> Option<PhpType> {
let v = value.trim();
if v.is_empty() {
return None;
}
if (v.starts_with('\'') && v.ends_with('\'')) || (v.starts_with('"') && v.ends_with('"')) {
return Some(PhpType::string());
}
if v.starts_with('[') || v.starts_with("array(") || v.starts_with("array (") {
return Some(PhpType::array());
}
let lower = v.to_lowercase();
if lower == "true" || lower == "false" {
return Some(PhpType::bool());
}
if lower == "null" {
return Some(PhpType::null());
}
let numeric = v
.strip_prefix('-')
.or_else(|| v.strip_prefix('+'))
.unwrap_or(v);
if numeric.starts_with("0x") || numeric.starts_with("0X") {
if numeric[2..]
.chars()
.all(|c| c.is_ascii_hexdigit() || c == '_')
{
return Some(PhpType::int());
}
}
if numeric.starts_with("0b") || numeric.starts_with("0B") {
if numeric[2..]
.chars()
.all(|c| c == '0' || c == '1' || c == '_')
{
return Some(PhpType::int());
}
}
if numeric.starts_with("0o") || numeric.starts_with("0O") {
if numeric[2..]
.chars()
.all(|c| ('0'..='7').contains(&c) || c == '_')
{
return Some(PhpType::int());
}
}
if !numeric.is_empty()
&& numeric.chars().all(|c| c.is_ascii_digit() || c == '_')
&& numeric.chars().next().is_some_and(|c| c.is_ascii_digit())
{
return Some(PhpType::int());
}
if !numeric.is_empty() {
let has_dot = numeric.contains('.');
let has_exp = numeric.contains('e') || numeric.contains('E');
if (has_dot || has_exp)
&& numeric.chars().all(|c| {
c.is_ascii_digit()
|| c == '.'
|| c == 'e'
|| c == 'E'
|| c == '+'
|| c == '-'
|| c == '_'
})
{
return Some(PhpType::float());
}
}
None
}
fn resolve_rhs_pipe(pipe: &Pipe<'_>, ctx: &VarResolutionCtx<'_>) -> Vec<ResolvedType> {
match pipe.callable {
Expression::PartialApplication(PartialApplication::Function(fpa)) => {
let func_name = match fpa.function {
Expression::Identifier(ident) => ident.value().to_string(),
_ => return vec![],
};
if let Some(fl) = ctx.function_loader()
&& let Some(func_info) = fl(&func_name)
&& let Some(ref ret) = func_info.return_type
{
return ResolvedType::from_classes_with_hint(
crate::completion::type_resolution::type_hint_to_classes_typed(
ret,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
),
ret.clone(),
);
}
vec![]
}
_ => vec![],
}
}
fn resolve_rhs_instantiation(
inst: &Instantiation<'_>,
ctx: &VarResolutionCtx<'_>,
) -> Vec<ResolvedType> {
let class_name = match inst.class {
Expression::Self_(_) => Some("self"),
Expression::Static(_) => Some("static"),
Expression::Identifier(ident) => Some(ident.value()),
_ => None,
};
if let Some(name) = class_name {
let parsed_name = PhpType::Named(name.to_string());
let classes = crate::completion::type_resolution::type_hint_to_classes_typed(
&parsed_name,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
);
if classes.len() == 1 && !classes[0].template_params.is_empty() {
let cls = &classes[0];
if let Some(ctor) = cls.methods.iter().find(|m| m.name == "__construct")
&& !ctor.template_bindings.is_empty()
&& let Some(ref arg_list) = inst.argument_list
{
let text_args =
super::raw_type_inference::extract_argument_text(arg_list, ctx.content);
if !text_args.is_empty() {
let rctx = ctx.as_resolution_ctx();
let subs = build_constructor_template_subs(cls, ctor, &text_args, &rctx, ctx);
if !subs.is_empty() {
let type_args: Vec<PhpType> = cls
.template_params
.iter()
.map(|p| {
subs.get(p)
.cloned()
.unwrap_or_else(|| PhpType::Named(p.to_string()))
})
.collect();
let resolved =
crate::virtual_members::resolve_class_fully(cls, ctx.class_loader);
let mut substituted =
crate::inheritance::apply_generic_args(&resolved, &type_args);
if cls.mixins.iter().any(|m| cls.template_params.contains(m)) {
let generic_subs =
crate::inheritance::build_generic_subs(cls, &type_args);
if !generic_subs.is_empty() {
let mixin_members =
crate::virtual_members::phpdoc::resolve_template_param_mixins(
cls,
&generic_subs,
ctx.class_loader,
);
if !mixin_members.is_empty() {
crate::virtual_members::merge_virtual_members(
&mut substituted,
mixin_members,
);
}
}
}
let generic_type =
PhpType::Generic(substituted.name.clone(), type_args.clone());
return vec![ResolvedType::from_both(generic_type, substituted)];
}
}
}
}
return ResolvedType::from_classes_with_hint(classes, parsed_name);
}
if let Expression::Variable(Variable::Direct(dv)) = inst.class {
let var_name = dv.name.to_string();
let resolved =
crate::completion::variable::class_string_resolution::resolve_class_string_targets(
&var_name,
ctx.current_class,
ctx.all_classes,
ctx.content,
ctx.cursor_offset,
ctx.class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes(resolved);
}
}
vec![]
}
fn build_constructor_template_subs(
_class: &ClassInfo,
ctor: &crate::types::MethodInfo,
text_args: &str,
rctx: &crate::completion::resolver::ResolutionCtx<'_>,
ctx: &VarResolutionCtx<'_>,
) -> HashMap<String, PhpType> {
let args = crate::completion::conditional_resolution::split_text_args(text_args);
let mut subs = HashMap::new();
for (tpl_name, param_name) in &ctor.template_bindings {
let param_idx = match ctor.parameters.iter().position(|p| p.name == *param_name) {
Some(idx) => idx,
None => continue,
};
let arg_text = match args.get(param_idx) {
Some(text) => text.trim(),
None => continue,
};
let param_hint = ctor
.parameters
.get(param_idx)
.and_then(|p| p.type_hint.as_ref());
let binding_mode = classify_template_binding(tpl_name, param_hint);
match binding_mode {
TemplateBindingMode::Direct => {
if let Some(resolved_type) = Backend::resolve_arg_text_to_type(arg_text, rctx) {
subs.insert(tpl_name.clone(), resolved_type);
}
}
TemplateBindingMode::CallableReturnType => {
if let Some(ret_type) =
crate::completion::source::helpers::extract_closure_return_type_from_text(
arg_text,
)
{
subs.insert(tpl_name.clone(), ret_type);
}
}
TemplateBindingMode::CallableParamType(position) => {
if let Some(param_type) =
crate::completion::source::helpers::extract_closure_param_type_from_text(
arg_text, position,
)
{
subs.insert(tpl_name.clone(), param_type);
}
}
TemplateBindingMode::ArrayElement => {
if arg_text.starts_with('[') && arg_text.ends_with(']') {
let inner = arg_text[1..arg_text.len() - 1].trim();
if !inner.is_empty() {
let first_elem =
crate::completion::conditional_resolution::split_text_args(inner);
if let Some(elem) = first_elem.first()
&& let Some(resolved_type) =
Backend::resolve_arg_text_to_type(elem.trim(), rctx)
{
subs.insert(tpl_name.clone(), resolved_type);
}
}
} else if let Some(resolved_type) =
Backend::resolve_arg_text_to_type(arg_text, rctx)
{
subs.insert(tpl_name.clone(), resolved_type);
}
}
TemplateBindingMode::GenericWrapper(wrapper_name, tpl_position) => {
if let Some(concrete) = resolve_generic_wrapper_template(
&wrapper_name,
tpl_position,
arg_text,
rctx,
ctx,
) {
subs.insert(tpl_name.clone(), concrete);
}
}
}
}
subs
}
#[derive(Debug)]
pub(crate) enum TemplateBindingMode {
Direct,
ArrayElement,
GenericWrapper(String, usize),
CallableReturnType,
CallableParamType(usize),
}
pub(crate) fn classify_template_binding(
tpl_name: &str,
param_hint: Option<&PhpType>,
) -> TemplateBindingMode {
let hint = match param_hint {
Some(h) => h,
None => return TemplateBindingMode::Direct,
};
classify_from_php_type(tpl_name, hint)
}
fn classify_from_php_type(tpl_name: &str, ty: &PhpType) -> TemplateBindingMode {
match ty {
PhpType::Nullable(inner) => classify_from_php_type(tpl_name, inner),
PhpType::Union(members) => {
for member in members {
if member.is_null() {
continue;
}
let result = classify_from_php_type(tpl_name, member);
if !matches!(result, TemplateBindingMode::Direct) {
return result;
}
if member.is_named(tpl_name) {
return TemplateBindingMode::Direct;
}
}
TemplateBindingMode::Direct
}
PhpType::Array(inner) => {
if inner.as_ref().is_named(tpl_name) {
return TemplateBindingMode::ArrayElement;
}
TemplateBindingMode::Direct
}
PhpType::Named(n) if n == tpl_name => TemplateBindingMode::Direct,
PhpType::Generic(wrapper_name, args) => {
for (i, arg) in args.iter().enumerate() {
if arg.is_named(tpl_name) {
return TemplateBindingMode::GenericWrapper(wrapper_name.clone(), i);
}
}
TemplateBindingMode::Direct
}
PhpType::Callable {
params,
return_type,
..
} => {
if let Some(rt) = return_type
&& type_contains_name(rt, tpl_name)
{
return TemplateBindingMode::CallableReturnType;
}
for (i, p) in params.iter().enumerate() {
if type_contains_name(&p.type_hint, tpl_name) {
return TemplateBindingMode::CallableParamType(i);
}
}
TemplateBindingMode::Direct
}
_ => TemplateBindingMode::Direct,
}
}
fn type_contains_name(ty: &PhpType, name: &str) -> bool {
match ty {
PhpType::Named(n) => n == name,
PhpType::Nullable(inner) | PhpType::Array(inner) => type_contains_name(inner, name),
PhpType::Union(members) | PhpType::Intersection(members) => {
members.iter().any(|m| type_contains_name(m, name))
}
PhpType::Generic(_, args) => args.iter().any(|a| type_contains_name(a, name)),
PhpType::Callable {
params,
return_type,
..
} => {
params
.iter()
.any(|p| type_contains_name(&p.type_hint, name))
|| return_type
.as_ref()
.is_some_and(|rt| type_contains_name(rt, name))
}
PhpType::ClassString(Some(inner))
| PhpType::InterfaceString(Some(inner))
| PhpType::KeyOf(inner)
| PhpType::ValueOf(inner) => type_contains_name(inner, name),
_ => false,
}
}
fn resolve_generic_wrapper_template(
wrapper_name: &str,
tpl_position: usize,
arg_text: &str,
rctx: &crate::completion::resolver::ResolutionCtx<'_>,
ctx: &VarResolutionCtx<'_>,
) -> Option<PhpType> {
let wrapper_cls = (ctx.class_loader)(wrapper_name)
.map(Arc::unwrap_or_clone)
.or_else(|| {
ctx.all_classes
.iter()
.find(|c| crate::util::short_name(&c.name) == crate::util::short_name(wrapper_name))
.map(|c| ClassInfo::clone(c))
})?;
let wrapper_ctor = wrapper_cls
.methods
.iter()
.find(|m| m.name == "__construct")?;
if wrapper_ctor.template_bindings.is_empty() {
return None;
}
let paren_start = arg_text.find('(')?;
let paren_end = arg_text.rfind(')')?;
let inner_args = arg_text[paren_start + 1..paren_end].trim();
let wrapper_subs =
build_constructor_template_subs(&wrapper_cls, wrapper_ctor, inner_args, rctx, ctx);
let wrapper_tpl = wrapper_cls.template_params.get(tpl_position)?;
wrapper_subs.get(wrapper_tpl).cloned()
}
fn resolve_rhs_array_access<'b>(
array_access: &ArrayAccess<'b>,
expr: &'b Expression<'b>,
ctx: &VarResolutionCtx<'_>,
) -> Vec<ResolvedType> {
let mut segments: Vec<ArrayBracketSegment> = Vec::new();
let mut current_expr: &Expression<'_> = array_access.array;
segments.push(classify_array_index(array_access.index));
while let Expression::ArrayAccess(inner) = current_expr {
segments.push(classify_array_index(inner.index));
current_expr = inner.array;
}
segments.reverse();
let access_offset = expr.span().start.offset as usize;
let raw_type: Option<PhpType> =
if let Expression::Variable(Variable::Direct(base_dv)) = current_expr {
let base_var = base_dv.name.to_string();
docblock::find_iterable_raw_type_in_source(ctx.content, access_offset, &base_var)
.or_else(|| {
let resolved = super::resolution::resolve_variable_types(
&base_var,
ctx.current_class,
ctx.all_classes,
ctx.content,
access_offset as u32,
ctx.class_loader,
Loaders::with_function(ctx.function_loader()),
);
if resolved.is_empty() {
None
} else {
Some(ResolvedType::types_joined(&resolved))
}
})
} else {
let base_resolved = resolve_rhs_expression(current_expr, ctx);
if base_resolved.is_empty() {
None
} else {
Some(ResolvedType::types_joined(&base_resolved))
}
};
let Some(mut current) = raw_type else {
return vec![];
};
if let Some(expanded) = crate::completion::type_resolution::resolve_type_alias_typed(
¤t,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
) {
current = expanded;
}
for seg in &segments {
let extracted = match seg {
ArrayBracketSegment::StringKey(key) => current
.shape_value_type(key)
.cloned()
.or_else(|| current.extract_value_type(true).cloned()),
ArrayBracketSegment::ElementAccess => current.extract_value_type(true).cloned(),
};
if let Some(element) = extracted {
current = element;
} else {
let class_element = crate::completion::type_resolution::type_hint_to_classes_typed(
¤t,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
)
.into_iter()
.find_map(|cls| {
let merged = crate::virtual_members::resolve_class_fully(&cls, ctx.class_loader);
super::foreach_resolution::extract_iterable_element_type_from_class(
&merged,
ctx.class_loader,
)
});
if let Some(element) = class_element {
current = element;
} else {
return vec![];
}
}
if let Some(expanded) = crate::completion::type_resolution::resolve_type_alias_typed(
¤t,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
) {
current = expanded;
}
}
ResolvedType::from_classes_with_hint(
crate::completion::type_resolution::type_hint_to_classes_typed(
¤t,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
),
current,
)
}
enum ArrayBracketSegment {
StringKey(String),
ElementAccess,
}
fn classify_array_index(index: &Expression<'_>) -> ArrayBracketSegment {
if let Expression::Literal(Literal::String(s)) = index {
let key = s.value.map(|v| v.to_string()).unwrap_or_else(|| {
crate::util::unquote_php_string(s.raw)
.unwrap_or(s.raw)
.to_string()
});
ArrayBracketSegment::StringKey(key)
} else {
ArrayBracketSegment::ElementAccess
}
}
pub(crate) fn build_function_template_subs(
func_info: &crate::types::FunctionInfo,
text_args: &str,
rctx: &crate::completion::resolver::ResolutionCtx<'_>,
) -> HashMap<String, PhpType> {
let args = crate::completion::conditional_resolution::split_text_args(text_args);
let mut subs = HashMap::new();
for (tpl_name, param_name) in &func_info.template_bindings {
let param_idx = match func_info
.parameters
.iter()
.position(|p| p.name == *param_name)
{
Some(idx) => idx,
None => continue,
};
let arg_text = match args.get(param_idx) {
Some(text) => text.trim(),
None => continue,
};
let param_hint = func_info
.parameters
.get(param_idx)
.and_then(|p| p.type_hint.as_ref());
let binding_mode = classify_template_binding(tpl_name, param_hint);
match binding_mode {
TemplateBindingMode::Direct => {
if let Some(resolved_type) = Backend::resolve_arg_text_to_type(arg_text, rctx) {
subs.insert(tpl_name.clone(), resolved_type);
}
}
TemplateBindingMode::CallableReturnType => {
if let Some(ret_type) =
crate::completion::source::helpers::extract_closure_return_type_from_text(
arg_text,
)
{
subs.insert(tpl_name.clone(), ret_type);
}
}
TemplateBindingMode::CallableParamType(position) => {
if let Some(param_type) =
crate::completion::source::helpers::extract_closure_param_type_from_text(
arg_text, position,
)
{
subs.insert(tpl_name.clone(), param_type);
}
}
TemplateBindingMode::ArrayElement => {
if arg_text.starts_with('[') && arg_text.ends_with(']') {
let inner = arg_text[1..arg_text.len() - 1].trim();
if !inner.is_empty() {
let first_elem =
crate::completion::conditional_resolution::split_text_args(inner);
if let Some(elem) = first_elem.first()
&& let Some(resolved_type) =
Backend::resolve_arg_text_to_type(elem.trim(), rctx)
{
subs.insert(tpl_name.clone(), resolved_type);
}
}
} else if let Some(resolved_type) =
Backend::resolve_arg_text_to_type(arg_text, rctx)
{
subs.insert(tpl_name.clone(), resolved_type);
}
}
TemplateBindingMode::GenericWrapper(ref wrapper_name, tpl_position) => {
if is_array_like_wrapper(wrapper_name)
&& arg_text.starts_with('$')
&& let Some(resolved) = resolve_arg_variable_raw_type(arg_text, rctx)
&& let Some(concrete) = extract_array_type_at_position(&resolved, tpl_position)
{
subs.insert(tpl_name.clone(), concrete);
continue;
}
if let Some(resolved_type) = Backend::resolve_arg_text_to_type(arg_text, rctx) {
subs.insert(tpl_name.clone(), resolved_type);
}
}
}
}
subs
}
fn resolve_arg_variable_raw_type(
arg_text: &str,
rctx: &crate::completion::resolver::ResolutionCtx<'_>,
) -> Option<PhpType> {
let var_name = arg_text.trim();
if !var_name.starts_with('$') {
return None;
}
if let Some(arrow_pos) = var_name.find("->") {
let base = &var_name[..arrow_pos];
let prop = &var_name[arrow_pos + 2..];
if !prop.is_empty() && !prop.contains("->") && !prop.contains('(') {
let base_classes = ResolvedType::into_arced_classes(
crate::completion::resolver::resolve_target_classes(
base,
crate::types::AccessKind::Arrow,
rctx,
),
);
for cls in &base_classes {
if let Some(hint) =
crate::inheritance::resolve_property_type_hint(cls, prop, rctx.class_loader)
{
return Some(hint);
}
}
}
}
if let Some(raw) = crate::docblock::find_iterable_raw_type_in_source(
rctx.content,
rctx.cursor_offset as usize,
var_name,
) {
return Some(raw);
}
let default_class = crate::types::ClassInfo::default();
let current_class = rctx.current_class.unwrap_or(&default_class);
let resolved = super::resolution::resolve_variable_types(
var_name,
current_class,
rctx.all_classes,
rctx.content,
rctx.cursor_offset,
rctx.class_loader,
Loaders::with_function(rctx.function_loader),
);
if resolved.is_empty() {
None
} else {
Some(ResolvedType::types_joined(&resolved))
}
}
fn extract_array_type_at_position(ty: &PhpType, position: usize) -> Option<PhpType> {
match position {
0 => ty.extract_key_type(false).cloned(),
1 => ty.extract_value_type(false).cloned(),
_ => None,
}
}
fn is_array_like_wrapper(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"array" | "list" | "non-empty-array" | "non-empty-list" | "iterable"
) || crate::util::short_name(name).eq_ignore_ascii_case("arrayable")
}
fn resolve_rhs_call<'b>(
call: &'b Call<'b>,
expr: &'b Expression<'b>,
ctx: &VarResolutionCtx<'_>,
) -> Vec<ResolvedType> {
match call {
Call::Function(func_call) => resolve_rhs_function_call(func_call, expr, ctx),
Call::Method(method_call) => resolve_rhs_method_call_inner(
method_call.object,
&method_call.method,
&method_call.argument_list,
ctx,
),
Call::NullSafeMethod(method_call) => resolve_rhs_method_call_inner(
method_call.object,
&method_call.method,
&method_call.argument_list,
ctx,
),
Call::StaticMethod(static_call) => resolve_rhs_static_call(static_call, ctx),
}
}
fn resolve_rhs_function_call<'b>(
func_call: &'b FunctionCall<'b>,
expr: &'b Expression<'b>,
ctx: &VarResolutionCtx<'_>,
) -> Vec<ResolvedType> {
let current_class_name: &str = &ctx.current_class.name;
let all_classes = ctx.all_classes;
let content = ctx.content;
let class_loader = ctx.class_loader;
let function_loader = ctx.function_loader();
let func_name = match func_call.function {
Expression::Identifier(ident) => Some(ident.value().to_string()),
_ => None,
};
if let Some(ref name) = func_name
&& let Some(element_type) = super::raw_type_inference::resolve_array_func_element_type(
name,
&func_call.argument_list,
ctx,
)
{
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
&element_type,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, element_type);
}
}
if let Some(ref name) = func_name
&& let Some(raw_type) = super::raw_type_inference::resolve_array_func_raw_type(
name,
&func_call.argument_list,
ctx,
)
{
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
&raw_type,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, raw_type);
}
return vec![resolved_type_with_lookup(
raw_type,
current_class_name,
all_classes,
class_loader,
)];
}
if let Some(ref name) = func_name
&& let Some(fl) = function_loader
&& let Some(func_info) = fl(name)
{
if let Some(ref cond) = func_info.conditional_return {
let var_resolver = build_var_resolver_from_ctx(ctx);
let resolved_type = resolve_conditional_with_args(
cond,
&func_info.parameters,
&func_call.argument_list,
Some(&var_resolver),
Some(current_class_name),
);
if let Some(ref ty) = resolved_type {
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
ty,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, ty.clone());
}
}
}
if !func_info.template_params.is_empty()
&& !func_info.template_bindings.is_empty()
&& func_info.return_type.is_some()
{
let text_args =
super::raw_type_inference::extract_argument_text(&func_call.argument_list, content);
if !text_args.is_empty() {
let rctx = ctx.as_resolution_ctx();
let subs = build_function_template_subs(&func_info, &text_args, &rctx);
if !subs.is_empty()
&& let Some(ref ret) = func_info.return_type
{
let substituted = ret.substitute(&subs);
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
&substituted,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, substituted);
}
}
}
}
if let Some(ref ret) = func_info.return_type {
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
ret,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, ret.clone());
}
if *ret == PhpType::void() {
return vec![ResolvedType::from_type_string(PhpType::null())];
}
return vec![resolved_type_with_lookup(
ret.clone(),
current_class_name,
all_classes,
class_loader,
)];
}
}
if let Some(ref name) = func_name
&& function_loader.is_none()
&& let Some(ret) =
crate::completion::source::helpers::extract_function_return_from_source(name, content)
{
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
&ret,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, ret);
}
if ret == PhpType::void() {
return vec![ResolvedType::from_type_string(PhpType::null())];
}
return vec![resolved_type_with_lookup(
ret,
current_class_name,
all_classes,
class_loader,
)];
}
if let Expression::Variable(Variable::Direct(dv)) = func_call.function {
let var_name = dv.name.to_string();
let offset = expr.span().start.offset as usize;
if let Some(raw_type) =
crate::docblock::find_iterable_raw_type_in_source(content, offset, &var_name)
&& let Some(ret_type) = raw_type.callable_return_type()
{
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
ret_type,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, ret_type.clone());
}
}
if let Some(ret) =
crate::completion::source::helpers::extract_closure_return_type_from_assignment(
&var_name,
content,
ctx.cursor_offset,
)
{
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
&ret,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, ret);
}
}
let rctx = ctx.as_resolution_ctx();
if let Some(ret) =
crate::completion::source::helpers::extract_first_class_callable_return_type(
&var_name, &rctx,
)
{
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
&ret,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, ret);
}
}
let rctx = ctx.as_resolution_ctx();
let var_classes =
ResolvedType::into_arced_classes(crate::completion::resolver::resolve_target_classes(
&var_name,
crate::types::AccessKind::Arrow,
&rctx,
));
for owner in &var_classes {
if let Some(invoke) = owner.methods.iter().find(|m| m.name == "__invoke")
&& let Some(ref ret) = invoke.return_type
{
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
ret,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, ret.clone());
}
if !ret.is_empty() {
return vec![resolved_type_with_lookup(
ret.clone(),
current_class_name,
all_classes,
class_loader,
)];
}
}
}
}
let callee_expr = match func_call.function {
Expression::Parenthesized(p) => p.expression,
other => other,
};
if !matches!(callee_expr, Expression::Variable(Variable::Direct(_))) {
if let Some(parsed_ret_type) = extract_closure_or_arrow_return_type(callee_expr) {
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
&parsed_ret_type,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, parsed_ret_type);
}
}
let callee_results = resolve_rhs_expression(callee_expr, ctx);
for rt in &callee_results {
if let Some(ref owner_cls) = rt.class_info
&& let Some(invoke) = owner_cls.methods.iter().find(|m| m.name == "__invoke")
&& let Some(ref ret) = invoke.return_type
{
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
ret,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, ret.clone());
}
if !ret.is_empty() {
return vec![resolved_type_with_lookup(
ret.clone(),
current_class_name,
all_classes,
class_loader,
)];
}
}
}
}
vec![]
}
fn resolve_rhs_method_call_inner<'b>(
object: &'b Expression<'b>,
method: &'b ClassLikeMemberSelector<'b>,
argument_list: &'b ArgumentList<'b>,
ctx: &VarResolutionCtx<'_>,
) -> Vec<ResolvedType> {
let method_name = match method {
ClassLikeMemberSelector::Identifier(ident) => ident.value.to_string(),
_ => return vec![],
};
let (owner_classes, receiver_resolved): (Vec<ClassInfo>, Vec<ResolvedType>) =
if let Expression::Variable(Variable::Direct(dv)) = object
&& dv.name == "$this"
{
let classes: Vec<ClassInfo> = ctx
.all_classes
.iter()
.find(|c| c.name == ctx.current_class.name)
.map(|c| ClassInfo::clone(c))
.into_iter()
.collect();
(classes, vec![])
} else if let Expression::Variable(Variable::Direct(dv)) = object {
let var = dv.name.to_string();
let resolved = crate::completion::variable::resolution::resolve_variable_types(
&var,
ctx.current_class,
ctx.all_classes,
ctx.content,
object.span().end.offset,
ctx.class_loader,
crate::completion::resolver::Loaders::with_function(ctx.function_loader()),
);
if !resolved.is_empty() {
let classes = ResolvedType::into_classes(resolved.clone());
(classes, resolved)
} else {
let classes: Vec<ClassInfo> = ResolvedType::into_classes(
crate::completion::resolver::resolve_target_classes(
&var,
crate::types::AccessKind::Arrow,
&ctx.as_resolution_ctx(),
),
);
(classes, vec![])
}
} else {
let resolved = resolve_rhs_expression(object, ctx);
let classes = ResolvedType::into_classes(resolved.clone());
(classes, resolved)
};
let text_args = super::raw_type_inference::extract_argument_text(argument_list, ctx.content);
let rctx = ctx.as_resolution_ctx();
let var_resolver = build_var_resolver_from_ctx(ctx);
for owner in &owner_classes {
let template_subs =
Backend::build_method_template_subs(owner, &method_name, &text_args, &rctx);
let mr_ctx = MethodReturnCtx {
all_classes: ctx.all_classes,
class_loader: ctx.class_loader,
template_subs: &template_subs,
var_resolver: Some(&var_resolver),
cache: ctx.resolved_class_cache,
calling_class_name: Some(&ctx.current_class.name),
is_static: false,
};
let merged = crate::virtual_members::resolve_class_fully(owner, ctx.class_loader);
let ret_type_string = merged
.methods
.iter()
.find(|m| m.name == method_name)
.and_then(|m| m.return_type.as_ref())
.map(|ret| {
let substituted = if !template_subs.is_empty() {
ret.substitute(&template_subs)
} else {
ret.clone()
};
let receiver_type = if substituted.contains_self_ref() {
receiver_type_for_owner(&receiver_resolved, &owner.name)
} else {
None
};
match receiver_type {
Some(rt) => substituted.replace_self_with_type(&rt),
None => substituted.replace_self(&owner.name),
}
});
let results = Backend::resolve_method_return_types_with_args(
owner,
&method_name,
&text_args,
&mr_ctx,
);
if !results.is_empty() {
let classes: Vec<ClassInfo> = results.into_iter().map(Arc::unwrap_or_clone).collect();
let has_conditional = merged
.methods
.iter()
.any(|m| m.name == method_name && m.conditional_return.is_some());
let effective_hint = if has_conditional {
None
} else {
ret_type_string
};
return match effective_hint {
Some(hint) => ResolvedType::from_classes_with_hint(classes, hint),
None => ResolvedType::from_classes(classes),
};
}
if let Some(ref hint) = ret_type_string {
let expanded = crate::completion::type_resolution::resolve_type_alias_typed(
hint,
&owner.name,
ctx.all_classes,
ctx.class_loader,
);
let parsed_effective = match expanded {
Some(e) => e,
None => hint.clone(),
};
if parsed_effective == PhpType::void() {
return vec![ResolvedType::from_type_string(PhpType::null())];
}
return vec![resolved_type_with_lookup(
parsed_effective,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
)];
}
}
vec![]
}
fn receiver_type_for_owner(
receiver_resolved: &[ResolvedType],
owner_name: &str,
) -> Option<PhpType> {
for rt in receiver_resolved {
let matches = rt
.class_info
.as_ref()
.is_some_and(|ci| ci.name == owner_name)
&& matches!(rt.type_string, PhpType::Generic(_, _));
if matches {
return Some(rt.type_string.clone());
}
}
None
}
fn resolve_rhs_static_call(
static_call: &StaticMethodCall<'_>,
ctx: &VarResolutionCtx<'_>,
) -> Vec<ResolvedType> {
let current_class_name: &str = &ctx.current_class.name;
let class_name = match static_call.class {
Expression::Self_(_) => Some(current_class_name.to_string()),
Expression::Static(_) => Some(current_class_name.to_string()),
Expression::Parent(_) => ctx.current_class.parent_class.clone(),
Expression::Identifier(ident) => Some(ident.value().to_string()),
Expression::Variable(Variable::Direct(dv)) => {
let var_name = dv.name.to_string();
let targets =
crate::completion::variable::class_string_resolution::resolve_class_string_targets(
&var_name,
ctx.current_class,
ctx.all_classes,
ctx.content,
ctx.cursor_offset,
ctx.class_loader,
);
if let Some(first) = targets.first() {
Some(first.name.clone())
} else {
let resolved = super::resolution::resolve_variable_types(
&var_name,
ctx.current_class,
ctx.all_classes,
ctx.content,
ctx.cursor_offset,
ctx.class_loader,
Loaders::with_function(ctx.function_loader()),
);
resolved.iter().find_map(|rt| match &rt.type_string {
PhpType::ClassString(Some(inner)) => inner.base_name().map(|s| s.to_string()),
PhpType::Nullable(inner) => match inner.as_ref() {
PhpType::ClassString(Some(cs_inner)) => {
cs_inner.base_name().map(|s| s.to_string())
}
_ => None,
},
PhpType::Union(members) => members.iter().find_map(|m| match m {
PhpType::ClassString(Some(inner)) => {
inner.base_name().map(|s| s.to_string())
}
PhpType::Nullable(inner) => match inner.as_ref() {
PhpType::ClassString(Some(cs_inner)) => {
cs_inner.base_name().map(|s| s.to_string())
}
_ => None,
},
_ => None,
}),
_ => None,
})
}
}
_ => None,
};
if let Some(cls_name) = class_name
&& let ClassLikeMemberSelector::Identifier(ident) = &static_call.method
{
let method_name = ident.value.to_string();
let owner = ctx
.all_classes
.iter()
.find(|c| c.name == cls_name)
.map(|c| ClassInfo::clone(c))
.or_else(|| (ctx.class_loader)(&cls_name).map(Arc::unwrap_or_clone));
if let Some(ref owner) = owner {
let text_args = super::raw_type_inference::extract_argument_text(
&static_call.argument_list,
ctx.content,
);
let rctx = ctx.as_resolution_ctx();
let template_subs =
Backend::build_method_template_subs(owner, &method_name, &text_args, &rctx);
let var_resolver = build_var_resolver_from_ctx(ctx);
let mr_ctx = MethodReturnCtx {
all_classes: ctx.all_classes,
class_loader: ctx.class_loader,
template_subs: &template_subs,
var_resolver: Some(&var_resolver),
cache: ctx.resolved_class_cache,
calling_class_name: Some(&ctx.current_class.name),
is_static: true,
};
let merged = crate::virtual_members::resolve_class_fully(owner, ctx.class_loader);
let ret_type_string = merged
.methods
.iter()
.find(|m| m.name == method_name)
.and_then(|m| m.return_type.as_ref())
.map(|ret| {
let substituted = if !template_subs.is_empty() {
ret.substitute(&template_subs)
} else {
ret.clone()
};
substituted.replace_self(&owner.name)
});
let results = Backend::resolve_method_return_types_with_args(
owner,
&method_name,
&text_args,
&mr_ctx,
);
if !results.is_empty() {
let classes: Vec<ClassInfo> =
results.into_iter().map(Arc::unwrap_or_clone).collect();
let has_conditional = merged
.methods
.iter()
.any(|m| m.name == method_name && m.conditional_return.is_some());
let effective_hint = if has_conditional {
None
} else {
ret_type_string
};
return match effective_hint {
Some(hint) => ResolvedType::from_classes_with_hint(classes, hint),
None => ResolvedType::from_classes(classes),
};
}
if let Some(ref hint) = ret_type_string {
if *hint == PhpType::void() {
return vec![ResolvedType::from_type_string(PhpType::null())];
}
return vec![resolved_type_with_lookup(
hint.clone(),
current_class_name,
ctx.all_classes,
ctx.class_loader,
)];
}
}
}
vec![]
}
fn resolve_rhs_property_access(
access: &Access<'_>,
ctx: &VarResolutionCtx<'_>,
) -> Vec<ResolvedType> {
let current_class_name: &str = &ctx.current_class.name;
let all_classes = ctx.all_classes;
let class_loader = ctx.class_loader;
fn resolve_property_with_hint(
prop_name: &str,
owner: &ClassInfo,
current_class_name: &str,
all_classes: &[Arc<ClassInfo>],
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> Vec<ResolvedType> {
let type_hint =
crate::inheritance::resolve_property_type_hint(owner, prop_name, class_loader);
let resolved = crate::completion::type_resolution::resolve_property_types(
prop_name,
owner,
all_classes,
class_loader,
);
if resolved.is_empty() {
return match type_hint {
Some(hint) => {
vec![resolved_type_with_lookup(
hint,
current_class_name,
all_classes,
class_loader,
)]
}
_ => vec![],
};
}
match type_hint {
Some(hint) => ResolvedType::from_classes_with_hint(resolved, hint),
None => ResolvedType::from_classes(resolved),
}
}
if let Access::ClassConstant(cca) = access {
let class_name = match cca.class {
Expression::Identifier(ident) => Some(ident.value().to_string()),
Expression::Self_(_) => Some(current_class_name.to_string()),
Expression::Static(_) => Some(current_class_name.to_string()),
_ => None,
};
if let Some(class_name) = class_name {
let resolved_name = class_name.strip_prefix('\\').unwrap_or(&class_name);
let resolved_typed = PhpType::Named(resolved_name.to_string());
let target_classes = crate::completion::type_resolution::type_hint_to_classes_typed(
&resolved_typed,
current_class_name,
all_classes,
class_loader,
);
let const_name = match &cca.constant {
ClassLikeConstantSelector::Identifier(ident) => Some(ident.value.to_string()),
_ => None,
};
if let Some(const_name) = const_name {
for cls in &target_classes {
if let Some(c) = cls.constants.iter().find(|c| c.name == const_name) {
if c.is_enum_case {
return ResolvedType::from_classes(target_classes);
}
if let Some(ref th) = c.type_hint {
let resolved =
crate::completion::type_resolution::type_hint_to_classes_typed(
th,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, th.clone());
}
}
if let Some(ref val) = c.value
&& let Some(ts) = infer_type_from_constant_value(val)
{
let resolved =
crate::completion::type_resolution::type_hint_to_classes_typed(
&ts,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return ResolvedType::from_classes_with_hint(resolved, ts);
}
return vec![ResolvedType::from_type_string(ts)];
}
}
}
}
}
return vec![];
}
let (object_expr, prop_selector) = match access {
Access::Property(pa) => (Some(pa.object), Some(&pa.property)),
Access::NullSafeProperty(pa) => (Some(pa.object), Some(&pa.property)),
_ => (None, None),
};
if let Some(obj) = object_expr
&& let Some(sel) = prop_selector
{
let prop_name = match sel {
ClassLikeMemberSelector::Identifier(ident) => Some(ident.value.to_string()),
_ => None,
};
if let Some(prop_name) = prop_name {
let owner_classes: Vec<ClassInfo> = if let Expression::Variable(Variable::Direct(dv)) =
obj
&& dv.name == "$this"
{
all_classes
.iter()
.find(|c| c.name == current_class_name)
.map(|c| ClassInfo::clone(c))
.into_iter()
.collect()
} else if let Expression::Variable(Variable::Direct(dv)) = obj {
let var = dv.name.to_string();
ResolvedType::into_classes(crate::completion::resolver::resolve_target_classes(
&var,
crate::types::AccessKind::Arrow,
&ctx.as_resolution_ctx(),
))
} else {
ResolvedType::into_classes(resolve_rhs_expression(obj, ctx))
};
for owner in &owner_classes {
let resolved = resolve_property_with_hint(
&prop_name,
owner,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
return resolved;
}
}
}
}
vec![]
}
fn resolve_rhs_clone(clone_expr: &Clone<'_>, ctx: &VarResolutionCtx<'_>) -> Vec<ResolvedType> {
let structural = resolve_rhs_expression(clone_expr.object, ctx);
if !structural.is_empty() {
return structural;
}
let obj_span = clone_expr.object.span();
let start = obj_span.start.offset as usize;
let end = obj_span.end.offset as usize;
if end <= ctx.content.len() {
let obj_text = ctx.content[start..end].trim();
if !obj_text.is_empty() {
let rctx = ctx.as_resolution_ctx();
return crate::completion::resolver::resolve_target_classes(
obj_text,
crate::types::AccessKind::Arrow,
&rctx,
);
}
}
vec![]
}
fn extract_closure_or_arrow_return_type(expr: &Expression<'_>) -> Option<PhpType> {
match expr {
Expression::ArrowFunction(arrow) => arrow
.return_type_hint
.as_ref()
.map(|rth| extract_hint_type(&rth.hint)),
Expression::Closure(closure) => closure
.return_type_hint
.as_ref()
.map(|rth| extract_hint_type(&rth.hint)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_direct_param() {
let ty = PhpType::parse("T");
let mode = classify_template_binding("T", Some(&ty));
assert!(matches!(mode, TemplateBindingMode::Direct));
}
#[test]
fn classify_array_element() {
let ty = PhpType::parse("T[]");
let mode = classify_template_binding("T", Some(&ty));
assert!(matches!(mode, TemplateBindingMode::ArrayElement));
}
#[test]
fn classify_generic_wrapper() {
let ty = PhpType::parse("Collection<T>");
let mode = classify_template_binding("T", Some(&ty));
assert!(matches!(mode, TemplateBindingMode::GenericWrapper(_, 0)));
}
#[test]
fn classify_callable_return_type() {
let ty =
PhpType::parse("callable(TReduceInitial|TReduceReturnType, TValue): TReduceReturnType");
let mode = classify_template_binding("TReduceReturnType", Some(&ty));
assert!(matches!(mode, TemplateBindingMode::CallableReturnType));
}
#[test]
fn classify_closure_return_type() {
let ty = PhpType::parse("Closure(int, string): T");
let mode = classify_template_binding("T", Some(&ty));
assert!(matches!(mode, TemplateBindingMode::CallableReturnType));
}
#[test]
fn classify_callable_param_type() {
let ty = PhpType::parse("callable(T): void");
let mode = classify_template_binding("T", Some(&ty));
assert!(matches!(mode, TemplateBindingMode::CallableParamType(0)));
}
#[test]
fn classify_callable_param_type_second_position() {
let ty = PhpType::parse("Closure(int, T): void");
let mode = classify_template_binding("T", Some(&ty));
assert!(matches!(mode, TemplateBindingMode::CallableParamType(1)));
}
#[test]
fn classify_callable_return_type_preferred_over_param() {
let ty = PhpType::parse("callable(T): T");
let mode = classify_template_binding("T", Some(&ty));
assert!(matches!(mode, TemplateBindingMode::CallableReturnType));
}
#[test]
fn classify_nullable_union_callable() {
let ty = PhpType::parse("callable(int): T|null");
let mode = classify_template_binding("T", Some(&ty));
assert!(matches!(mode, TemplateBindingMode::CallableReturnType));
}
#[test]
fn classify_none_hint() {
let mode = classify_template_binding("T", None);
assert!(matches!(mode, TemplateBindingMode::Direct));
}
#[test]
fn type_contains_name_simple() {
let ty = PhpType::Named("Foo".to_owned());
assert!(type_contains_name(&ty, "Foo"));
assert!(!type_contains_name(&ty, "Bar"));
}
#[test]
fn type_contains_name_nested_callable() {
let ty = PhpType::parse("callable(int): Decimal");
assert!(type_contains_name(&ty, "Decimal"));
assert!(type_contains_name(&ty, "int"));
assert!(!type_contains_name(&ty, "string"));
}
#[test]
fn type_contains_name_union() {
let ty = PhpType::parse("Foo|Bar|null");
assert!(type_contains_name(&ty, "Foo"));
assert!(type_contains_name(&ty, "Bar"));
assert!(type_contains_name(&ty, "null"));
assert!(!type_contains_name(&ty, "Baz"));
}
}