use std::cell::Cell;
use std::sync::Arc;
use mago_span::HasSpan;
use mago_syntax::ast::*;
use crate::completion::types::narrowing;
use crate::docblock;
use crate::parser::{extract_hint_type, with_parsed_program};
use crate::php_type::{PhpType, ShapeEntry, is_keyword_type};
use crate::types::{ClassInfo, ParameterInfo, ResolvedType};
use crate::completion::resolver::{Loaders, VarResolutionCtx};
thread_local! {
static VAR_RESOLUTION_DEPTH: Cell<u8> = const { Cell::new(0) };
static VAR_RESOLUTION_DEPTH_LIMITED: Cell<bool> = const { Cell::new(false) };
}
const MAX_VAR_RESOLUTION_DEPTH: u8 = 4;
pub(crate) fn is_var_resolution_depth_limited() -> bool {
VAR_RESOLUTION_DEPTH_LIMITED.with(|f| f.get())
}
pub(in crate::completion) fn build_var_resolver_from_ctx<'a>(
ctx: &'a VarResolutionCtx<'a>,
) -> impl Fn(&str) -> Vec<String> + 'a {
move |var_name: &str| -> Vec<String> {
super::class_string_resolution::resolve_class_string_targets(
var_name,
ctx.current_class,
ctx.all_classes,
ctx.content,
ctx.cursor_offset,
ctx.class_loader,
)
.iter()
.map(|c| c.name.clone())
.collect()
}
}
fn enrich_builder_type_in_scope(
type_hint: &PhpType,
method_name: &str,
has_scope_attr: bool,
current_class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> Option<PhpType> {
use crate::virtual_members::laravel::{ELOQUENT_BUILDER_FQN, extends_eloquent_model};
let is_convention_scope = method_name.starts_with("scope") && method_name.len() > 5;
if !is_convention_scope && !has_scope_attr {
return None;
}
if !extends_eloquent_model(current_class, class_loader) {
return None;
}
if type_hint.has_type_structure() {
return None;
}
let type_name = match type_hint {
PhpType::Named(n) => n.as_str(),
_ => return None,
};
let is_eloquent_builder = type_name == ELOQUENT_BUILDER_FQN || type_name == "Builder";
if !is_eloquent_builder {
return None;
}
Some(PhpType::Generic(
type_name.to_string(),
vec![PhpType::Named(current_class.name.clone())],
))
}
pub(crate) fn resolve_variable_types(
var_name: &str,
current_class: &ClassInfo,
all_classes: &[Arc<ClassInfo>],
content: &str,
cursor_offset: u32,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
loaders: Loaders<'_>,
) -> Vec<ResolvedType> {
let depth = VAR_RESOLUTION_DEPTH.with(|d| {
let cur = d.get();
d.set(cur.saturating_add(1));
cur
});
if depth >= MAX_VAR_RESOLUTION_DEPTH {
VAR_RESOLUTION_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
VAR_RESOLUTION_DEPTH_LIMITED.with(|f| f.set(true));
return vec![];
}
let result = with_parsed_program(content, "resolve_variable_types", |program, _content| {
let ctx = VarResolutionCtx {
var_name,
current_class,
all_classes,
content,
cursor_offset,
class_loader,
loaders,
resolved_class_cache: None,
enclosing_return_type: None,
branch_aware: false,
};
resolve_variable_in_statements(program.statements.iter(), &ctx)
});
let new_depth = VAR_RESOLUTION_DEPTH.with(|d| {
let v = d.get().saturating_sub(1);
d.set(v);
v
});
if new_depth == 0 {
VAR_RESOLUTION_DEPTH_LIMITED.with(|f| f.set(false));
}
result
}
pub(crate) fn resolve_variable_types_branch_aware(
var_name: &str,
current_class: &ClassInfo,
all_classes: &[Arc<ClassInfo>],
content: &str,
cursor_offset: u32,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
loaders: Loaders<'_>,
) -> Vec<ResolvedType> {
let depth = VAR_RESOLUTION_DEPTH.with(|d| {
let cur = d.get();
d.set(cur.saturating_add(1));
cur
});
if depth >= MAX_VAR_RESOLUTION_DEPTH {
VAR_RESOLUTION_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
VAR_RESOLUTION_DEPTH_LIMITED.with(|f| f.set(true));
return vec![];
}
let result = with_parsed_program(
content,
"resolve_variable_types_branch_aware",
|program, _content| {
let ctx = VarResolutionCtx {
var_name,
current_class,
all_classes,
content,
cursor_offset,
class_loader,
loaders,
resolved_class_cache: None,
enclosing_return_type: None,
branch_aware: true,
};
resolve_variable_in_statements(program.statements.iter(), &ctx)
},
);
let new_depth = VAR_RESOLUTION_DEPTH.with(|d| {
let v = d.get().saturating_sub(1);
d.set(v);
v
});
if new_depth == 0 {
VAR_RESOLUTION_DEPTH_LIMITED.with(|f| f.set(false));
}
result
}
pub(in crate::completion) fn resolve_variable_in_statements<'b>(
statements: impl Iterator<Item = &'b Statement<'b>>,
ctx: &VarResolutionCtx<'_>,
) -> Vec<ResolvedType> {
let stmts: Vec<&Statement> = statements.collect();
for &stmt in &stmts {
match stmt {
Statement::Class(class) => {
let start = class.left_brace.start.offset;
let end = class.right_brace.end.offset;
if ctx.cursor_offset < start || ctx.cursor_offset > end {
continue;
}
return resolve_variable_in_members(class.members.iter(), ctx);
}
Statement::Interface(iface) => {
let start = iface.left_brace.start.offset;
let end = iface.right_brace.end.offset;
if ctx.cursor_offset < start || ctx.cursor_offset > end {
continue;
}
return resolve_variable_in_members(iface.members.iter(), ctx);
}
Statement::Enum(enum_def) => {
let start = enum_def.left_brace.start.offset;
let end = enum_def.right_brace.end.offset;
if ctx.cursor_offset < start || ctx.cursor_offset > end {
continue;
}
return resolve_variable_in_members(enum_def.members.iter(), ctx);
}
Statement::Trait(trait_def) => {
let start = trait_def.left_brace.start.offset;
let end = trait_def.right_brace.end.offset;
if ctx.cursor_offset < start || ctx.cursor_offset > end {
continue;
}
return resolve_variable_in_members(trait_def.members.iter(), ctx);
}
Statement::Namespace(ns) => {
let results = resolve_variable_in_statements(ns.statements().iter(), ctx);
if !results.is_empty() {
return results;
}
}
Statement::Function(func) => {
if let Some(results) = try_resolve_in_function(func, ctx) {
return results;
}
}
Statement::If(_) | Statement::Block(_) => {
if let Some(results) = try_resolve_in_nested_function(stmt, ctx) {
return results;
}
}
_ => {}
}
let stmt_span = stmt.span();
if ctx.cursor_offset >= stmt_span.start.offset
&& ctx.cursor_offset <= stmt_span.end.offset
&& let Some(anon) = find_anonymous_class_containing_cursor(stmt, ctx.cursor_offset)
{
return resolve_variable_in_members(anon.members.iter(), ctx);
}
}
let mut results: Vec<ResolvedType> = Vec::new();
walk_statements_for_assignments(stmts.into_iter(), ctx, &mut results, false);
results
}
fn find_anonymous_class_containing_cursor<'a>(
stmt: &'a Statement<'a>,
cursor_offset: u32,
) -> Option<&'a AnonymousClass<'a>> {
fn walk_expr<'a>(expr: &'a Expression<'a>, cursor: u32) -> Option<&'a AnonymousClass<'a>> {
let sp = expr.span();
if cursor < sp.start.offset || cursor > sp.end.offset {
return None;
}
match expr {
Expression::AnonymousClass(anon) => {
if cursor >= anon.left_brace.start.offset && cursor <= anon.right_brace.end.offset {
return Some(anon);
}
None
}
Expression::Parenthesized(p) => walk_expr(p.expression, cursor),
Expression::Assignment(a) => {
walk_expr(a.lhs, cursor).or_else(|| walk_expr(a.rhs, cursor))
}
Expression::Binary(b) => walk_expr(b.lhs, cursor).or_else(|| walk_expr(b.rhs, cursor)),
Expression::Conditional(c) => walk_expr(c.condition, cursor)
.or_else(|| c.then.and_then(|e| walk_expr(e, cursor)))
.or_else(|| walk_expr(c.r#else, cursor)),
Expression::Call(call) => match call {
Call::Function(fc) => walk_args(&fc.argument_list.arguments, cursor),
Call::Method(mc) => walk_expr(mc.object, cursor)
.or_else(|| walk_args(&mc.argument_list.arguments, cursor)),
Call::NullSafeMethod(mc) => walk_expr(mc.object, cursor)
.or_else(|| walk_args(&mc.argument_list.arguments, cursor)),
Call::StaticMethod(sc) => walk_expr(sc.class, cursor)
.or_else(|| walk_args(&sc.argument_list.arguments, cursor)),
},
Expression::Array(arr) => {
for elem in arr.elements.iter() {
let found = match elem {
ArrayElement::KeyValue(kv) => {
walk_expr(kv.key, cursor).or_else(|| walk_expr(kv.value, cursor))
}
ArrayElement::Value(v) => walk_expr(v.value, cursor),
ArrayElement::Variadic(v) => walk_expr(v.value, cursor),
_ => None,
};
if found.is_some() {
return found;
}
}
None
}
Expression::LegacyArray(arr) => {
for elem in arr.elements.iter() {
let found = match elem {
ArrayElement::KeyValue(kv) => {
walk_expr(kv.key, cursor).or_else(|| walk_expr(kv.value, cursor))
}
ArrayElement::Value(v) => walk_expr(v.value, cursor),
ArrayElement::Variadic(v) => walk_expr(v.value, cursor),
_ => None,
};
if found.is_some() {
return found;
}
}
None
}
Expression::Closure(closure) => {
for inner in closure.body.statements.iter() {
if let Some(anon) = find_anonymous_class_containing_cursor(inner, cursor) {
return Some(anon);
}
}
None
}
Expression::ArrowFunction(arrow) => walk_expr(arrow.expression, cursor),
Expression::Instantiation(inst) => {
if let Some(ref args) = inst.argument_list {
walk_args(&args.arguments, cursor)
} else {
None
}
}
Expression::UnaryPrefix(u) => walk_expr(u.operand, cursor),
Expression::UnaryPostfix(u) => walk_expr(u.operand, cursor),
Expression::Throw(t) => walk_expr(t.exception, cursor),
Expression::Clone(c) => walk_expr(c.object, cursor),
Expression::Match(m) => {
if let Some(found) = walk_expr(m.expression, cursor) {
return Some(found);
}
for arm in m.arms.iter() {
if let Some(found) = walk_expr(arm.expression(), cursor) {
return Some(found);
}
}
None
}
_ => None,
}
}
fn walk_args<'a>(
arguments: &'a mago_syntax::ast::sequence::TokenSeparatedSequence<'a, Argument<'a>>,
cursor: u32,
) -> Option<&'a AnonymousClass<'a>> {
for arg in arguments.iter() {
let arg_expr = match arg {
Argument::Positional(pos) => pos.value,
Argument::Named(named) => named.value,
};
if let Some(found) = walk_expr(arg_expr, cursor) {
return Some(found);
}
}
None
}
match stmt {
Statement::Expression(expr_stmt) => walk_expr(expr_stmt.expression, cursor_offset),
Statement::Return(ret) => ret.value.as_ref().and_then(|v| walk_expr(v, cursor_offset)),
Statement::Block(block) => {
for inner in block.statements.iter() {
if let Some(anon) = find_anonymous_class_containing_cursor(inner, cursor_offset) {
return Some(anon);
}
}
None
}
Statement::If(if_stmt) => match &if_stmt.body {
IfBody::Statement(body) => {
find_anonymous_class_containing_cursor(body.statement, cursor_offset)
}
IfBody::ColonDelimited(body) => {
for inner in body.statements.iter() {
if let Some(anon) = find_anonymous_class_containing_cursor(inner, cursor_offset)
{
return Some(anon);
}
}
None
}
},
Statement::Foreach(foreach) => match &foreach.body {
ForeachBody::Statement(inner) => {
find_anonymous_class_containing_cursor(inner, cursor_offset)
}
ForeachBody::ColonDelimited(body) => {
for inner in body.statements.iter() {
if let Some(anon) = find_anonymous_class_containing_cursor(inner, cursor_offset)
{
return Some(anon);
}
}
None
}
},
Statement::While(while_stmt) => match &while_stmt.body {
WhileBody::Statement(inner) => {
find_anonymous_class_containing_cursor(inner, cursor_offset)
}
WhileBody::ColonDelimited(body) => {
for inner in body.statements.iter() {
if let Some(anon) = find_anonymous_class_containing_cursor(inner, cursor_offset)
{
return Some(anon);
}
}
None
}
},
Statement::For(for_stmt) => match &for_stmt.body {
ForBody::Statement(inner) => {
find_anonymous_class_containing_cursor(inner, cursor_offset)
}
ForBody::ColonDelimited(body) => {
for inner in body.statements.iter() {
if let Some(anon) = find_anonymous_class_containing_cursor(inner, cursor_offset)
{
return Some(anon);
}
}
None
}
},
Statement::DoWhile(dw) => {
find_anonymous_class_containing_cursor(dw.statement, cursor_offset)
}
Statement::Try(try_stmt) => {
for inner in try_stmt.block.statements.iter() {
if let Some(anon) = find_anonymous_class_containing_cursor(inner, cursor_offset) {
return Some(anon);
}
}
for catch in try_stmt.catch_clauses.iter() {
for inner in catch.block.statements.iter() {
if let Some(anon) = find_anonymous_class_containing_cursor(inner, cursor_offset)
{
return Some(anon);
}
}
}
if let Some(finally) = &try_stmt.finally_clause {
for inner in finally.block.statements.iter() {
if let Some(anon) = find_anonymous_class_containing_cursor(inner, cursor_offset)
{
return Some(anon);
}
}
}
None
}
_ => None,
}
}
fn try_resolve_in_function(
func: &Function<'_>,
ctx: &VarResolutionCtx<'_>,
) -> Option<Vec<ResolvedType>> {
let body_start = func.body.left_brace.start.offset;
let body_end = func.body.right_brace.end.offset;
if ctx.cursor_offset < body_start || ctx.cursor_offset > body_end {
return None;
}
let enclosing_ret =
crate::docblock::find_enclosing_return_type(ctx.content, (body_start + 1) as usize);
let body_ctx = ctx.with_enclosing_return_type(enclosing_ret);
let mut results: Vec<ResolvedType> = Vec::new();
super::closure_resolution::resolve_closure_params(
&func.parameter_list,
&body_ctx,
&mut results,
);
let func_start = func.span().start.offset as usize;
for rt in results.iter_mut() {
if matches!(&rt.type_string, PhpType::ClassString(Some(inner)) if matches!(inner.as_ref(), PhpType::Named(_)))
{
rt.type_string = substitute_class_string_template_bounds(
rt.type_string.clone(),
ctx.content,
func_start,
);
}
rt.type_string =
substitute_template_param_bounds(rt.type_string.clone(), ctx.content, func_start);
}
walk_statements_for_assignments(func.body.statements.iter(), &body_ctx, &mut results, false);
if !results.is_empty() {
return Some(results);
}
super::closure_resolution::try_standalone_var_docblock(&body_ctx, &mut results);
if !results.is_empty() {
return Some(results);
}
if let Some(ref ret_type) = body_ctx.enclosing_return_type {
let yield_results =
super::raw_type_inference::try_infer_from_generator_yield(ret_type, &body_ctx);
if !yield_results.is_empty() {
return Some(ResolvedType::from_classes(yield_results));
}
}
Some(results)
}
fn try_resolve_in_nested_function(
stmt: &Statement<'_>,
ctx: &VarResolutionCtx<'_>,
) -> Option<Vec<ResolvedType>> {
let span = stmt.span();
if ctx.cursor_offset < span.start.offset || ctx.cursor_offset > span.end.offset {
return None;
}
match stmt {
Statement::Function(func) => try_resolve_in_function(func, ctx),
Statement::Block(block) => {
for inner in block.statements.iter() {
if let Some(results) = try_resolve_in_nested_function(inner, ctx) {
return Some(results);
}
}
None
}
Statement::If(if_stmt) => {
match &if_stmt.body {
IfBody::Statement(body) => {
if let Some(results) = try_resolve_in_nested_function(body.statement, ctx) {
return Some(results);
}
for else_if in body.else_if_clauses.iter() {
if let Some(results) =
try_resolve_in_nested_function(else_if.statement, ctx)
{
return Some(results);
}
}
if let Some(else_clause) = &body.else_clause
&& let Some(results) =
try_resolve_in_nested_function(else_clause.statement, ctx)
{
return Some(results);
}
}
IfBody::ColonDelimited(body) => {
for inner in body.statements.iter() {
if let Some(results) = try_resolve_in_nested_function(inner, ctx) {
return Some(results);
}
}
for else_if in body.else_if_clauses.iter() {
for inner in else_if.statements.iter() {
if let Some(results) = try_resolve_in_nested_function(inner, ctx) {
return Some(results);
}
}
}
if let Some(else_clause) = &body.else_clause {
for inner in else_clause.statements.iter() {
if let Some(results) = try_resolve_in_nested_function(inner, ctx) {
return Some(results);
}
}
}
}
}
None
}
_ => None,
}
}
fn resolve_variable_in_members<'b>(
members: impl Iterator<Item = &'b ClassLikeMember<'b>>,
ctx: &VarResolutionCtx<'_>,
) -> Vec<ResolvedType> {
for member in members {
if let ClassLikeMember::Method(method) = member {
let mut param_results: Vec<ResolvedType> = Vec::new();
let mut matched_param_is_variadic = false;
for param in method.parameter_list.parameters.iter() {
let pname = param.variable.name.to_string();
if pname == ctx.var_name {
matched_param_is_variadic = param.ellipsis.is_some();
let native_type = param.hint.as_ref().map(|h| extract_hint_type(h));
let enriched_type = native_type.as_ref().and_then(|nt| {
let method_name = method.name.value.to_string();
let has_scope_attr = method.attribute_lists.iter().any(|al| {
al.attributes
.iter()
.any(|a| a.name.last_segment() == "Scope")
});
enrich_builder_type_in_scope(
nt,
&method_name,
has_scope_attr,
ctx.current_class,
ctx.class_loader,
)
});
let type_for_resolution: Option<&PhpType> =
enriched_type.as_ref().or(native_type.as_ref());
let method_start = method.span().start.offset as usize;
let raw_docblock_type = crate::docblock::find_iterable_raw_type_in_source(
ctx.content,
method_start,
ctx.var_name,
);
let native_parsed = if let Some(ref enriched) = enriched_type {
Some(enriched.clone())
} else {
native_type.clone()
};
let doc_parsed = raw_docblock_type.clone();
let effective_type = crate::docblock::resolve_effective_type_typed(
native_parsed.as_ref(),
doc_parsed.as_ref(),
);
let effective_type = effective_type.map(|ty| {
substitute_template_param_bounds(
ty,
ctx.content,
method.span().start.offset as usize,
)
});
let resolved_from_effective = effective_type
.as_ref()
.map(|ty| {
crate::completion::type_resolution::type_hint_to_classes_typed(
ty,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
)
})
.unwrap_or_default();
if !resolved_from_effective.is_empty() {
param_results = ResolvedType::from_classes_with_hint(
resolved_from_effective,
effective_type.unwrap_or_else(|| {
type_for_resolution
.cloned()
.unwrap_or_else(PhpType::untyped)
}),
);
break;
}
if let Some(ref raw_docblock_type) = raw_docblock_type {
let parsed_docblock = raw_docblock_type.clone();
let resolved =
crate::completion::type_resolution::type_hint_to_classes_typed(
&parsed_docblock,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
);
if !resolved.is_empty() {
param_results =
ResolvedType::from_classes_with_hint(resolved, parsed_docblock);
break;
}
}
let mut merged_type_hint: Option<PhpType> = None;
let method_name = method.name.value.to_string();
let merged = crate::virtual_members::resolve_class_fully_maybe_cached(
ctx.current_class,
ctx.class_loader,
ctx.resolved_class_cache,
);
if let Some(merged_method) =
merged.methods.iter().find(|m| m.name == method_name)
{
if let Some(merged_param) = merged_method
.parameters
.iter()
.find(|p| p.name == ctx.var_name)
&& let Some(ref hint) = merged_param.type_hint
{
let resolved =
crate::completion::type_resolution::type_hint_to_classes_typed(
hint,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
);
if !resolved.is_empty() {
param_results =
ResolvedType::from_classes_with_hint(resolved, hint.clone());
break;
}
merged_type_hint = Some(hint.clone());
}
}
let best_type = if let Some(ref rdt) = raw_docblock_type {
Some(rdt.clone())
} else if let Some(ref mth) = merged_type_hint {
Some(mth.clone())
} else {
type_for_resolution.cloned()
};
if let Some(mut parsed) = best_type {
parsed = substitute_class_string_template_bounds(
parsed,
ctx.content,
method.span().start.offset as usize,
);
param_results = vec![ResolvedType::from_type_string(parsed)];
}
}
}
if matched_param_is_variadic && !param_results.is_empty() {
for rt in &mut param_results {
rt.type_string = PhpType::list(rt.type_string.clone());
rt.class_info = None;
}
}
if let MethodBody::Concrete(block) = &method.body {
let blk_start = block.left_brace.start.offset;
let blk_end = block.right_brace.end.offset;
if ctx.cursor_offset >= blk_start && ctx.cursor_offset <= blk_end {
let enclosing_ret = crate::docblock::find_enclosing_return_type(
ctx.content,
(blk_start + 1) as usize,
);
let body_ctx = ctx.with_enclosing_return_type(enclosing_ret);
let mut results = param_results.clone();
walk_statements_for_assignments(
block.statements.iter(),
&body_ctx,
&mut results,
false,
);
if !results.is_empty() {
return results;
}
super::closure_resolution::try_standalone_var_docblock(&body_ctx, &mut results);
if !results.is_empty() {
return results;
}
if let Some(ref ret_type) = body_ctx.enclosing_return_type {
let yield_results =
super::raw_type_inference::try_infer_from_generator_yield(
ret_type, &body_ctx,
);
if !yield_results.is_empty() {
return ResolvedType::from_classes(yield_results);
}
}
return vec![];
} else {
continue;
}
}
let method_start = method.span().start.offset;
let method_end = method.span().end.offset;
if !param_results.is_empty()
&& ctx.cursor_offset >= method_start
&& ctx.cursor_offset <= method_end
{
return param_results;
}
}
}
vec![]
}
fn substitute_template_param_bounds(
ty: PhpType,
content: &str,
method_start_offset: usize,
) -> PhpType {
if !type_may_contain_template_param(&ty) {
return ty;
}
let before = &content[..method_start_offset];
let docblock = extract_preceding_docblock(before);
let Some(docblock) = docblock else {
return ty;
};
let bounds = docblock::extract_template_params_with_bounds(docblock);
if bounds.is_empty() {
return ty;
}
let mut subs = std::collections::HashMap::new();
for (name, bound) in bounds {
if let Some(bound_type) = bound {
subs.insert(name, bound_type);
}
}
if subs.is_empty() {
return ty;
}
ty.substitute(&subs)
}
fn type_may_contain_template_param(ty: &PhpType) -> bool {
match ty {
PhpType::Named(name) => {
!is_keyword_type(name)
}
PhpType::Union(members) | PhpType::Intersection(members) => {
members.iter().any(type_may_contain_template_param)
}
PhpType::Nullable(inner) => type_may_contain_template_param(inner),
PhpType::Generic(base, args) => {
!crate::php_type::is_keyword_type(base)
|| args.iter().any(type_may_contain_template_param)
}
_ => false,
}
}
fn substitute_class_string_template_bounds(
ty: PhpType,
content: &str,
method_start_offset: usize,
) -> PhpType {
let inner_name = match &ty {
PhpType::ClassString(Some(inner)) => match inner.as_ref() {
PhpType::Named(name) => Some(name.clone()),
_ => None,
},
_ => None,
};
let Some(tpl_name) = inner_name else {
return ty;
};
let before = &content[..method_start_offset];
let docblock = extract_preceding_docblock(before);
let Some(docblock) = docblock else {
return ty;
};
let bounds = docblock::extract_template_params_with_bounds(docblock);
for (name, bound) in bounds {
if name == tpl_name
&& let Some(bound_type) = bound
{
return PhpType::ClassString(Some(Box::new(bound_type)));
}
}
ty
}
fn extract_preceding_docblock(before: &str) -> Option<&str> {
let trimmed = before.trim_end();
if !trimmed.ends_with("*/") {
return None;
}
let close_pos = trimmed.len();
let open_pos = trimmed.rfind("/**")?;
Some(&trimmed[open_pos..close_pos])
}
pub(in crate::completion) fn walk_statements_for_assignments<'b>(
statements: impl Iterator<Item = &'b Statement<'b>>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
conditional: bool,
) {
fn result_names(results: &[ResolvedType]) -> Vec<String> {
let mut names: Vec<String> = results
.iter()
.filter_map(|rt| rt.class_info.as_ref().map(|c| c.name.clone()))
.collect();
names.sort();
names
}
let mut assert_narrowed_types: Vec<ResolvedType> = Vec::new();
for stmt in statements {
let stmt_span = stmt.span();
if ctx.cursor_offset >= stmt_span.start.offset
&& ctx.cursor_offset <= stmt_span.end.offset
&& super::closure_resolution::try_resolve_in_closure_stmt(stmt, ctx, results)
{
return;
}
if stmt.span().start.offset >= ctx.cursor_offset {
continue;
}
match stmt {
Statement::Expression(expr_stmt) => {
let pre_assign_names = result_names(results);
if !try_inline_var_override(
expr_stmt.expression,
stmt.span().start.offset as usize,
ctx,
results,
conditional,
) {
check_expression_for_assignment(
expr_stmt.expression,
ctx,
results,
conditional,
);
}
if result_names(results) != pre_assign_names {
assert_narrowed_types.clear();
}
try_apply_pass_by_reference_type(expr_stmt.expression, ctx, results, conditional);
let pre_assert_names = result_names(results);
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_instanceof_narrowing(
expr_stmt.expression,
ctx,
classes,
);
});
let changed = result_names(results) != pre_assert_names;
if changed && !assert_narrowed_types.is_empty() {
for rt in &assert_narrowed_types {
if !results
.iter()
.any(|existing| existing.type_string == rt.type_string)
{
results.push(rt.clone());
}
}
}
if changed {
assert_narrowed_types.clone_from(results);
}
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_custom_assert_narrowing(
expr_stmt.expression,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_match_true_narrowing(expr_stmt.expression, ctx, classes);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_ternary_instanceof_narrowing(
expr_stmt.expression,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_inline_and_narrowing(expr_stmt.expression, ctx, classes);
});
narrowing::try_apply_inline_and_null_narrowing(expr_stmt.expression, ctx, results);
}
Statement::Return(ret) => {
if let Some(val) = ret.value {
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_match_true_narrowing(val, ctx, classes);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_ternary_instanceof_narrowing(val, ctx, classes);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_inline_and_narrowing(val, ctx, classes);
});
narrowing::try_apply_inline_and_null_narrowing(val, ctx, results);
}
}
Statement::Block(block) => {
walk_statements_for_assignments(block.statements.iter(), ctx, results, conditional);
}
Statement::If(if_stmt) => {
walk_if_statement(if_stmt, stmt, ctx, results);
}
Statement::Foreach(foreach) => {
walk_foreach_statement(foreach, ctx, results, conditional);
}
Statement::While(while_stmt) => {
walk_while_statement(while_stmt, ctx, results);
}
Statement::For(for_stmt) => {
let for_body_span = for_stmt.body.span();
let for_header_start = for_stmt.r#for.span().start.offset;
let cursor_inside_for = ctx.cursor_offset >= for_header_start
&& ctx.cursor_offset <= for_body_span.end.offset;
if cursor_inside_for {
match &for_stmt.body {
ForBody::Statement(inner) => {
prescan_loop_body_for_assignments(
std::iter::once(*inner),
for_body_span.end.offset,
ctx,
results,
);
}
ForBody::ColonDelimited(body) => {
prescan_loop_body_for_assignments(
body.statements.iter(),
for_body_span.end.offset,
ctx,
results,
);
}
}
}
match &for_stmt.body {
ForBody::Statement(inner) => {
check_statement_for_assignments(inner, ctx, results, true);
}
ForBody::ColonDelimited(body) => {
walk_statements_for_assignments(body.statements.iter(), ctx, results, true);
}
}
}
Statement::DoWhile(dw) => {
let dw_body_span = dw.statement.span();
let dw_header_start = dw.r#do.span().start.offset;
let cursor_inside_dw = ctx.cursor_offset >= dw_header_start
&& ctx.cursor_offset <= dw_body_span.end.offset;
if cursor_inside_dw {
prescan_loop_body_for_assignments(
std::iter::once(dw.statement),
dw_body_span.end.offset,
ctx,
results,
);
}
check_statement_for_assignments(dw.statement, ctx, results, true);
}
Statement::Try(try_stmt) => {
walk_try_statement(try_stmt, ctx, results);
}
Statement::Switch(switch) => {
for case in switch.body.cases().iter() {
walk_statements_for_assignments(case.statements().iter(), ctx, results, true);
}
}
Statement::Unset(unset_stmt) => {
if !conditional {
for val in unset_stmt.values.iter() {
if let Expression::Variable(Variable::Direct(dv)) = val
&& dv.name == ctx.var_name
{
results.clear();
}
}
}
}
Statement::Namespace(ns) => {
walk_statements_for_assignments(ns.statements().iter(), ctx, results, conditional);
}
_ => {}
}
}
}
fn walk_if_statement<'b>(
if_stmt: &'b If<'b>,
enclosing_stmt: &'b Statement<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
) {
if ctx.branch_aware
&& let Some(()) = walk_if_branch_aware(if_stmt, enclosing_stmt, ctx, results)
{
return;
}
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_inline_and_narrowing(if_stmt.condition, ctx, classes);
});
narrowing::try_apply_inline_and_null_narrowing(if_stmt.condition, ctx, results);
check_condition_for_assignment(if_stmt.condition, ctx, results);
match &if_stmt.body {
IfBody::Statement(body) => {
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing(
if_stmt.condition,
body.statement.span(),
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
if_stmt.condition,
body.statement.span(),
ctx,
classes,
false, );
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing(
if_stmt.condition,
body.statement.span(),
ctx,
classes,
);
});
narrowing::try_apply_if_body_null_narrowing(
if_stmt.condition,
body.statement.span(),
ctx,
results,
);
narrowing::try_apply_type_guard_narrowing(
if_stmt.condition,
body.statement.span(),
ctx,
results,
);
check_statement_for_assignments(body.statement, ctx, results, true);
for else_if in body.else_if_clauses.iter() {
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_inline_and_narrowing(else_if.condition, ctx, classes);
});
narrowing::try_apply_inline_and_null_narrowing(else_if.condition, ctx, results);
check_condition_for_assignment(else_if.condition, ctx, results);
narrowing::try_apply_if_body_null_narrowing(
else_if.condition,
else_if.statement.span(),
ctx,
results,
);
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing(
else_if.condition,
else_if.statement.span(),
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
else_if.condition,
else_if.statement.span(),
ctx,
classes,
false,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing(
else_if.condition,
else_if.statement.span(),
ctx,
classes,
);
});
narrowing::try_apply_type_guard_narrowing(
else_if.condition,
else_if.statement.span(),
ctx,
results,
);
check_statement_for_assignments(else_if.statement, ctx, results, true);
}
if let Some(else_clause) = &body.else_clause {
let else_span = else_clause.statement.span();
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
if_stmt.condition,
else_span,
ctx,
classes,
true, );
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
classes,
);
});
narrowing::try_apply_if_body_null_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
results,
);
narrowing::try_apply_type_guard_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
results,
);
for else_if in body.else_if_clauses.iter() {
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing_inverse(
else_if.condition,
else_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
else_if.condition,
else_span,
ctx,
classes,
true,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing_inverse(
else_if.condition,
else_span,
ctx,
classes,
);
});
narrowing::try_apply_type_guard_narrowing_inverse(
else_if.condition,
else_span,
ctx,
results,
);
}
check_statement_for_assignments(else_clause.statement, ctx, results, true);
}
}
IfBody::ColonDelimited(body) => {
let then_end = if !body.else_if_clauses.is_empty() {
body.else_if_clauses
.first()
.unwrap()
.elseif
.span()
.start
.offset
} else if let Some(ref ec) = body.else_clause {
ec.r#else.span().start.offset
} else {
body.endif.span().start.offset
};
let then_span = mago_span::Span::new(
body.colon.file_id,
body.colon.start,
mago_span::Position::new(then_end),
);
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing(
if_stmt.condition,
then_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
if_stmt.condition,
then_span,
ctx,
classes,
false,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing(if_stmt.condition, then_span, ctx, classes);
});
narrowing::try_apply_if_body_null_narrowing(if_stmt.condition, then_span, ctx, results);
narrowing::try_apply_type_guard_narrowing(if_stmt.condition, then_span, ctx, results);
walk_statements_for_assignments(body.statements.iter(), ctx, results, true);
for else_if in body.else_if_clauses.iter() {
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_inline_and_narrowing(else_if.condition, ctx, classes);
});
narrowing::try_apply_inline_and_null_narrowing(else_if.condition, ctx, results);
check_condition_for_assignment(else_if.condition, ctx, results);
let ei_span = mago_span::Span::new(
else_if.colon.file_id,
else_if.colon.start,
mago_span::Position::new(
else_if
.statements
.span(else_if.colon.file_id, else_if.colon.end)
.end
.offset,
),
);
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing(
else_if.condition,
ei_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
else_if.condition,
ei_span,
ctx,
classes,
false,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing(
else_if.condition,
ei_span,
ctx,
classes,
);
});
narrowing::try_apply_if_body_null_narrowing(
else_if.condition,
ei_span,
ctx,
results,
);
narrowing::try_apply_type_guard_narrowing(else_if.condition, ei_span, ctx, results);
walk_statements_for_assignments(else_if.statements.iter(), ctx, results, true);
}
if let Some(else_clause) = &body.else_clause {
let else_span = mago_span::Span::new(
else_clause.colon.file_id,
else_clause.colon.start,
mago_span::Position::new(
else_clause
.statements
.span(else_clause.colon.file_id, else_clause.colon.end)
.end
.offset,
),
);
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
if_stmt.condition,
else_span,
ctx,
classes,
true, );
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
classes,
);
});
narrowing::try_apply_if_body_null_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
results,
);
narrowing::try_apply_type_guard_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
results,
);
for else_if in body.else_if_clauses.iter() {
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing_inverse(
else_if.condition,
else_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
else_if.condition,
else_span,
ctx,
classes,
true,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing_inverse(
else_if.condition,
else_span,
ctx,
classes,
);
});
narrowing::try_apply_type_guard_narrowing_inverse(
else_if.condition,
else_span,
ctx,
results,
);
}
walk_statements_for_assignments(else_clause.statements.iter(), ctx, results, true);
}
}
}
if enclosing_stmt.span().end.offset < ctx.cursor_offset {
ResolvedType::apply_narrowing(results, |classes| {
narrowing::apply_guard_clause_narrowing(if_stmt, ctx, classes);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::apply_guard_clause_in_array_narrowing(if_stmt, ctx, classes);
});
narrowing::apply_guard_clause_null_narrowing(if_stmt, ctx, results);
narrowing::apply_guard_clause_type_guard_narrowing(if_stmt, ctx, results);
}
}
fn walk_if_branch_aware<'b>(
if_stmt: &'b If<'b>,
_enclosing_stmt: &'b Statement<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
) -> Option<()> {
match &if_stmt.body {
IfBody::Statement(body) => {
let then_span = body.statement.span();
if ctx.cursor_offset >= then_span.start.offset
&& ctx.cursor_offset <= then_span.end.offset
{
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing(
if_stmt.condition,
then_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
if_stmt.condition,
then_span,
ctx,
classes,
false,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing(
if_stmt.condition,
then_span,
ctx,
classes,
);
});
check_condition_for_assignment(if_stmt.condition, ctx, results);
narrowing::try_apply_if_body_null_narrowing(
if_stmt.condition,
then_span,
ctx,
results,
);
narrowing::try_apply_type_guard_narrowing(
if_stmt.condition,
then_span,
ctx,
results,
);
check_statement_for_assignments(body.statement, ctx, results, false);
return Some(());
}
for else_if in body.else_if_clauses.iter() {
let ei_span = else_if.statement.span();
if ctx.cursor_offset >= ei_span.start.offset
&& ctx.cursor_offset <= ei_span.end.offset
{
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing(
else_if.condition,
ei_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
else_if.condition,
ei_span,
ctx,
classes,
false,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing(
else_if.condition,
ei_span,
ctx,
classes,
);
});
narrowing::try_apply_if_body_null_narrowing(
else_if.condition,
ei_span,
ctx,
results,
);
narrowing::try_apply_type_guard_narrowing(
else_if.condition,
ei_span,
ctx,
results,
);
check_condition_for_assignment(else_if.condition, ctx, results);
narrowing::try_apply_if_body_null_narrowing(
else_if.condition,
ei_span,
ctx,
results,
);
check_statement_for_assignments(else_if.statement, ctx, results, false);
return Some(());
}
}
if let Some(else_clause) = &body.else_clause {
let el_span = else_clause.statement.span();
if ctx.cursor_offset >= el_span.start.offset
&& ctx.cursor_offset <= el_span.end.offset
{
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing_inverse(
if_stmt.condition,
el_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
if_stmt.condition,
el_span,
ctx,
classes,
true,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing_inverse(
if_stmt.condition,
el_span,
ctx,
classes,
);
});
narrowing::try_apply_if_body_null_narrowing_inverse(
if_stmt.condition,
el_span,
ctx,
results,
);
narrowing::try_apply_type_guard_narrowing_inverse(
if_stmt.condition,
el_span,
ctx,
results,
);
for else_if in body.else_if_clauses.iter() {
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing_inverse(
else_if.condition,
el_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
else_if.condition,
el_span,
ctx,
classes,
true,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing_inverse(
else_if.condition,
el_span,
ctx,
classes,
);
});
narrowing::try_apply_type_guard_narrowing_inverse(
else_if.condition,
el_span,
ctx,
results,
);
}
check_statement_for_assignments(else_clause.statement, ctx, results, false);
return Some(());
}
}
}
IfBody::ColonDelimited(body) => {
let then_start = body.colon.start.offset;
let then_end = body
.else_if_clauses
.first()
.map(|ei| ei.elseif.span().start.offset)
.or_else(|| {
body.else_clause
.as_ref()
.map(|ec| ec.r#else.span().start.offset)
})
.unwrap_or(body.endif.span().start.offset);
if ctx.cursor_offset >= then_start && ctx.cursor_offset < then_end {
let then_span = mago_span::Span::new(
body.colon.file_id,
body.colon.start,
mago_span::Position::new(then_end),
);
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing(
if_stmt.condition,
then_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
if_stmt.condition,
then_span,
ctx,
classes,
false,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing(
if_stmt.condition,
then_span,
ctx,
classes,
);
});
check_condition_for_assignment(if_stmt.condition, ctx, results);
narrowing::try_apply_if_body_null_narrowing(
if_stmt.condition,
then_span,
ctx,
results,
);
narrowing::try_apply_type_guard_narrowing(
if_stmt.condition,
then_span,
ctx,
results,
);
walk_statements_for_assignments(body.statements.iter(), ctx, results, false);
return Some(());
}
for (i, else_if) in body.else_if_clauses.iter().enumerate() {
let ei_start = else_if.colon.start.offset;
let ei_end = body
.else_if_clauses
.get(i + 1)
.map(|next| next.elseif.span().start.offset)
.or_else(|| {
body.else_clause
.as_ref()
.map(|ec| ec.r#else.span().start.offset)
})
.unwrap_or(body.endif.span().start.offset);
if ctx.cursor_offset >= ei_start && ctx.cursor_offset < ei_end {
let ei_span = mago_span::Span::new(
else_if.colon.file_id,
else_if.colon.start,
mago_span::Position::new(ei_end),
);
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing(
else_if.condition,
ei_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
else_if.condition,
ei_span,
ctx,
classes,
false,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing(
else_if.condition,
ei_span,
ctx,
classes,
);
});
narrowing::try_apply_if_body_null_narrowing(
else_if.condition,
ei_span,
ctx,
results,
);
narrowing::try_apply_type_guard_narrowing(
else_if.condition,
ei_span,
ctx,
results,
);
check_condition_for_assignment(else_if.condition, ctx, results);
narrowing::try_apply_if_body_null_narrowing(
else_if.condition,
ei_span,
ctx,
results,
);
walk_statements_for_assignments(else_if.statements.iter(), ctx, results, false);
return Some(());
}
}
if let Some(ref else_clause) = body.else_clause {
let el_start = else_clause.colon.start.offset;
let el_end = body.endif.span().start.offset;
if ctx.cursor_offset >= el_start && ctx.cursor_offset < el_end {
let else_span = mago_span::Span::new(
else_clause.colon.file_id,
else_clause.colon.start,
mago_span::Position::new(el_end),
);
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
if_stmt.condition,
else_span,
ctx,
classes,
true,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
classes,
);
});
narrowing::try_apply_if_body_null_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
results,
);
narrowing::try_apply_type_guard_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
results,
);
for else_if in body.else_if_clauses.iter() {
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing_inverse(
else_if.condition,
else_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
else_if.condition,
else_span,
ctx,
classes,
true,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing_inverse(
else_if.condition,
else_span,
ctx,
classes,
);
});
narrowing::try_apply_type_guard_narrowing_inverse(
else_if.condition,
else_span,
ctx,
results,
);
}
walk_statements_for_assignments(
else_clause.statements.iter(),
ctx,
results,
false,
);
return Some(());
}
}
}
}
None
}
fn prescan_loop_body_for_assignments<'b>(
statements: impl Iterator<Item = &'b Statement<'b>>,
body_end: u32,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
) {
if ctx.cursor_offset >= body_end {
return;
}
let prescan_ctx = ctx.with_cursor_offset(body_end);
let mut prescan_results: Vec<ResolvedType> = Vec::new();
walk_statements_for_assignments(statements, &prescan_ctx, &mut prescan_results, true);
ResolvedType::extend_unique(results, prescan_results);
}
fn walk_foreach_statement<'b>(
foreach: &'b Foreach<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
conditional: bool,
) {
let body_span = foreach.body.span();
let header_start = foreach.foreach.span().start.offset;
let cursor_inside =
ctx.cursor_offset >= header_start && ctx.cursor_offset <= body_span.end.offset;
if cursor_inside {
super::foreach_resolution::try_resolve_foreach_value_type(
foreach,
ctx,
results,
conditional,
);
super::foreach_resolution::try_resolve_foreach_key_type(foreach, ctx, results, conditional);
}
if cursor_inside {
match &foreach.body {
ForeachBody::Statement(inner) => {
prescan_loop_body_for_assignments(
std::iter::once(*inner),
body_span.end.offset,
ctx,
results,
);
}
ForeachBody::ColonDelimited(body) => {
prescan_loop_body_for_assignments(
body.statements.iter(),
body_span.end.offset,
ctx,
results,
);
}
}
}
match &foreach.body {
ForeachBody::Statement(inner) => {
check_statement_for_assignments(inner, ctx, results, true);
}
ForeachBody::ColonDelimited(body) => {
walk_statements_for_assignments(body.statements.iter(), ctx, results, true);
}
}
}
fn walk_while_statement<'b>(
while_stmt: &'b While<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
) {
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_inline_and_narrowing(while_stmt.condition, ctx, classes);
});
narrowing::try_apply_inline_and_null_narrowing(while_stmt.condition, ctx, results);
check_condition_for_assignment(while_stmt.condition, ctx, results);
let while_body_span = while_stmt.body.span();
let while_header_start = while_stmt.r#while.span().start.offset;
let cursor_inside_while =
ctx.cursor_offset >= while_header_start && ctx.cursor_offset <= while_body_span.end.offset;
if cursor_inside_while {
match &while_stmt.body {
WhileBody::Statement(inner) => {
prescan_loop_body_for_assignments(
std::iter::once(*inner),
while_body_span.end.offset,
ctx,
results,
);
}
WhileBody::ColonDelimited(body) => {
prescan_loop_body_for_assignments(
body.statements.iter(),
while_body_span.end.offset,
ctx,
results,
);
}
}
}
match &while_stmt.body {
WhileBody::Statement(inner) => {
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing(
while_stmt.condition,
inner.span(),
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
while_stmt.condition,
inner.span(),
ctx,
classes,
false,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing(
while_stmt.condition,
inner.span(),
ctx,
classes,
);
});
narrowing::try_apply_if_body_null_narrowing(
while_stmt.condition,
inner.span(),
ctx,
results,
);
check_statement_for_assignments(inner, ctx, results, true);
}
WhileBody::ColonDelimited(body) => {
let body_span = mago_span::Span::new(
body.colon.file_id,
body.colon.start,
mago_span::Position::new(body.end_while.span().start.offset),
);
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_instanceof_narrowing(
while_stmt.condition,
body_span,
ctx,
classes,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_assert_condition_narrowing(
while_stmt.condition,
body_span,
ctx,
classes,
false,
);
});
ResolvedType::apply_narrowing(results, |classes| {
narrowing::try_apply_in_array_narrowing(
while_stmt.condition,
body_span,
ctx,
classes,
);
});
narrowing::try_apply_if_body_null_narrowing(
while_stmt.condition,
body_span,
ctx,
results,
);
walk_statements_for_assignments(body.statements.iter(), ctx, results, true);
}
}
}
fn walk_try_statement<'b>(
try_stmt: &'b Try<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
) {
let try_span = try_stmt.block.span();
let cursor_in_try =
ctx.cursor_offset >= try_span.start.offset && ctx.cursor_offset <= try_span.end.offset;
walk_statements_for_assignments(
try_stmt.block.statements.iter(),
ctx,
results,
!cursor_in_try,
);
for catch in try_stmt.catch_clauses.iter() {
if let Some(ref var) = catch.variable
&& var.name == ctx.var_name
{
let parsed_hint = extract_hint_type(&catch.hint);
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
&parsed_hint,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
);
ResolvedType::extend_unique(
results,
ResolvedType::from_classes_with_hint(resolved, parsed_hint),
);
}
let catch_span = catch.block.span();
let cursor_in_catch = ctx.cursor_offset >= catch_span.start.offset
&& ctx.cursor_offset <= catch_span.end.offset;
walk_statements_for_assignments(
catch.block.statements.iter(),
ctx,
results,
!cursor_in_catch,
);
}
if let Some(finally) = &try_stmt.finally_clause {
let finally_span = finally.block.span();
let cursor_in_finally = ctx.cursor_offset >= finally_span.start.offset
&& ctx.cursor_offset <= finally_span.end.offset;
walk_statements_for_assignments(
finally.block.statements.iter(),
ctx,
results,
!cursor_in_finally,
);
}
}
pub(in crate::completion) fn check_statement_for_assignments<'b>(
stmt: &'b Statement<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
conditional: bool,
) {
walk_statements_for_assignments(std::iter::once(stmt), ctx, results, conditional);
}
pub(in crate::completion) fn try_inline_var_override<'b>(
expr: &'b Expression<'b>,
stmt_start: usize,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
conditional: bool,
) -> bool {
let assignment = match expr {
Expression::Assignment(a) if a.operator.is_assign() => a,
_ => return false,
};
let lhs_name = match assignment.lhs {
Expression::Variable(Variable::Direct(dv)) => dv.name.to_string(),
_ => return false,
};
if lhs_name != ctx.var_name {
return false;
}
let rhs_start = assignment.rhs.span().start.offset;
let assign_end = assignment.span().end.offset;
if ctx.cursor_offset >= rhs_start && ctx.cursor_offset <= assign_end {
return false;
}
let (var_type, var_name) = match docblock::find_inline_var_docblock(ctx.content, stmt_start) {
Some(pair) => pair,
None => return false,
};
if let Some(ref vn) = var_name
&& vn != ctx.var_name
{
return false;
}
let native_type = extract_native_type_from_rhs(assignment.rhs, ctx);
let native_parsed = native_type.as_ref().cloned();
let effective = docblock::resolve_effective_type_typed(native_parsed.as_ref(), Some(&var_type));
let eff_type = match effective {
Some(t) => t,
None => return false,
};
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
&eff_type,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
);
if resolved.is_empty() {
if eff_type.is_informative() {
let resolved_types = vec![ResolvedType::from_type_string(eff_type.clone())];
if !conditional {
results.clear();
}
ResolvedType::extend_unique(results, resolved_types);
return true;
}
return false;
}
let resolved_types = ResolvedType::from_classes_with_hint(resolved, eff_type.clone());
if !conditional {
results.clear();
}
ResolvedType::extend_unique(results, resolved_types);
true
}
fn extract_native_type_from_rhs<'b>(
rhs: &'b Expression<'b>,
ctx: &VarResolutionCtx<'_>,
) -> Option<PhpType> {
match rhs {
Expression::Instantiation(inst) => match inst.class {
Expression::Identifier(ident) => Some(PhpType::Named(ident.value().to_string())),
Expression::Self_(_) => Some(PhpType::Named(ctx.current_class.name.clone())),
Expression::Static(_) => Some(PhpType::Named(ctx.current_class.name.clone())),
_ => None,
},
Expression::Call(call) => match call {
Call::Function(func_call) => {
let func_name = match func_call.function {
Expression::Identifier(ident) => Some(ident.value().to_string()),
_ => None,
};
func_name.and_then(|name| {
ctx.function_loader()
.and_then(|fl| fl(&name))
.and_then(|fi| fi.return_type.clone())
})
}
Call::Method(method_call) => {
if let Expression::Variable(Variable::Direct(dv)) = method_call.object
&& dv.name == "$this"
&& let ClassLikeMemberSelector::Identifier(ident) = &method_call.method
{
let method_name = ident.value.to_string();
ctx.all_classes
.iter()
.find(|c| c.name == ctx.current_class.name)
.and_then(|cls| {
cls.methods
.iter()
.find(|m| m.name == method_name)
.and_then(|m| m.return_type.clone())
})
} else {
None
}
}
Call::StaticMethod(static_call) => {
let class_name = match static_call.class {
Expression::Self_(_) | Expression::Static(_) => {
Some(ctx.current_class.name.clone())
}
Expression::Identifier(ident) => Some(ident.value().to_string()),
_ => 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));
owner.and_then(|o| {
o.methods
.iter()
.find(|m| m.name == method_name)
.and_then(|m| m.return_type.clone())
})
} else {
None
}
}
_ => None,
},
Expression::PartialApplication(_)
| Expression::Closure(_)
| Expression::ArrowFunction(_) => Some(PhpType::closure()),
_ => None,
}
}
pub(in crate::completion) fn check_condition_for_assignment<'b>(
condition: &'b Expression<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
) {
let inner = match condition {
Expression::Parenthesized(p) => p.expression,
other => other,
};
if let Expression::Assignment(_) = inner {
check_expression_for_assignment(inner, ctx, results, true);
return;
}
if let Expression::Binary(bin) = inner {
if let Expression::Assignment(_) = bin.lhs {
check_expression_for_assignment(bin.lhs, ctx, results, true);
return;
}
if let Expression::Assignment(_) = bin.rhs {
check_expression_for_assignment(bin.rhs, ctx, results, true);
return;
}
if let Expression::Parenthesized(p) = bin.lhs
&& let Expression::Assignment(_) = p.expression
{
check_expression_for_assignment(p.expression, ctx, results, true);
return;
}
if let Expression::Parenthesized(p) = bin.rhs
&& let Expression::Assignment(_) = p.expression
{
check_expression_for_assignment(p.expression, ctx, results, true);
}
}
}
pub(in crate::completion) fn check_expression_for_assignment<'b>(
expr: &'b Expression<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
conditional: bool,
) {
let var_name = ctx.var_name;
fn push_results(results: &mut Vec<ResolvedType>, new: Vec<ResolvedType>, conditional: bool) {
if new.is_empty() {
return;
}
if !conditional {
results.clear();
}
ResolvedType::extend_unique(results, new);
}
if let Expression::Assignment(assignment) = expr {
if !assignment.operator.is_assign() {
return;
}
if matches!(assignment.lhs, Expression::Array(_) | Expression::List(_)) {
super::foreach_resolution::try_resolve_destructured_type(
assignment,
ctx,
results,
conditional,
);
return;
}
if let Expression::ArrayAccess(array_access) = assignment.lhs
&& let Expression::Variable(Variable::Direct(dv)) = array_access.array
&& dv.name == var_name
{
let rhs_start = assignment.rhs.span().start.offset;
let assign_end = assignment.span().end.offset;
if ctx.cursor_offset >= rhs_start && ctx.cursor_offset <= assign_end {
return;
}
let key = extract_array_key_for_shape(array_access.index);
if let Some(key) = key {
let rhs_ctx = ctx.with_cursor_offset(assignment.span().start.offset);
let resolved =
super::rhs_resolution::resolve_rhs_expression(assignment.rhs, &rhs_ctx);
let value_php_type = if !resolved.is_empty() {
ResolvedType::types_joined(&resolved)
} else {
PhpType::mixed()
};
let base = results
.last()
.map(|rt| &rt.type_string)
.cloned()
.unwrap_or_else(PhpType::array);
let merged = merge_shape_key(&base, &key, &value_php_type);
let new_rt = ResolvedType::from_type_string(merged);
results.clear();
results.push(new_rt);
} else {
let rhs_ctx = ctx.with_cursor_offset(assignment.span().start.offset);
let resolved =
super::rhs_resolution::resolve_rhs_expression(assignment.rhs, &rhs_ctx);
let value_php_type = if !resolved.is_empty() {
ResolvedType::types_joined(&resolved)
} else {
PhpType::mixed()
};
let base_type = results
.last()
.map(|rt| &rt.type_string)
.cloned()
.unwrap_or_else(PhpType::array);
if base_type.is_array_shape() {
return;
}
let key_php_type = infer_array_key_type(array_access.index, &rhs_ctx);
let merged = merge_keyed_type(&base_type, &key_php_type, &value_php_type);
let new_rt = ResolvedType::from_type_string(merged);
results.clear();
results.push(new_rt);
}
return;
}
if let Expression::ArrayAppend(array_append) = assignment.lhs
&& let Expression::Variable(Variable::Direct(dv)) = array_append.array
&& dv.name == var_name
{
let rhs_start = assignment.rhs.span().start.offset;
let assign_end = assignment.span().end.offset;
if ctx.cursor_offset >= rhs_start && ctx.cursor_offset <= assign_end {
return;
}
let rhs_ctx = ctx.with_cursor_offset(assignment.span().start.offset);
let resolved = super::rhs_resolution::resolve_rhs_expression(assignment.rhs, &rhs_ctx);
let value_php_type = if !resolved.is_empty() {
ResolvedType::types_joined(&resolved)
} else {
PhpType::mixed()
};
let base_type = results
.last()
.map(|rt| &rt.type_string)
.cloned()
.unwrap_or_else(PhpType::array);
if base_type.is_array_shape() {
return;
}
let merged = merge_push_type(&base_type, &value_php_type);
let new_rt = ResolvedType::from_type_string(merged);
results.clear();
results.push(new_rt);
return;
}
let lhs_name = match assignment.lhs {
Expression::Variable(Variable::Direct(dv)) => dv.name.to_string(),
_ => return,
};
if lhs_name != var_name {
return;
}
let rhs_start = assignment.rhs.span().start.offset;
let assign_end = assignment.span().end.offset;
if ctx.cursor_offset >= rhs_start && ctx.cursor_offset <= assign_end {
return;
}
let rhs_ctx = ctx.with_cursor_offset(assignment.span().start.offset);
let resolved = super::rhs_resolution::resolve_rhs_expression(assignment.rhs, &rhs_ctx);
push_results(results, resolved, conditional);
}
}
fn extract_array_key_for_shape(index: &Expression<'_>) -> Option<String> {
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()
});
if key.chars().all(|c| c.is_ascii_digit()) {
return None;
}
Some(key)
} else {
None
}
}
fn merge_shape_key(base: &PhpType, key: &str, value_type: &PhpType) -> PhpType {
let mut entries: Vec<ShapeEntry> = Vec::new();
if let Some(shape_entries) = base.shape_entries() {
for entry in shape_entries {
if entry.key.as_deref() != Some(key) {
entries.push(entry.clone());
}
}
}
entries.push(ShapeEntry {
key: Some(key.to_string()),
value_type: value_type.clone(),
optional: false,
});
PhpType::ArrayShape(entries)
}
fn merge_push_type(base: &PhpType, value_type: &PhpType) -> PhpType {
let mut elem_types: Vec<PhpType> = Vec::new();
if let Some(existing_elem) = base.extract_element_type() {
for member in existing_elem.union_members() {
if !member.is_empty() {
elem_types.push(member.clone());
}
}
}
for member in value_type.union_members() {
if !member.is_empty() && !elem_types.iter().any(|e| e.equivalent(member)) {
elem_types.push(member.clone());
}
}
if elem_types.is_empty() {
return PhpType::array();
}
let elem_type = if elem_types.len() == 1 {
elem_types.into_iter().next().unwrap()
} else {
PhpType::Union(elem_types)
};
PhpType::list(elem_type)
}
fn merge_keyed_type(base: &PhpType, key_type: &PhpType, value_type: &PhpType) -> PhpType {
let mut key_types: Vec<PhpType> = Vec::new();
if let Some(existing_key) = base.extract_key_type(false)
&& !existing_key.is_empty()
{
key_types.push(existing_key.clone());
}
for member in key_type.union_members() {
if !member.is_empty() && !key_types.iter().any(|e| e.equivalent(member)) {
key_types.push(member.clone());
}
}
let mut elem_types: Vec<PhpType> = Vec::new();
if let Some(existing_elem) = base.extract_element_type() {
for member in existing_elem.union_members() {
if !member.is_empty() {
elem_types.push(member.clone());
}
}
}
for member in value_type.union_members() {
if !member.is_empty() && !elem_types.iter().any(|e| e.equivalent(member)) {
elem_types.push(member.clone());
}
}
if elem_types.is_empty() {
return PhpType::array();
}
let val_type = if elem_types.len() == 1 {
elem_types.into_iter().next().unwrap()
} else {
PhpType::Union(elem_types)
};
if key_types.is_empty() {
PhpType::generic_array_val(val_type)
} else {
let k_type = if key_types.len() == 1 {
key_types.into_iter().next().unwrap()
} else {
PhpType::Union(key_types)
};
PhpType::generic_array(k_type, val_type)
}
}
fn infer_array_key_type(index: &Expression<'_>, ctx: &VarResolutionCtx<'_>) -> PhpType {
match index {
Expression::Literal(Literal::Integer(_)) => return PhpType::int(),
Expression::Literal(Literal::String(_)) => return PhpType::string(),
_ => {}
}
let resolved = super::rhs_resolution::resolve_rhs_expression(index, ctx);
if !resolved.is_empty() {
let joined = ResolvedType::types_joined(&resolved);
if is_int_like_key_typed(&joined) {
return PhpType::int();
}
if is_string_like_key(&joined) {
return PhpType::string();
}
if joined.is_mixed() || is_array_key_type(&joined) {
return PhpType::Union(vec![PhpType::int(), PhpType::string()]);
}
return joined;
}
PhpType::Union(vec![PhpType::int(), PhpType::string()])
}
fn is_int_like_key_typed(ty: &PhpType) -> bool {
ty.is_int_coercible_key()
}
fn is_string_like_key(ty: &PhpType) -> bool {
ty.is_string_subtype()
}
fn is_array_key_type(ty: &PhpType) -> bool {
if ty.is_array_key() {
return true;
}
match ty {
PhpType::Union(members) if members.len() == 2 => {
let has_int = members.iter().any(|m| m.is_int());
let has_string = members.iter().any(|m| m.is_string_type());
has_int && has_string
}
_ => false,
}
}
pub(in crate::completion) fn first_arg_expr<'b>(
args: &'b ArgumentList<'b>,
) -> Option<&'b Expression<'b>> {
args.arguments.first().map(|arg| match arg {
Argument::Positional(pos) => pos.value,
Argument::Named(named) => named.value,
})
}
pub(in crate::completion) fn nth_arg_expr<'b>(
args: &'b ArgumentList<'b>,
n: usize,
) -> Option<&'b Expression<'b>> {
args.arguments.iter().nth(n).map(|arg| match arg {
Argument::Positional(pos) => pos.value,
Argument::Named(named) => named.value,
})
}
pub(in crate::completion) fn resolve_arg_raw_type<'b>(
arg_expr: &'b Expression<'b>,
ctx: &VarResolutionCtx<'_>,
) -> Option<PhpType> {
if let Expression::Variable(Variable::Direct(dv)) = arg_expr {
let var_text = dv.name.to_string();
let offset = arg_expr.span().start.offset as usize;
let from_docblock =
docblock::find_iterable_raw_type_in_source(ctx.content, offset, &var_text);
if let Some(raw) = from_docblock {
return Some(raw);
}
let resolved = resolve_variable_types(
&var_text,
ctx.current_class,
ctx.all_classes,
ctx.content,
offset as u32,
ctx.class_loader,
Loaders::with_function(ctx.function_loader()),
);
if !resolved.is_empty() {
let joined = crate::types::ResolvedType::types_joined(&resolved);
if joined.extract_value_type(true).is_some() {
return Some(joined);
}
}
}
super::foreach_resolution::resolve_expression_type(arg_expr, ctx)
}
fn try_apply_pass_by_reference_type(
expr: &Expression<'_>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ResolvedType>,
conditional: bool,
) {
let (argument_list, parameters) = match expr {
Expression::Call(Call::Function(func_call)) => {
let func_name = match func_call.function {
Expression::Identifier(ident) => ident.value().to_string(),
_ => return,
};
let fl = match ctx.function_loader() {
Some(fl) => fl,
None => return,
};
let func_info = match fl(&func_name) {
Some(fi) => fi,
None => return,
};
(&func_call.argument_list, func_info.parameters)
}
Expression::Call(Call::Method(method_call)) => {
match try_resolve_method_params(method_call.object, &method_call.method, ctx) {
Some((params,)) => (&method_call.argument_list, params),
None => return,
}
}
Expression::Call(Call::NullSafeMethod(method_call)) => {
match try_resolve_method_params(method_call.object, &method_call.method, ctx) {
Some((params,)) => (&method_call.argument_list, params),
None => return,
}
}
Expression::Call(Call::StaticMethod(static_call)) => {
match try_resolve_static_method_params(static_call, ctx) {
Some((params, arg_list)) => (arg_list, params),
None => return,
}
}
Expression::Instantiation(inst) => match try_resolve_constructor_params(inst, ctx) {
Some((params, arg_list)) => (arg_list, params),
None => return,
},
_ => return,
};
for (i, arg) in argument_list.arguments.iter().enumerate() {
let arg_expr = match arg {
Argument::Positional(pos) => pos.value,
Argument::Named(named) => named.value,
};
let is_our_var = match arg_expr {
Expression::Variable(Variable::Direct(dv)) => dv.name == ctx.var_name,
_ => false,
};
if !is_our_var {
continue;
}
if let Some(param) = parameters.get(i)
&& param.is_reference
&& let Some(type_hint) = ¶m.type_hint
{
let resolved = crate::completion::type_resolution::type_hint_to_classes_typed(
type_hint,
&ctx.current_class.name,
ctx.all_classes,
ctx.class_loader,
);
if !resolved.is_empty() {
if !conditional {
results.clear();
}
ResolvedType::extend_unique(
results,
ResolvedType::from_classes_with_hint(resolved, type_hint.clone()),
);
}
}
}
}
fn try_resolve_method_params(
object: &Expression<'_>,
method: &ClassLikeMemberSelector<'_>,
ctx: &VarResolutionCtx<'_>,
) -> Option<(Vec<ParameterInfo>,)> {
let method_name = match method {
ClassLikeMemberSelector::Identifier(ident) => ident.value,
_ => return None,
};
match object {
Expression::Variable(Variable::Direct(dv)) if dv.name == "$this" => {}
_ => return None,
}
let method_info = ctx
.current_class
.methods
.iter()
.find(|m| m.name == method_name)?;
Some((method_info.parameters.clone(),))
}
fn try_resolve_static_method_params<'a>(
static_call: &'a StaticMethodCall<'a>,
ctx: &VarResolutionCtx<'_>,
) -> Option<(Vec<ParameterInfo>, &'a ArgumentList<'a>)> {
let method_name = match &static_call.method {
ClassLikeMemberSelector::Identifier(ident) => ident.value,
_ => return None,
};
let class_name = match static_call.class {
Expression::Self_(_) | Expression::Static(_) => ctx.current_class.name.clone(),
Expression::Parent(_) => ctx.current_class.parent_class.clone()?,
Expression::Identifier(ident) => ident.value().to_string(),
_ => return None,
};
let cls = (ctx.class_loader)(&class_name)?;
let method_info = cls.methods.iter().find(|m| m.name == method_name)?;
Some((method_info.parameters.clone(), &static_call.argument_list))
}
fn try_resolve_constructor_params<'a>(
inst: &'a Instantiation<'a>,
ctx: &VarResolutionCtx<'_>,
) -> Option<(Vec<ParameterInfo>, &'a ArgumentList<'a>)> {
let class_name = match inst.class {
Expression::Identifier(ident) => ident.value().to_string(),
Expression::Self_(_) | Expression::Static(_) => ctx.current_class.name.clone(),
Expression::Parent(_) => ctx.current_class.parent_class.clone()?,
_ => return None,
};
let args = inst.argument_list.as_ref()?;
let cls = (ctx.class_loader)(&class_name)?;
let ctor = cls.methods.iter().find(|m| m.name == "__construct")?;
Some((ctor.parameters.clone(), args))
}
#[cfg(test)]
#[path = "resolution_tests.rs"]
mod tests;