use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use crate::Backend;
use crate::docblock;
use crate::inheritance::resolve_property_type_hint;
use crate::php_type::PhpType;
use crate::subject_expr::SubjectExpr;
use crate::types::*;
use crate::util::{find_class_by_name, is_self_or_static, resolve_class_keyword};
use crate::virtual_members::resolve_class_fully_maybe_cached;
pub(crate) type FunctionLoaderFn<'a> = Option<&'a dyn Fn(&str) -> Option<FunctionInfo>>;
pub(crate) type ConstantLoaderFn<'a> = Option<&'a dyn Fn(&str) -> Option<Option<String>>>;
#[derive(Clone, Copy, Default)]
pub(crate) struct Loaders<'a> {
pub function_loader: FunctionLoaderFn<'a>,
pub constant_loader: ConstantLoaderFn<'a>,
}
impl<'a> Loaders<'a> {
pub fn with_function(fl: FunctionLoaderFn<'a>) -> Self {
Self {
function_loader: fl,
constant_loader: None,
}
}
}
pub(crate) struct ResolutionCtx<'a> {
pub current_class: Option<&'a ClassInfo>,
pub all_classes: &'a [Arc<ClassInfo>],
pub content: &'a str,
pub cursor_offset: u32,
pub class_loader: &'a dyn Fn(&str) -> Option<Arc<ClassInfo>>,
pub resolved_class_cache: Option<&'a crate::virtual_members::ResolvedClassCache>,
pub function_loader: FunctionLoaderFn<'a>,
}
pub(super) struct VarResolutionCtx<'a> {
pub var_name: &'a str,
pub current_class: &'a ClassInfo,
pub all_classes: &'a [Arc<ClassInfo>],
pub content: &'a str,
pub cursor_offset: u32,
pub class_loader: &'a dyn Fn(&str) -> Option<Arc<ClassInfo>>,
pub loaders: Loaders<'a>,
pub resolved_class_cache: Option<&'a crate::virtual_members::ResolvedClassCache>,
pub enclosing_return_type: Option<PhpType>,
pub branch_aware: bool,
}
impl<'a> VarResolutionCtx<'a> {
pub(crate) fn as_resolution_ctx(&self) -> ResolutionCtx<'a> {
ResolutionCtx {
current_class: Some(self.current_class),
all_classes: self.all_classes,
content: self.content,
cursor_offset: self.cursor_offset,
class_loader: self.class_loader,
function_loader: self.loaders.function_loader,
resolved_class_cache: self.resolved_class_cache,
}
}
pub fn function_loader(&self) -> FunctionLoaderFn<'a> {
self.loaders.function_loader
}
pub fn constant_loader(&self) -> ConstantLoaderFn<'a> {
self.loaders.constant_loader
}
pub(super) fn with_enclosing_return_type(
&self,
enclosing_return_type: Option<PhpType>,
) -> VarResolutionCtx<'a> {
VarResolutionCtx {
var_name: self.var_name,
current_class: self.current_class,
all_classes: self.all_classes,
content: self.content,
cursor_offset: self.cursor_offset,
class_loader: self.class_loader,
loaders: self.loaders,
resolved_class_cache: self.resolved_class_cache,
enclosing_return_type,
branch_aware: self.branch_aware,
}
}
pub(super) fn with_cursor_offset(&self, cursor_offset: u32) -> VarResolutionCtx<'a> {
VarResolutionCtx {
var_name: self.var_name,
current_class: self.current_class,
all_classes: self.all_classes,
content: self.content,
cursor_offset,
class_loader: self.class_loader,
loaders: self.loaders,
resolved_class_cache: self.resolved_class_cache,
enclosing_return_type: self.enclosing_return_type.clone(),
branch_aware: self.branch_aware,
}
}
}
type DiagSubjectCache =
HashMap<(String, AccessKind, u32, u32, u32, u32), Vec<crate::types::ResolvedType>>;
struct DiagSubjectCacheFileData {
scopes: Vec<(u32, u32)>,
var_defs: Vec<crate::symbol_map::VarDefSite>,
narrowing_blocks: Vec<(u32, u32)>,
assert_narrowing_offsets: Vec<u32>,
}
type DiagSubjectCacheState = (DiagSubjectCache, DiagSubjectCacheFileData);
thread_local! {
static DIAG_SUBJECT_CACHE: RefCell<Option<DiagSubjectCacheState>> = const { RefCell::new(None) };
}
fn resolved_to_arcs(resolved: Vec<ResolvedType>) -> Vec<Arc<ClassInfo>> {
ResolvedType::into_arced_classes(resolved)
}
pub(crate) struct DiagSubjectCacheGuard {
owns_cache: bool,
}
impl Drop for DiagSubjectCacheGuard {
fn drop(&mut self) {
if self.owns_cache {
DIAG_SUBJECT_CACHE.with(|cell| {
*cell.borrow_mut() = None;
});
}
}
}
pub(crate) fn with_diagnostic_subject_cache() -> DiagSubjectCacheGuard {
let already_active = DIAG_SUBJECT_CACHE.with(|cell| cell.borrow().is_some());
if already_active {
return DiagSubjectCacheGuard { owns_cache: false };
}
DIAG_SUBJECT_CACHE.with(|cell| {
*cell.borrow_mut() = Some((
HashMap::new(),
DiagSubjectCacheFileData {
scopes: Vec::new(),
var_defs: Vec::new(),
narrowing_blocks: Vec::new(),
assert_narrowing_offsets: Vec::new(),
},
));
});
DiagSubjectCacheGuard { owns_cache: true }
}
pub(crate) fn set_diagnostic_subject_cache_scopes(
scopes: Vec<(u32, u32)>,
var_defs: Vec<crate::symbol_map::VarDefSite>,
narrowing_blocks: Vec<(u32, u32)>,
assert_narrowing_offsets: Vec<u32>,
) {
DIAG_SUBJECT_CACHE.with(|cell| {
let mut borrow = cell.borrow_mut();
if let Some((_map, file_data)) = borrow.as_mut() {
file_data.scopes = scopes;
file_data.var_defs = var_defs;
file_data.narrowing_blocks = narrowing_blocks;
file_data.assert_narrowing_offsets = assert_narrowing_offsets;
}
});
}
fn diag_cache_enclosing_scope(cursor_offset: u32) -> u32 {
DIAG_SUBJECT_CACHE.with(|cell| {
let borrow = cell.borrow();
match borrow.as_ref() {
Some((_map, file_data)) => {
let mut best: u32 = 0;
for &(start, end) in &file_data.scopes {
if start <= cursor_offset && cursor_offset <= end && start > best {
best = start;
}
}
best
}
None => 0,
}
})
}
fn diag_cache_narrowing_block(cursor_offset: u32) -> u32 {
DIAG_SUBJECT_CACHE.with(|cell| {
let borrow = cell.borrow();
match borrow.as_ref() {
Some((_map, file_data)) => {
let mut best: u32 = 0;
for &(start, end) in &file_data.narrowing_blocks {
if start <= cursor_offset && cursor_offset <= end && start > best {
best = start;
}
}
best
}
None => 0,
}
})
}
fn diag_cache_assert_offset(cursor_offset: u32) -> u32 {
DIAG_SUBJECT_CACHE.with(|cell| {
let borrow = cell.borrow();
match borrow.as_ref() {
Some((_map, file_data)) => {
match file_data
.assert_narrowing_offsets
.partition_point(|&o| o < cursor_offset)
{
0 => 0,
i => file_data.assert_narrowing_offsets[i - 1],
}
}
None => 0,
}
})
}
fn diag_cache_var_def_offset(subject: &str, cursor_offset: u32) -> u32 {
if !subject.starts_with('$') || subject.starts_with("$this") {
return 0;
}
let after_dollar = &subject[1..];
let var_name = after_dollar
.find("->")
.map(|i| &after_dollar[..i])
.unwrap_or(after_dollar);
DIAG_SUBJECT_CACHE.with(|cell| {
let borrow = cell.borrow();
match borrow.as_ref() {
Some((_map, file_data)) => {
let scope_start = {
let mut best: u32 = 0;
for &(start, end) in &file_data.scopes {
if start <= cursor_offset && cursor_offset <= end && start > best {
best = start;
}
}
best
};
file_data
.var_defs
.iter()
.rev()
.find(|d| {
d.name == var_name
&& d.scope_start == scope_start
&& d.effective_from <= cursor_offset
})
.map(|d| d.effective_from)
.unwrap_or(0)
}
None => 0,
}
})
}
pub(crate) fn resolve_target_classes(
subject: &str,
access_kind: AccessKind,
ctx: &ResolutionCtx<'_>,
) -> Vec<ResolvedType> {
resolve_target_classes_inner(subject, access_kind, ctx, false)
}
fn resolve_target_classes_inner(
subject: &str,
access_kind: AccessKind,
ctx: &ResolutionCtx<'_>,
skip_cache_lookup: bool,
) -> Vec<ResolvedType> {
let scope_start = diag_cache_enclosing_scope(ctx.cursor_offset);
let var_def_offset = diag_cache_var_def_offset(subject, ctx.cursor_offset);
let narrowing_offset = if subject.starts_with('$') && !subject.starts_with("$this") {
diag_cache_narrowing_block(ctx.cursor_offset)
} else {
0
};
let assert_offset = if subject.starts_with('$') && !subject.starts_with("$this") {
diag_cache_assert_offset(ctx.cursor_offset)
} else {
0
};
let cache_key = (
subject.to_string(),
access_kind,
scope_start,
var_def_offset,
narrowing_offset,
assert_offset,
);
if !skip_cache_lookup {
let cached = DIAG_SUBJECT_CACHE.with(|cell| {
let borrow = cell.borrow();
borrow
.as_ref()
.and_then(|(map, _)| map.get(&cache_key).cloned())
});
if let Some(result) = cached {
return result;
}
}
let expr = SubjectExpr::parse(subject);
let result = resolve_target_classes_expr(&expr, access_kind, ctx);
let skip_cache =
result.is_empty() && super::variable::resolution::is_var_resolution_depth_limited();
if !skip_cache {
DIAG_SUBJECT_CACHE.with(|cell| {
let mut borrow = cell.borrow_mut();
if let Some((map, _)) = borrow.as_mut() {
map.insert(cache_key, result.clone());
}
});
}
result
}
pub(crate) fn resolve_target_classes_expr(
expr: &SubjectExpr,
access_kind: AccessKind,
ctx: &ResolutionCtx<'_>,
) -> Vec<ResolvedType> {
let current_class = ctx.current_class;
let all_classes = ctx.all_classes;
let class_loader = ctx.class_loader;
match expr {
SubjectExpr::This => {
if let Some(override_cls) =
super::variable::closure_resolution::find_closure_this_override(ctx)
{
return vec![ResolvedType::from_class(override_cls)];
}
current_class
.map(|cc| ResolvedType::from_class(cc.clone()))
.into_iter()
.collect()
}
SubjectExpr::SelfKw | SubjectExpr::StaticKw => current_class
.map(|cc| ResolvedType::from_class(cc.clone()))
.into_iter()
.collect(),
SubjectExpr::Parent => {
if let Some(cc) = current_class
&& let Some(ref parent_name) = cc.parent_class
{
if let Some(cls) = find_class_by_name(all_classes, parent_name) {
return vec![ResolvedType::from_class(cls.as_ref().clone())];
}
return class_loader(parent_name)
.map(|arc| ResolvedType::from_class(Arc::unwrap_or_clone(arc)))
.into_iter()
.collect();
}
vec![]
}
SubjectExpr::InlineArray { elements, .. } => {
let mut element_types = Vec::new();
for elem_text in elements {
let elem = elem_text.trim();
if elem.is_empty() {
continue;
}
let elem_expr = SubjectExpr::parse(elem);
let resolved = resolve_target_classes_expr(&elem_expr, AccessKind::Arrow, ctx);
ResolvedType::extend_unique(&mut element_types, resolved);
}
element_types
}
SubjectExpr::StaticAccess { class, member } => {
let owner_classes: Vec<Arc<ClassInfo>> = if is_self_or_static(class) {
current_class
.map(|cc| Arc::new(cc.clone()))
.into_iter()
.collect()
} else if let Some(parent_name) = resolve_class_keyword(class, current_class) {
if let Some(cls) = find_class_by_name(all_classes, &parent_name) {
vec![Arc::clone(cls)]
} else {
class_loader(&parent_name).into_iter().collect()
}
} else {
if let Some(cls) = find_class_by_name(all_classes, class) {
vec![Arc::clone(cls)]
} else {
class_loader(class).into_iter().collect()
}
};
if let Some(prop_name) = member.strip_prefix('$') {
let mut results: Vec<ResolvedType> = Vec::new();
for cls in &owner_classes {
let resolved = super::type_resolution::resolve_property_types(
prop_name,
cls,
all_classes,
class_loader,
);
ResolvedType::extend_unique(
&mut results,
resolved.into_iter().map(ResolvedType::from_class).collect(),
);
}
if !results.is_empty() {
return results;
}
}
owner_classes
.into_iter()
.map(|arc| ResolvedType::from_class(Arc::unwrap_or_clone(arc)))
.collect()
}
SubjectExpr::ClassName(name) => {
if let Some(cls) = find_class_by_name(all_classes, name) {
return vec![ResolvedType::from_class(cls.as_ref().clone())];
}
class_loader(name)
.map(|arc| ResolvedType::from_class(Arc::unwrap_or_clone(arc)))
.into_iter()
.collect()
}
SubjectExpr::NewExpr { class_name } => {
if let Some(cls) = find_class_by_name(all_classes, class_name) {
return vec![ResolvedType::from_class(cls.as_ref().clone())];
}
class_loader(class_name)
.map(|arc| ResolvedType::from_class(Arc::unwrap_or_clone(arc)))
.into_iter()
.collect()
}
SubjectExpr::CallExpr { callee, args_text } => {
let classes = Backend::resolve_call_return_types_expr(callee, args_text, ctx);
classes
.into_iter()
.map(|arc| ResolvedType::from_class(Arc::unwrap_or_clone(arc)))
.collect()
}
SubjectExpr::PropertyChain { base, property } => {
let base_arcs = resolved_to_arcs(resolve_target_classes_expr(base, access_kind, ctx));
let mut arc_results: Vec<Arc<ClassInfo>> = Vec::new();
for cls in &base_arcs {
let resolved = super::type_resolution::resolve_property_types(
property,
cls,
all_classes,
class_loader,
);
ClassInfo::extend_unique_arc(
&mut arc_results,
resolved.into_iter().map(Arc::new).collect(),
);
}
{
let dummy_class;
let effective_class = match current_class {
Some(cc) => cc,
None => {
dummy_class = ClassInfo::default();
&dummy_class
}
};
let full_path = format!("{}->{}", base.to_subject_text(), property);
apply_property_narrowing(&full_path, effective_class, ctx, &mut arc_results);
}
arc_results
.into_iter()
.map(|arc| ResolvedType::from_class(Arc::unwrap_or_clone(arc)))
.collect()
}
SubjectExpr::ArrayAccess { base, segments } => {
if let SubjectExpr::CallExpr { callee, args_text } = base.as_ref() {
let call_raw = resolve_call_raw_return_type(callee, args_text, ctx);
if let Some(raw) = call_raw {
let candidates = std::iter::once(raw);
if let Some(resolved) =
super::source::helpers::try_chained_array_access_with_candidates(
candidates,
segments,
current_class,
all_classes,
class_loader,
)
{
return resolved.into_iter().map(ResolvedType::from_class).collect();
}
}
return vec![];
}
let base_var = base.to_subject_text();
let property_raw_type: Option<PhpType> = if let SubjectExpr::PropertyChain {
base: prop_base,
property,
} = base.as_ref()
{
let owner_arcs =
resolved_to_arcs(resolve_target_classes_expr(prop_base, access_kind, ctx));
owner_arcs.iter().find_map(|cls| {
crate::inheritance::resolve_property_type_hint(cls, property, class_loader)
})
} else {
None
};
let docblock_type: Option<PhpType> = docblock::find_iterable_raw_type_in_source(
ctx.content,
ctx.cursor_offset as usize,
&base_var,
);
let ast_type: Option<PhpType> = {
let dummy_class;
let effective_class = match current_class {
Some(cc) => cc,
None => {
dummy_class = ClassInfo::default();
&dummy_class
}
};
let resolved = crate::completion::variable::resolution::resolve_variable_types(
&base_var,
effective_class,
all_classes,
ctx.content,
ctx.cursor_offset,
class_loader,
Loaders::with_function(ctx.function_loader),
);
if resolved.is_empty() {
None
} else {
Some(ResolvedType::types_joined(&resolved))
}
};
let candidates = property_raw_type
.into_iter()
.chain(docblock_type)
.chain(ast_type);
if let Some(resolved) = super::source::helpers::try_chained_array_access_with_candidates(
candidates,
segments,
current_class,
all_classes,
class_loader,
) {
return resolved.into_iter().map(ResolvedType::from_class).collect();
}
vec![]
}
SubjectExpr::Variable(var_name) => resolve_variable_fallback(var_name, access_kind, ctx),
SubjectExpr::MethodCall { .. }
| SubjectExpr::StaticMethodCall { .. }
| SubjectExpr::FunctionCall(_) => {
let text = expr.to_subject_text();
if let Some(cls) = find_class_by_name(all_classes, &text) {
return vec![ResolvedType::from_class(cls.as_ref().clone())];
}
class_loader(&text)
.map(|arc| ResolvedType::from_class(Arc::unwrap_or_clone(arc)))
.into_iter()
.collect()
}
}
}
fn resolve_call_raw_return_type(
callee: &SubjectExpr,
_args_text: &str,
ctx: &ResolutionCtx<'_>,
) -> Option<PhpType> {
match callee {
SubjectExpr::MethodCall { base, method } => {
let base_classes =
resolved_to_arcs(resolve_target_classes_expr(base, AccessKind::Arrow, ctx));
for cls in &base_classes {
let merged = crate::virtual_members::resolve_class_fully_maybe_cached(
cls,
ctx.class_loader,
ctx.resolved_class_cache,
);
let found = merged
.methods
.iter()
.find(|m| m.name.eq_ignore_ascii_case(method));
if let Some(m) = found {
if let Some(ref ret) = m.return_type {
return Some(ret.clone());
}
if !m.is_virtual {
continue;
}
}
if let Some(m) = merged
.methods
.iter()
.find(|m| m.name.eq_ignore_ascii_case("__call"))
&& let Some(ref ret) = m.return_type
{
return Some(ret.clone());
}
}
None
}
SubjectExpr::StaticMethodCall { class, method } => {
let owner = resolve_static_owner_class(class, ctx);
if let Some(ref cls) = owner {
let merged = crate::virtual_members::resolve_class_fully_maybe_cached(
cls,
ctx.class_loader,
ctx.resolved_class_cache,
);
let found = merged
.methods
.iter()
.find(|m| m.name.eq_ignore_ascii_case(method));
if let Some(m) = found {
if let Some(ref ret) = m.return_type {
return Some(ret.clone());
}
if !m.is_virtual {
return None;
}
}
if let Some(m) = merged
.methods
.iter()
.find(|m| m.name.eq_ignore_ascii_case("__callStatic"))
&& let Some(ref ret) = m.return_type
{
return Some(ret.clone());
}
}
None
}
SubjectExpr::FunctionCall(fn_name) => {
if let Some(fl) = ctx.function_loader
&& let Some(func_info) = fl(fn_name)
{
return func_info.return_type.clone();
}
None
}
_ => None,
}
}
#[derive(Clone, Debug)]
pub(crate) enum SubjectOutcome {
Resolved(Vec<Arc<ClassInfo>>),
Scalar(PhpType),
UnresolvableClass(String),
Untyped,
}
pub(crate) fn resolve_subject_outcome(
subject: &str,
access_kind: AccessKind,
ctx: &ResolutionCtx<'_>,
) -> SubjectOutcome {
let expr = SubjectExpr::parse(subject);
let skip_cache = matches!(expr, SubjectExpr::Variable(_));
let resolved = resolve_target_classes_inner(subject, access_kind, ctx, skip_cache);
if !resolved.is_empty() {
let arced: Vec<Arc<ClassInfo>> = ResolvedType::into_arced_classes(resolved.clone());
if !arced.is_empty() {
return SubjectOutcome::Resolved(arced);
}
let joined = ResolvedType::types_joined(&resolved);
if joined.all_members_primitive_scalar() {
let scalar = joined.non_null_type().unwrap_or(joined);
return SubjectOutcome::Scalar(scalar);
}
if resolved
.iter()
.any(|rt| rt.type_string.is_named_ci("stdclass") || rt.type_string.is_object())
{
let synthetic = Arc::new(ClassInfo {
name: "stdClass".to_string(),
..ClassInfo::default()
});
return SubjectOutcome::Resolved(vec![synthetic]);
}
if let Some(unresolved) = check_unresolvable_class_name(&joined, ctx.class_loader) {
return SubjectOutcome::UnresolvableClass(unresolved);
}
return SubjectOutcome::Untyped;
}
if let SubjectExpr::CallExpr {
callee,
args_text: _,
} = &expr
{
if let Some(scalar) = resolve_call_scalar_return(callee, access_kind, ctx) {
return SubjectOutcome::Scalar(scalar);
}
if let SubjectExpr::FunctionCall(fn_name) = callee.as_ref()
&& let Some(fl) = ctx.function_loader
&& let Some(func_info) = fl(fn_name.as_str())
&& let Some(ref raw_type) = func_info.return_type
&& let Some(unresolved) = check_unresolvable_class_name(raw_type, ctx.class_loader)
{
return SubjectOutcome::UnresolvableClass(unresolved);
}
}
if let SubjectExpr::PropertyChain { base, property } = &expr {
let base_arcs = resolved_to_arcs(resolve_target_classes_expr(base, access_kind, ctx));
for cls in &base_arcs {
let merged =
resolve_class_fully_maybe_cached(cls, ctx.class_loader, ctx.resolved_class_cache);
if let Some(parsed) = resolve_property_type_hint(&merged, property, ctx.class_loader) {
if parsed.all_members_primitive_scalar() {
let scalar = parsed.non_null_type().unwrap_or(parsed);
return SubjectOutcome::Scalar(scalar);
}
return SubjectOutcome::Untyped;
}
}
}
if let SubjectExpr::Variable(var_name) = &expr
&& let Some(resolved_type) = crate::hover::variable_type::resolve_variable_type(
var_name,
ctx.content,
ctx.cursor_offset,
ctx.current_class,
ctx.all_classes,
ctx.class_loader,
Loaders::with_function(ctx.function_loader),
)
&& let Some(unresolved) = check_unresolvable_class_name(&resolved_type, ctx.class_loader)
{
return SubjectOutcome::UnresolvableClass(unresolved);
}
SubjectOutcome::Untyped
}
fn resolve_call_scalar_return(
callee: &SubjectExpr,
access_kind: AccessKind,
ctx: &ResolutionCtx<'_>,
) -> Option<PhpType> {
match callee {
SubjectExpr::MethodCall { base, method } => {
let base_arcs = resolved_to_arcs(resolve_target_classes_expr(base, access_kind, ctx));
for cls in &base_arcs {
let resolved = resolve_class_fully_maybe_cached(
cls,
ctx.class_loader,
ctx.resolved_class_cache,
);
if let Some(m) = resolved
.methods
.iter()
.find(|m| m.name.eq_ignore_ascii_case(method))
&& let Some(ref hint) = m.return_type
&& hint.all_members_primitive_scalar()
{
let scalar = hint.non_null_type().unwrap_or_else(|| hint.clone());
return Some(scalar);
}
}
None
}
SubjectExpr::FunctionCall(fn_name) => {
if let Some(fl) = ctx.function_loader
&& let Some(func_info) = fl(fn_name)
&& let Some(ref hint) = func_info.return_type
&& hint.all_members_primitive_scalar()
{
let scalar = hint.non_null_type().unwrap_or_else(|| hint.clone());
return Some(scalar);
}
None
}
SubjectExpr::StaticMethodCall { class, method } => {
let cls = (ctx.class_loader)(class);
if let Some(cls) = cls {
let resolved = resolve_class_fully_maybe_cached(
&cls,
ctx.class_loader,
ctx.resolved_class_cache,
);
if let Some(m) = resolved
.methods
.iter()
.find(|m| m.name.eq_ignore_ascii_case(method))
&& let Some(ref hint) = m.return_type
&& hint.all_members_primitive_scalar()
{
let scalar = hint.non_null_type().unwrap_or_else(|| hint.clone());
return Some(scalar);
}
}
None
}
_ => None,
}
}
fn check_unresolvable_class_name(
raw_type: &PhpType,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> Option<String> {
if raw_type.all_members_scalar() {
return None;
}
let effective = raw_type.non_null_type().unwrap_or_else(|| raw_type.clone());
let base = effective.base_name()?;
if class_loader(base).is_none() {
Some(base.to_string())
} else {
None
}
}
fn resolve_variable_fallback(
var_name: &str,
access_kind: AccessKind,
ctx: &ResolutionCtx<'_>,
) -> Vec<ResolvedType> {
let current_class = ctx.current_class;
let all_classes = ctx.all_classes;
let class_loader = ctx.class_loader;
let function_loader = ctx.function_loader;
let dummy_class;
let effective_class = match current_class {
Some(cc) => cc,
None => {
dummy_class = ClassInfo::default();
&dummy_class
}
};
if access_kind == AccessKind::DoubleColon {
let class_string_targets =
crate::completion::variable::class_string_resolution::resolve_class_string_targets(
var_name,
effective_class,
all_classes,
ctx.content,
ctx.cursor_offset,
class_loader,
);
if !class_string_targets.is_empty() {
return class_string_targets
.into_iter()
.map(ResolvedType::from_class)
.collect();
}
}
let resolved_types = super::variable::resolution::resolve_variable_types(
var_name,
effective_class,
all_classes,
ctx.content,
ctx.cursor_offset,
class_loader,
Loaders::with_function(function_loader),
);
if access_kind == AccessKind::DoubleColon {
let mut class_string_results: Vec<ResolvedType> = Vec::new();
for rt in &resolved_types {
let inner = match &rt.type_string {
PhpType::ClassString(Some(inner)) => Some(inner.as_ref()),
PhpType::Nullable(inner) => match inner.as_ref() {
PhpType::ClassString(Some(cs_inner)) => Some(cs_inner.as_ref()),
_ => None,
},
PhpType::Union(members) => {
for member in members {
let cs_inner = match member {
PhpType::ClassString(Some(inner)) => Some(inner.as_ref()),
PhpType::Nullable(inner) => match inner.as_ref() {
PhpType::ClassString(Some(cs_inner)) => Some(cs_inner.as_ref()),
_ => None,
},
_ => None,
};
if let Some(inner_ty) = cs_inner {
let resolved = super::type_resolution::type_hint_to_classes_typed(
inner_ty,
&effective_class.name,
all_classes,
class_loader,
);
for cls in resolved {
ResolvedType::push_unique(
&mut class_string_results,
ResolvedType::from_class(cls),
);
}
}
}
None }
_ => None,
};
if let Some(inner_ty) = inner {
let resolved = super::type_resolution::type_hint_to_classes_typed(
inner_ty,
&effective_class.name,
all_classes,
class_loader,
);
for cls in resolved {
ResolvedType::push_unique(
&mut class_string_results,
ResolvedType::from_class(cls),
);
}
}
}
if !class_string_results.is_empty() {
return class_string_results;
}
}
resolved_types
}
pub(in crate::completion) fn resolve_static_owner_class(
class: &str,
rctx: &ResolutionCtx<'_>,
) -> Option<Arc<ClassInfo>> {
if is_self_or_static(class) {
rctx.current_class.map(|cc| Arc::new(cc.clone()))
} else if let Some(resolved_name) = resolve_class_keyword(class, rctx.current_class) {
(rctx.class_loader)(&resolved_name)
} else {
find_class_by_name(rctx.all_classes, class)
.map(Arc::clone)
.or_else(|| (rctx.class_loader)(class))
.or_else(|| {
resolved_to_arcs(resolve_target_classes(
class,
crate::AccessKind::DoubleColon,
rctx,
))
.into_iter()
.next()
})
}
}
fn apply_property_narrowing(
property_path: &str,
current_class: &ClassInfo,
rctx: &ResolutionCtx<'_>,
results: &mut Vec<Arc<ClassInfo>>, ) {
use crate::parser::with_parsed_program;
let mut plain: Vec<ClassInfo> = results.drain(..).map(Arc::unwrap_or_clone).collect();
with_parsed_program(
rctx.content,
"apply_property_narrowing",
|program, _content| {
let ctx = VarResolutionCtx {
var_name: property_path,
current_class,
all_classes: rctx.all_classes,
content: rctx.content,
cursor_offset: rctx.cursor_offset,
class_loader: rctx.class_loader,
loaders: Loaders::with_function(rctx.function_loader),
resolved_class_cache: None,
enclosing_return_type: None,
branch_aware: false,
};
walk_property_narrowing_in_statements(program.statements.iter(), &ctx, &mut plain);
},
);
*results = plain.into_iter().map(Arc::new).collect();
}
fn walk_property_narrowing_in_statements<'b>(
statements: impl Iterator<Item = &'b mago_syntax::ast::Statement<'b>>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ClassInfo>,
) {
use mago_span::HasSpan;
use mago_syntax::ast::*;
for stmt in statements {
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 {
walk_property_narrowing_in_members(class.members.iter(), ctx, results);
return;
}
}
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 {
walk_property_narrowing_in_members(trait_def.members.iter(), ctx, results);
return;
}
}
Statement::Namespace(ns) => {
let ns_span = ns.span();
if ctx.cursor_offset >= ns_span.start.offset
&& ctx.cursor_offset <= ns_span.end.offset
{
walk_property_narrowing_in_statements(ns.statements().iter(), ctx, results);
return;
}
}
Statement::Function(func) => {
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 {
walk_property_narrowing_stmts(func.body.statements.iter(), ctx, results);
return;
}
}
Statement::If(if_stmt) => {
let if_span = stmt.span();
if ctx.cursor_offset >= if_span.start.offset
&& ctx.cursor_offset <= if_span.end.offset
{
for inner in if_stmt.body.statements().iter() {
walk_property_narrowing_in_statements(std::iter::once(inner), ctx, results);
}
}
}
Statement::Block(block) => {
let blk_span = stmt.span();
if ctx.cursor_offset >= blk_span.start.offset
&& ctx.cursor_offset <= blk_span.end.offset
{
walk_property_narrowing_in_statements(block.statements.iter(), ctx, results);
}
}
_ => {}
}
}
}
fn walk_property_narrowing_in_members<'b>(
members: impl Iterator<Item = &'b mago_syntax::ast::class_like::member::ClassLikeMember<'b>>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ClassInfo>,
) {
use mago_syntax::ast::class_like::member::ClassLikeMember;
use mago_syntax::ast::class_like::method::MethodBody;
for member in members {
if let ClassLikeMember::Method(method) = member {
let body = match &method.body {
MethodBody::Concrete(block) => block,
_ => continue,
};
let body_start = body.left_brace.start.offset;
let body_end = body.right_brace.end.offset;
if ctx.cursor_offset >= body_start && ctx.cursor_offset <= body_end {
walk_property_narrowing_stmts(body.statements.iter(), ctx, results);
return;
}
}
}
}
fn walk_property_narrowing_stmts<'b>(
statements: impl Iterator<Item = &'b mago_syntax::ast::Statement<'b>>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ClassInfo>,
) {
use mago_span::HasSpan;
use mago_syntax::ast::*;
use super::types::narrowing;
for stmt in statements {
let stmt_span = stmt.span();
if stmt_span.start.offset >= ctx.cursor_offset {
continue;
}
match stmt {
Statement::If(if_stmt) => {
walk_property_narrowing_if(if_stmt, stmt, ctx, results);
}
Statement::Block(block) => {
walk_property_narrowing_stmts(block.statements.iter(), ctx, results);
}
Statement::Expression(expr_stmt) => {
narrowing::try_apply_assert_instanceof_narrowing(
expr_stmt.expression,
ctx,
results,
);
}
Statement::Foreach(foreach) => match &foreach.body {
ForeachBody::Statement(inner) => {
walk_property_narrowing_stmt(inner, ctx, results);
}
ForeachBody::ColonDelimited(body) => {
walk_property_narrowing_stmts(body.statements.iter(), ctx, results);
}
},
Statement::While(while_stmt) => match &while_stmt.body {
WhileBody::Statement(inner) => {
walk_property_narrowing_stmt(inner, ctx, results);
}
WhileBody::ColonDelimited(body) => {
walk_property_narrowing_stmts(body.statements.iter(), ctx, results);
}
},
Statement::For(for_stmt) => match &for_stmt.body {
ForBody::Statement(inner) => {
walk_property_narrowing_stmt(inner, ctx, results);
}
ForBody::ColonDelimited(body) => {
walk_property_narrowing_stmts(body.statements.iter(), ctx, results);
}
},
Statement::DoWhile(dw) => {
walk_property_narrowing_stmt(dw.statement, ctx, results);
}
Statement::Try(try_stmt) => {
walk_property_narrowing_stmts(try_stmt.block.statements.iter(), ctx, results);
for catch in try_stmt.catch_clauses.iter() {
walk_property_narrowing_stmts(catch.block.statements.iter(), ctx, results);
}
if let Some(finally) = &try_stmt.finally_clause {
walk_property_narrowing_stmts(finally.block.statements.iter(), ctx, results);
}
}
Statement::Switch(switch) => {
for case in switch.body.cases().iter() {
walk_property_narrowing_stmts(case.statements().iter(), ctx, results);
}
}
_ => {}
}
}
}
fn walk_property_narrowing_if<'b>(
if_stmt: &'b mago_syntax::ast::If<'b>,
enclosing_stmt: &'b mago_syntax::ast::Statement<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ClassInfo>,
) {
use mago_span::HasSpan;
use mago_syntax::ast::*;
use super::types::narrowing;
match &if_stmt.body {
IfBody::Statement(body) => {
narrowing::try_apply_instanceof_narrowing(
if_stmt.condition,
body.statement.span(),
ctx,
results,
);
walk_property_narrowing_stmt(body.statement, ctx, results);
for else_if in body.else_if_clauses.iter() {
narrowing::try_apply_instanceof_narrowing(
else_if.condition,
else_if.statement.span(),
ctx,
results,
);
walk_property_narrowing_stmt(else_if.statement, ctx, results);
}
if let Some(else_clause) = &body.else_clause {
let else_span = else_clause.statement.span();
narrowing::try_apply_instanceof_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
results,
);
for else_if in body.else_if_clauses.iter() {
narrowing::try_apply_instanceof_narrowing_inverse(
else_if.condition,
else_span,
ctx,
results,
);
}
walk_property_narrowing_stmt(else_clause.statement, ctx, results);
}
}
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),
);
narrowing::try_apply_instanceof_narrowing(if_stmt.condition, then_span, ctx, results);
walk_property_narrowing_stmts(body.statements.iter(), ctx, results);
for else_if in body.else_if_clauses.iter() {
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,
),
);
narrowing::try_apply_instanceof_narrowing(else_if.condition, ei_span, ctx, results);
walk_property_narrowing_stmts(else_if.statements.iter(), ctx, results);
}
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,
),
);
narrowing::try_apply_instanceof_narrowing_inverse(
if_stmt.condition,
else_span,
ctx,
results,
);
for else_if in body.else_if_clauses.iter() {
narrowing::try_apply_instanceof_narrowing_inverse(
else_if.condition,
else_span,
ctx,
results,
);
}
walk_property_narrowing_stmts(else_clause.statements.iter(), ctx, results);
}
}
}
if enclosing_stmt.span().end.offset < ctx.cursor_offset {
narrowing::apply_guard_clause_narrowing(if_stmt, ctx, results);
}
}
fn walk_property_narrowing_stmt<'b>(
stmt: &'b mago_syntax::ast::Statement<'b>,
ctx: &VarResolutionCtx<'_>,
results: &mut Vec<ClassInfo>,
) {
walk_property_narrowing_stmts(std::iter::once(stmt), ctx, results);
}