use mago_span::HasSpan;
use mago_syntax::ast::*;
use std::sync::Arc;
use crate::docblock;
use crate::php_type::PhpType;
use crate::types::{ClassInfo, ResolvedType};
use crate::util::short_name;
use crate::completion::resolver::{Loaders, VarResolutionCtx};
pub(in crate::completion) fn resolve_expression_type<'b>(
expr: &'b mago_syntax::ast::Expression<'b>,
ctx: &VarResolutionCtx<'_>,
) -> Option<PhpType> {
let resolved = super::rhs_resolution::resolve_rhs_expression(expr, ctx);
if resolved.is_empty() {
return None;
}
Some(ResolvedType::types_joined(&resolved))
}
fn expression_uses_variable(expr: &mago_syntax::ast::Expression<'_>, var_name: &str) -> bool {
use mago_syntax::ast::{Access, Call, Expression, Variable};
match expr {
Expression::Variable(Variable::Direct(dv)) => dv.name == var_name,
Expression::Call(call) => match call {
Call::Method(mc) => expression_uses_variable(mc.object, var_name),
Call::NullSafeMethod(mc) => expression_uses_variable(mc.object, var_name),
_ => false,
},
Expression::Access(access) => match access {
Access::Property(pa) => expression_uses_variable(pa.object, var_name),
Access::NullSafeProperty(pa) => expression_uses_variable(pa.object, var_name),
_ => false,
},
_ => false,
}
}
pub(in crate::completion) fn try_resolve_foreach_value_type<'b>(
foreach: &'b Foreach<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
conditional: bool,
) {
let value_expr = foreach.target.value();
let value_var_name = match value_expr {
Expression::Variable(Variable::Direct(dv)) => dv.name.to_string(),
_ => return,
};
if value_var_name != ctx.var_name {
return;
}
let foreach_offset = foreach.foreach.span().start.offset as usize;
if let Some((var_type, var_name)) =
crate::docblock::find_inline_var_docblock(ctx.content, foreach_offset)
{
let name_matches = var_name.as_ref().is_none_or(|n| *n == value_var_name);
if name_matches {
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
&var_type,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
);
if !resolved.is_empty() {
let resolved_types = ResolvedType::from_classes_with_hint(resolved, var_type);
if conditional {
ResolvedType::extend_unique(results, resolved_types);
} else {
results.clear();
ResolvedType::extend_unique(results, resolved_types);
}
return;
}
}
}
let value_shadows_iterator = expression_uses_variable(foreach.expression, &value_var_name);
let raw_type = if value_shadows_iterator {
None
} else {
resolve_expression_type(foreach.expression, ctx).filter(|pt| pt.has_type_structure())
}
.or_else(|| {
let expr_span = foreach.expression.span();
let expr_start = expr_span.start.offset as usize;
let expr_end = expr_span.end.offset as usize;
let expr_text = ctx.content.get(expr_start..expr_end)?.trim();
if !expr_text.starts_with('$') || expr_text.contains("->") || expr_text.contains("::") {
return None;
}
let foreach_offset = foreach.foreach.span().start.offset as usize;
docblock::find_iterable_raw_type_in_source(ctx.content, foreach_offset, expr_text)
})
.or_else(|| {
if value_shadows_iterator {
return None;
}
let expr_span = foreach.expression.span();
let expr_start = expr_span.start.offset as usize;
let expr_end = expr_span.end.offset as usize;
let expr_text = ctx.content.get(expr_start..expr_end)?.trim();
if !expr_text.starts_with('$') || expr_text.contains("->") || expr_text.contains("::") {
return None;
}
let foreach_offset = foreach.foreach.span().start.offset;
let resolved = super::resolution::resolve_variable_types(
expr_text,
ctx.current_class,
ctx.all_classes,
ctx.content,
foreach_offset,
ctx.class_loader,
Loaders::with_function(ctx.function_loader()),
);
if resolved.is_empty() {
None
} else {
let joined = ResolvedType::types_joined(&resolved);
if !joined.has_type_structure() {
None
} else {
Some(joined)
}
}
});
let raw_type = raw_type.map(|rt| {
crate::completion::type_resolution::resolve_type_alias_typed(
&rt,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
)
.unwrap_or(rt)
});
if let Some(ref parsed) = raw_type
&& let Some(element_type) = parsed.extract_value_type(false)
{
push_foreach_resolved_types_typed(element_type, ctx, results, conditional);
return;
}
let iterable_classes = if let Some(ref parsed) = raw_type {
crate::completion::type_resolution::type_hint_to_classes_typed(
parsed,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
)
.into_iter()
.map(Arc::new)
.collect()
} else {
resolve_foreach_expression_to_classes(foreach.expression, ctx)
};
for cls in &iterable_classes {
let merged = crate::virtual_members::resolve_class_fully_maybe_cached(
cls,
ctx.class_loader,
ctx.resolved_class_cache,
);
if let Some(value_type) =
extract_iterable_element_type_from_class(&merged, ctx.class_loader)
{
push_foreach_resolved_types_typed(&value_type, ctx, results, conditional);
return;
}
}
}
pub(in crate::completion) fn try_resolve_foreach_key_type<'b>(
foreach: &'b Foreach<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
conditional: bool,
) {
let key_expr = match foreach.target.key() {
Some(expr) => expr,
None => return,
};
let key_var_name = match key_expr {
Expression::Variable(Variable::Direct(dv)) => dv.name.to_string(),
_ => return,
};
if key_var_name != ctx.var_name {
return;
}
let raw_type = resolve_expression_type(foreach.expression, ctx)
.filter(|pt| pt.has_type_structure())
.or_else(|| {
let expr_span = foreach.expression.span();
let expr_start = expr_span.start.offset as usize;
let expr_end = expr_span.end.offset as usize;
let expr_text = ctx.content.get(expr_start..expr_end)?.trim();
if !expr_text.starts_with('$') || expr_text.contains("->") || expr_text.contains("::") {
return None;
}
let foreach_offset = foreach.foreach.span().start.offset as usize;
docblock::find_iterable_raw_type_in_source(ctx.content, foreach_offset, expr_text)
})
.or_else(|| {
let expr_span = foreach.expression.span();
let expr_start = expr_span.start.offset as usize;
let expr_end = expr_span.end.offset as usize;
let expr_text = ctx.content.get(expr_start..expr_end)?.trim();
if !expr_text.starts_with('$') || expr_text.contains("->") || expr_text.contains("::") {
return None;
}
let foreach_offset = foreach.foreach.span().start.offset;
let resolved = super::resolution::resolve_variable_types(
expr_text,
ctx.current_class,
ctx.all_classes,
ctx.content,
foreach_offset,
ctx.class_loader,
Loaders::with_function(ctx.function_loader()),
);
if resolved.is_empty() {
None
} else {
let joined = ResolvedType::types_joined(&resolved);
if !joined.has_type_structure() {
None
} else {
Some(joined)
}
}
});
let raw_type = raw_type.map(|rt| {
crate::completion::type_resolution::resolve_type_alias_typed(
&rt,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
)
.unwrap_or(rt)
});
if let Some(ref parsed) = raw_type
&& let Some(key_type) = parsed.extract_key_type(true)
{
push_foreach_resolved_types_typed(key_type, ctx, results, conditional);
return;
}
let iterable_classes = if let Some(ref parsed) = raw_type {
crate::completion::type_resolution::type_hint_to_classes_typed(
parsed,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
)
.into_iter()
.map(Arc::new)
.collect()
} else {
resolve_foreach_expression_to_classes(foreach.expression, ctx)
};
for cls in &iterable_classes {
let merged = crate::virtual_members::resolve_class_fully_maybe_cached(
cls,
ctx.class_loader,
ctx.resolved_class_cache,
);
if let Some(key_type) = extract_iterable_key_type_from_class(&merged, ctx.class_loader) {
push_foreach_resolved_types_typed(&key_type, ctx, results, conditional);
return;
}
}
}
fn push_foreach_resolved_types_typed(
ty: &PhpType,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
conditional: bool,
) {
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
ty,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
);
let resolved_types = if resolved.is_empty() {
if ty.is_mixed() {
return;
}
vec![ResolvedType::from_type_string(ty.clone())]
} else {
ResolvedType::from_classes_with_hint(resolved, ty.clone())
};
if !conditional {
results.clear();
}
ResolvedType::extend_unique(results, resolved_types);
}
fn resolve_foreach_expression_to_classes<'b>(
expression: &'b Expression<'b>,
ctx: &VarResolutionCtx<'_>,
) -> Vec<Arc<ClassInfo>> {
let expr_span = expression.span();
let expr_start = expr_span.start.offset as usize;
let expr_end = expr_span.end.offset as usize;
let expr_text = match ctx.content.get(expr_start..expr_end) {
Some(t) => t.trim(),
None => return vec![],
};
if expr_text.is_empty() {
return vec![];
}
ResolvedType::into_arced_classes(crate::completion::resolver::resolve_target_classes(
expr_text,
crate::types::AccessKind::Arrow,
&ctx.as_resolution_ctx(),
))
}
const ITERABLE_IFACE_NAMES: &[&str] = &[
"Iterator",
"IteratorAggregate",
"Traversable",
"ArrayAccess",
"Enumerable",
];
pub(in crate::completion) fn extract_iterable_element_type_from_class(
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> Option<PhpType> {
for (name, args) in &class.implements_generics {
let short = short_name(name);
if ITERABLE_IFACE_NAMES.contains(&short) && !args.is_empty() {
let value = args.last().unwrap();
if !value.is_scalar() {
return Some(value.clone());
}
}
}
for (name, args) in &class.implements_generics {
let short = short_name(name);
if !ITERABLE_IFACE_NAMES.contains(&short)
&& !args.is_empty()
&& let Some(iface) = class_loader(name)
&& is_transitive_iterable(&iface, class_loader)
{
let value = args.last().unwrap();
if !value.is_scalar() {
return Some(value.clone());
}
}
}
for (_, args) in &class.extends_generics {
if !args.is_empty() {
let value = args.last().unwrap();
if !value.is_scalar() {
return Some(value.clone());
}
}
}
None
}
fn extract_iterable_key_type_from_class(
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> Option<PhpType> {
for (name, args) in &class.implements_generics {
let short = short_name(name);
if ITERABLE_IFACE_NAMES.contains(&short) && args.len() >= 2 {
let key = &args[0];
if !key.is_scalar() {
return Some(key.clone());
}
}
}
for (name, args) in &class.implements_generics {
let short = short_name(name);
if !ITERABLE_IFACE_NAMES.contains(&short)
&& args.len() >= 2
&& let Some(iface) = class_loader(name)
&& is_transitive_iterable(&iface, class_loader)
{
let key = &args[0];
if !key.is_scalar() {
return Some(key.clone());
}
}
}
for (_, args) in &class.extends_generics {
if args.len() >= 2 {
let key = &args[0];
if !key.is_scalar() {
return Some(key.clone());
}
}
}
None
}
fn is_transitive_iterable(
iface: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> bool {
for parent in &iface.interfaces {
let s = short_name(parent);
if ITERABLE_IFACE_NAMES.contains(&s) {
return true;
}
}
for (name, _) in &iface.extends_generics {
let s = short_name(name);
if ITERABLE_IFACE_NAMES.contains(&s) {
return true;
}
}
if let Some(ref parent_name) = iface.parent_class {
let s = short_name(parent_name);
if ITERABLE_IFACE_NAMES.contains(&s) {
return true;
}
if let Some(parent) = class_loader(parent_name) {
return is_transitive_iterable(&parent, class_loader);
}
}
false
}
pub(in crate::completion) fn try_resolve_destructured_type<'b>(
assignment: &'b Assignment<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
conditional: bool,
) {
let elements = match assignment.lhs {
Expression::Array(arr) => &arr.elements,
Expression::List(list) => &list.elements,
_ => return,
};
let var_name = ctx.var_name;
let mut shape_key: Option<String> = None;
let mut found = false;
let mut positional_index: usize = 0;
for elem in elements.iter() {
match elem {
ArrayElement::KeyValue(kv) => {
if let Expression::Variable(Variable::Direct(dv)) = kv.value
&& dv.name == var_name
{
found = true;
shape_key = extract_destructuring_key(kv.key);
break;
}
}
ArrayElement::Value(val) => {
if let Expression::Variable(Variable::Direct(dv)) = val.value
&& dv.name == var_name
{
found = true;
shape_key = Some(positional_index.to_string());
break;
}
positional_index += 1;
}
_ => {}
}
}
if !found {
return;
}
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 stmt_offset = assignment.span().start.offset as usize;
if let Some((var_type, _var_name_opt)) =
docblock::find_inline_var_docblock(content, stmt_offset)
{
if let Some(ref key) = shape_key
&& let Some(entry_type) = var_type.shape_value_type(key)
{
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
entry_type,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
let resolved_types =
ResolvedType::from_classes_with_hint(resolved, entry_type.clone());
if !conditional {
results.clear();
}
ResolvedType::extend_unique(results, resolved_types);
return;
}
}
if let Some(element_type) = var_type.extract_value_type(true) {
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
element_type,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
let resolved_types =
ResolvedType::from_classes_with_hint(resolved, element_type.clone());
if !conditional {
results.clear();
}
ResolvedType::extend_unique(results, resolved_types);
return;
}
}
}
let raw_type: Option<PhpType> = resolve_expression_type(assignment.rhs, ctx);
let raw_type = raw_type.map(|rt| {
crate::completion::type_resolution::resolve_type_alias_typed(
&rt,
current_class_name,
all_classes,
class_loader,
)
.unwrap_or(rt)
});
if let Some(ref raw) = raw_type {
if let Some(ref key) = shape_key
&& let Some(entry_type) = raw.shape_value_type(key)
{
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
entry_type,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
let resolved_types =
ResolvedType::from_classes_with_hint(resolved, entry_type.clone());
if !conditional {
results.clear();
}
ResolvedType::extend_unique(results, resolved_types);
return;
}
}
if let Some(element_type) = raw.extract_value_type(true) {
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
element_type,
current_class_name,
all_classes,
class_loader,
);
if !resolved.is_empty() {
let resolved_types =
ResolvedType::from_classes_with_hint(resolved, element_type.clone());
if !conditional {
results.clear();
}
ResolvedType::extend_unique(results, resolved_types);
}
}
}
}
fn extract_destructuring_key(key_expr: &Expression<'_>) -> Option<String> {
match key_expr {
Expression::Literal(Literal::String(lit_str)) => {
lit_str
.value
.map(|v| v.to_string())
.or_else(|| crate::util::unquote_php_string(lit_str.raw).map(|s| s.to_string()))
}
Expression::Literal(Literal::Integer(lit_int)) => Some(lit_int.raw.to_string()),
_ => None,
}
}