use std::collections::HashMap;
use php_ast::{ClassMemberKind, EnumMemberKind, Expr, ExprKind, NamespaceBody, Stmt, StmtKind};
use tower_lsp_server::ls_types::Position;
use crate::document::ast::{ParsedDoc, format_type_hint};
use crate::navigation::moniker::resolve_fqn;
use crate::navigation::references::collect_class_imports;
use crate::text::{word_at_position, word_range_at};
use mir_analyzer::FileAnalysis;
fn resolve_type_at_cursor(
source: &str,
doc: &ParsedDoc,
analysis: Option<&FileAnalysis>,
position: Position,
) -> Option<Vec<String>> {
let imports = collect_class_imports(doc);
let class_names = if let Some(word) = word_at_position(source, position) {
if word.starts_with('$') {
let bare_word = word.trim_start_matches('$');
let hint = param_type_for(&doc.program().stmts, bare_word)
.or_else(|| param_type_for(&doc.program().stmts, &word));
let is_late_binding = hint.as_deref().is_some_and(|h| {
h.split(['|', '&']).any(|p| {
matches!(
p.trim().trim_start_matches('?'),
"parent" | "self" | "static"
)
})
});
if is_late_binding {
param_decl_type(source, doc, &imports, &word, position)?
} else {
let from_mir = analysis.and_then(|a| {
let offset = word_range_at(source, position)
.map(|r| doc.view().byte_of_position(r.start))
.unwrap_or_else(|| doc.view().byte_of_position(position));
let names = crate::types::type_query::class_names(
crate::types::type_query::type_at_offset(a, offset)?,
);
(!names.is_empty()).then_some(names)
});
match from_mir {
Some(names) => names,
None => param_decl_type(source, doc, &imports, &word, position)?,
}
}
} else {
let raw = param_type_for(&doc.program().stmts, &word)?;
type_candidates(&raw)
.into_iter()
.map(|c| resolve_fqn(doc, c, &imports))
.collect()
}
} else {
let analysis = analysis?;
let cursor_byte = doc.view().byte_of_position(position);
let offset = innermost_call_method_offset(&doc.program().stmts, cursor_byte)?;
let names = crate::types::type_query::class_names(
crate::types::type_query::type_at_offset(analysis, offset)?,
);
if names.is_empty() {
return None;
}
names
};
Some(class_names)
}
pub fn type_class_fqns(
source: &str,
doc: &ParsedDoc,
analysis: Option<&FileAnalysis>,
position: Position,
) -> Vec<String> {
let Some(class_names) = resolve_type_at_cursor(source, doc, analysis, position) else {
return Vec::new();
};
let mut fqns: Vec<String> = class_names
.iter()
.map(|c| c.trim_start_matches('\\').to_string())
.collect();
fqns.dedup();
fqns
}
fn type_candidates(type_hint: &str) -> Vec<&str> {
let hint = type_hint.strip_prefix('?').unwrap_or(type_hint);
hint.split(['|', '&'])
.map(str::trim)
.filter(|s| !s.is_empty())
.collect()
}
fn innermost_call_method_offset(stmts: &[Stmt<'_, '_>], cursor: u32) -> Option<u32> {
for stmt in stmts {
if !span_contains_cursor(stmt.span, cursor) {
continue;
}
let found = match &stmt.kind {
StmtKind::Expression(e) => call_method_offset_in_expr(e, cursor),
StmtKind::Return(Some(e)) => call_method_offset_in_expr(e, cursor),
StmtKind::Echo(exprs) => exprs
.iter()
.find_map(|e| call_method_offset_in_expr(e, cursor)),
StmtKind::Function(f) => innermost_call_method_offset(&f.body.stmts, cursor),
StmtKind::Class(c) => c.body.members.iter().find_map(|m| {
if let ClassMemberKind::Method(method) = &m.kind
&& let Some(body) = &method.body
{
innermost_call_method_offset(&body.stmts, cursor)
} else {
None
}
}),
StmtKind::Namespace(ns) => {
if let NamespaceBody::Braced(inner) = &ns.body {
innermost_call_method_offset(&inner.stmts, cursor)
} else {
None
}
}
_ => None,
};
if found.is_some() {
return found;
}
}
None
}
fn call_method_offset_in_expr(expr: &Expr<'_, '_>, cursor: u32) -> Option<u32> {
if !span_contains_cursor(expr.span, cursor) {
return None;
}
match &expr.kind {
ExprKind::MethodCall(mc) | ExprKind::NullsafeMethodCall(mc) => {
call_method_offset_in_expr(mc.object, cursor).or(Some(mc.method.span.start))
}
ExprKind::Assign(a) => call_method_offset_in_expr(a.value, cursor),
_ => None,
}
}
#[inline]
fn span_contains_cursor(span: php_ast::Span, cursor: u32) -> bool {
cursor >= span.start && cursor <= span.end
}
fn param_decl_type(
source: &str,
doc: &ParsedDoc,
imports: &HashMap<String, String>,
word: &str,
position: Position,
) -> Option<Vec<String>> {
let raw = param_type_for(&doc.program().stmts, word)
.or_else(|| param_type_for(&doc.program().stmts, word.trim_start_matches('$')))?;
let bare = raw.trim_start_matches('?');
let resolved: Vec<String> = bare
.split(['|', '&'])
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|cand| match cand {
"self" | "static" => crate::types::type_map::enclosing_class_at(source, doc, position)
.map(|c| resolve_fqn(doc, &c, imports))
.unwrap_or_else(|| cand.to_string()),
"parent" => crate::types::type_map::enclosing_class_at(source, doc, position)
.and_then(|c| crate::types::type_map::parent_class_name(doc, &c))
.map(|p| resolve_fqn(doc, &p, imports))
.unwrap_or_else(|| cand.to_string()),
other => resolve_fqn(doc, other, imports),
})
.collect();
(!resolved.is_empty()).then_some(resolved)
}
fn param_type_for(stmts: &[Stmt<'_, '_>], word: &str) -> Option<String> {
for stmt in stmts {
match &stmt.kind {
StmtKind::Function(f) => {
for p in f.params.iter() {
if p.name == word
&& let Some(type_hint) = &p.type_hint
{
return Some(format_type_hint(type_hint));
}
}
}
StmtKind::Class(c) => {
for member in c.body.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind {
for p in m.params.iter() {
if p.name == word
&& let Some(type_hint) = &p.type_hint
{
return Some(format_type_hint(type_hint));
}
}
}
}
}
StmtKind::Interface(i) => {
for member in i.body.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind {
for p in m.params.iter() {
if p.name == word
&& let Some(type_hint) = &p.type_hint
{
return Some(format_type_hint(type_hint));
}
}
}
}
}
StmtKind::Trait(trait_) => {
for member in trait_.body.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind {
for p in m.params.iter() {
if p.name == word
&& let Some(type_hint) = &p.type_hint
{
return Some(format_type_hint(type_hint));
}
}
}
}
}
StmtKind::Enum(e) => {
for member in e.body.members.iter() {
if let EnumMemberKind::Method(m) = &member.kind {
for p in m.params.iter() {
if p.name == word
&& let Some(type_hint) = &p.type_hint
{
return Some(format_type_hint(type_hint));
}
}
}
}
}
StmtKind::Namespace(ns) => {
if let NamespaceBody::Braced(inner) = &ns.body
&& let Some(type_hint) = param_type_for(&inner.stmts, word)
{
return Some(type_hint);
}
}
_ => {}
}
}
None
}