forked from PHPantom-dev/phpantom_lsp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresolver.rs
More file actions
1805 lines (1704 loc) · 74.9 KB
/
Copy pathresolver.rs
File metadata and controls
1805 lines (1704 loc) · 74.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/// Type resolution for completion subjects.
///
/// This module contains the core entry points for resolving a completion
/// subject (e.g. `$this`, `self`, `static`, `$var`, `$this->prop`,
/// `ClassName`) to a concrete `ClassInfo` so that the correct completion
/// items can be offered.
///
/// The resolution logic is split across several sibling modules:
///
/// - [`super::call_resolution`]: Call expression and callable target
/// resolution (method calls, static calls, function calls, constructor
/// calls, signature help, named-argument completion).
/// - [`super::type_resolution`]: Type-hint string to `ClassInfo` mapping
/// (unions, intersections, generics, type aliases, object shapes).
/// - [`super::source_helpers`]: Source-text scanning helpers (closure return
/// types, first-class callable resolution, `new` expression parsing,
/// array access segment walking).
/// - [`super::variable_resolution`]: Variable type resolution via
/// assignment scanning and parameter type hints.
/// - [`super::type_narrowing`]: instanceof / assert / custom type guard
/// narrowing.
/// - [`super::closure_resolution`]: Closure and arrow-function parameter
/// resolution.
/// - [`crate::inheritance`]: Class inheritance merging (traits, mixins,
/// parent chain).
/// - [`super::conditional_resolution`]: PHPStan conditional return type
/// resolution at call sites.
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use crate::atom::AtomMap;
use crate::Backend;
use crate::docblock;
use crate::inheritance::resolve_property_type_hint;
use crate::php_type::PhpType;
use crate::subject_expr::BracketSegment;
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;
// ─── Thread-local chain resolution cache ────────────────────────────────────
//
// During a single diagnostic pass a file may contain many chain expressions
// that share common prefixes (e.g. `$model->where(...)` is the prefix of
// `$model->where(...)->whereNotNull(...)` which is the prefix of
// `$model->where(...)->whereNotNull(...)->orderBy(...)`, etc.).
//
// Without caching, each chain link re-resolves the entire prefix from
// scratch via recursive calls to `resolve_target_classes_expr`. For a
// 6-link Eloquent chain this means the base variable is resolved 6 times,
// the first method call 5 times, etc. — O(depth²) total work.
//
// The chain cache stores `resolve_target_classes` results keyed by the
// raw subject text string. It is activated per-request for all LSP
// handlers (completion, hover, definition, diagnostics, etc.) via
// [`with_chain_resolution_cache`] and consulted by
// `resolve_target_classes` before doing any work.
thread_local! {
/// When `Some`, `resolve_target_classes` will consult and populate
/// this map. Set by [`with_chain_resolution_cache`], cleared on
/// guard drop.
static CHAIN_CACHE: RefCell<Option<HashMap<String, Vec<ResolvedType>>>> =
const { RefCell::new(None) };
}
/// RAII guard that clears the thread-local chain cache on drop.
pub(crate) struct ChainCacheGuard {
/// `true` when this guard owns the cache (outermost activation).
owns: bool,
}
impl Drop for ChainCacheGuard {
fn drop(&mut self) {
if self.owns {
CHAIN_CACHE.with(|cell| {
*cell.borrow_mut() = None;
});
}
}
}
/// Activate the thread-local chain resolution cache.
///
/// While the returned guard is alive, `resolve_target_classes` caches
/// its results by subject text so that shared chain prefixes are
/// resolved only once.
///
/// Nested activations are no-ops — the outermost guard owns the cache.
pub(crate) fn with_chain_resolution_cache() -> ChainCacheGuard {
let already_active = CHAIN_CACHE.with(|cell| cell.borrow().is_some());
if already_active {
return ChainCacheGuard { owns: false };
}
CHAIN_CACHE.with(|cell| {
*cell.borrow_mut() = Some(HashMap::new());
});
ChainCacheGuard { owns: true }
}
/// Type alias for the optional function-loader closure passed through
/// the resolution chain. Reduces clippy `type_complexity` warnings.
pub(crate) type FunctionLoaderFn<'a> = Option<&'a dyn Fn(&str) -> Option<FunctionInfo>>;
/// Type alias for the optional constant-value-loader closure passed
/// through the resolution chain. Given a constant name, returns
/// `Some(Some(value))` when the constant exists with a known value,
/// `Some(None)` when it exists but the value is unknown, and `None`
/// when the constant was not found.
pub(crate) type ConstantLoaderFn<'a> = Option<&'a dyn Fn(&str) -> Option<Option<String>>>;
/// Type alias for the optional scope-based variable resolver from the
/// forward walker. When set on a [`VarResolutionCtx`], variable
/// lookups read from the forward walker's in-progress `ScopeState`
/// instead of re-entering `resolve_variable_types`.
pub(crate) type ScopeVarResolverFn<'a> =
Option<&'a dyn Fn(&str) -> Vec<crate::types::ResolvedType>>;
/// Bundles optional cross-file loader callbacks so they can be threaded
/// through the resolution chain as a single argument instead of one
/// parameter per loader.
#[derive(Clone, Copy, Default)]
pub(crate) struct Loaders<'a> {
/// Cross-file function resolution callback (optional).
pub function_loader: FunctionLoaderFn<'a>,
/// Cross-file constant value resolution callback (optional).
///
/// Given a global constant name (e.g. `"PHP_EOL"`), returns the
/// constant's value string so that the type can be inferred from
/// the literal value.
pub constant_loader: ConstantLoaderFn<'a>,
}
impl<'a> Loaders<'a> {
/// Create a `Loaders` with only a function loader.
pub fn with_function(fl: FunctionLoaderFn<'a>) -> Self {
Self {
function_loader: fl,
constant_loader: None,
}
}
}
/// Bundles the context needed by [`resolve_target_classes`] and
/// the functions it delegates to.
///
/// Introduced to replace the 8-parameter signature of
/// `resolve_target_classes` with a cleaner `(subject, access_kind, ctx)`
/// triple. Also used directly by `resolve_call_return_types_expr` and
/// `resolve_arg_text_to_type` (formerly `CallResolutionCtx`).
pub(crate) struct ResolutionCtx<'a> {
/// The class the cursor is inside, if any.
pub current_class: Option<&'a ClassInfo>,
/// All classes known in the current file.
pub all_classes: &'a [Arc<ClassInfo>],
/// The full source text of the current file.
pub content: &'a str,
/// Byte offset of the cursor in `content`.
pub cursor_offset: u32,
/// Cross-file class resolution callback.
pub class_loader: &'a dyn Fn(&str) -> Option<Arc<ClassInfo>>,
/// Shared cache of fully-resolved classes, keyed by FQN.
///
/// When `Some`, [`resolve_class_fully_cached`](crate::virtual_members::resolve_class_fully_cached)
/// is used instead of the uncached variant, eliminating redundant
/// full-resolution work within a single request cycle. `None` in
/// contexts where no `Backend` (and therefore no cache) is available
/// (e.g. standalone free-function callers, some test helpers).
pub resolved_class_cache: Option<&'a crate::virtual_members::ResolvedClassCache>,
/// Cross-file function resolution callback (optional).
pub function_loader: FunctionLoaderFn<'a>,
/// Optional scope-based variable resolver carried from the forward
/// walker. When set, `resolve_variable_fallback` reads variable
/// types from this closure (which reads the forward walker's
/// in-progress `ScopeState`) instead of calling
/// `resolve_variable_types` which would trigger a full method-body
/// re-walk.
pub scope_var_resolver: ScopeVarResolverFn<'a>,
/// Whether the cursor is inside a `static` method body.
/// When `true`, `$this` is not available and `SubjectExpr::This`
/// resolves to nothing. Precomputed from the `SymbolMap` at the
/// call site to avoid re-parsing the AST.
pub is_in_static_method: bool,
}
/// Bundles the common parameters threaded through variable-type resolution.
///
/// Introducing this struct avoids passing 7–10 individual arguments to
/// every helper in the resolution chain, which keeps clippy happy and
/// makes call-sites much easier to read.
pub(crate) 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>>,
/// Cross-file loader callbacks (function loader, constant loader).
pub loaders: Loaders<'a>,
/// Shared cache of fully-resolved classes, keyed by FQN.
///
/// See [`ResolutionCtx::resolved_class_cache`] for details.
pub resolved_class_cache: Option<&'a crate::virtual_members::ResolvedClassCache>,
/// The `@return` type annotation of the enclosing function/method,
/// if known. Used inside generator bodies to reverse-infer variable
/// types from `Generator<TKey, TValue, TSend, TReturn>`.
pub enclosing_return_type: Option<PhpType>,
/// Pre-computed top-level scope for resolving `global` variable imports.
/// When a function body contains `global $x;`, the walker looks up
/// `$x` in this map to seed the local scope with the top-level type.
pub top_level_scope: Option<AtomMap<Vec<crate::types::ResolvedType>>>,
/// Legacy flag: historically selected branch-aware resolution for
/// hover vs union-all resolution for completion. The forward
/// walker now inherently produces position-accurate types, so both
/// paths behave identically. Kept for API compatibility with
/// callers that set it to `true` (hover, diagnostics).
pub branch_aware: bool,
/// Match-arm instanceof narrowings: var name → narrowed types.
/// Empty outside of match(true) arm bodies.
pub match_arm_narrowing: HashMap<String, Vec<crate::types::ResolvedType>>,
/// Optional scope-based variable resolver from the forward walker.
///
/// When set, `resolve_var_types` in `rhs_resolution.rs` reads
/// variable types from this closure instead of re-entering
/// `resolve_variable_types`, which would trigger a redundant
/// forward walk of the method body.
///
/// The closure takes a `$`-prefixed variable name and returns the
/// variable's types from the forward walker's in-progress
/// `ScopeState`.
pub scope_var_resolver: ScopeVarResolverFn<'a>,
}
impl<'a> VarResolutionCtx<'a> {
/// Create a [`ResolutionCtx`] from this variable resolution context.
///
/// The non-optional `current_class` is wrapped in `Some(…)`.
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,
scope_var_resolver: self.scope_var_resolver,
is_in_static_method: false,
}
}
/// Convenience accessor for the function loader.
pub fn function_loader(&self) -> FunctionLoaderFn<'a> {
self.loaders.function_loader
}
/// Convenience accessor for the constant loader.
pub fn constant_loader(&self) -> ConstantLoaderFn<'a> {
self.loaders.constant_loader
}
/// Clone this context with a different `cursor_offset`.
///
/// All other fields (including `enclosing_return_type`) are preserved.
/// This is useful when resolving a right-hand-side expression at a
/// position earlier than the original cursor to avoid infinite
/// recursion on self-referential assignments.
pub(crate) 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(),
top_level_scope: self.top_level_scope.clone(),
branch_aware: self.branch_aware,
match_arm_narrowing: self.match_arm_narrowing.clone(),
scope_var_resolver: self.scope_var_resolver,
}
}
/// Clone this context with match-arm instanceof narrowings applied.
///
/// All other fields are preserved. This is used when descending
/// into a `match(true)` arm body whose conditions narrow one or
/// more variables via `instanceof`.
pub(crate) fn with_match_arm_narrowing(
&self,
match_arm_narrowing: HashMap<String, Vec<crate::types::ResolvedType>>,
) -> 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: self.enclosing_return_type.clone(),
top_level_scope: self.top_level_scope.clone(),
branch_aware: self.branch_aware,
match_arm_narrowing,
scope_var_resolver: self.scope_var_resolver,
}
}
}
// ── Helpers to convert between ResolvedType and Arc<ClassInfo> ──────
//
// Many internal callers (property chain bases, call resolution, etc.)
// still operate on `Vec<Arc<ClassInfo>>`. These thin wrappers avoid
// repeating the conversion at every call site inside this module.
/// Convert `Vec<ResolvedType>` to `Vec<Arc<ClassInfo>>`, discarding
/// entries without class info (scalars, shapes, unresolvable types).
fn resolved_to_arcs(resolved: Vec<ResolvedType>) -> Vec<Arc<ClassInfo>> {
ResolvedType::into_arced_classes(resolved)
}
/// Resolve a completion subject to all candidate types, preserving
/// both class info and type strings.
///
/// This is the primary entry point for subject resolution. It returns
/// `Vec<ResolvedType>` which carries both the structured type string
/// (e.g. `PhpType::Named("Collection")`) and the optional `ClassInfo`.
/// Callers that only need classes can call
/// `ResolvedType::into_arced_classes()` on the result.
pub(crate) fn resolve_target_classes(
subject: &str,
access_kind: AccessKind,
ctx: &ResolutionCtx<'_>,
) -> Vec<ResolvedType> {
let expr = SubjectExpr::parse(subject);
resolve_target_classes_expr(&expr, access_kind, ctx)
}
/// Core dispatch for [`resolve_target_classes`], operating on a
/// pre-parsed [`SubjectExpr`].
pub(crate) fn resolve_target_classes_expr(
expr: &SubjectExpr,
access_kind: AccessKind,
ctx: &ResolutionCtx<'_>,
) -> Vec<ResolvedType> {
// ── Chain cache lookup ───────────────────────────────────────
// During diagnostic passes the chain cache is active and stores
// results by subject text. This eliminates O(depth²) re-resolution
// of shared chain prefixes (e.g. `$model->where(...)` resolved once
// and reused by `$model->where(...)->whereNotNull(...)` etc.).
//
// The cache is NOT used for variable-only subjects (no `->` or `::`
// in the expression) because those are context-sensitive: the same
// `$var` may resolve to different types at different cursor offsets
// due to reassignment or narrowing.
//
// PropertyChain expressions rooted in a variable (e.g. `$this->pet`,
// `$obj->prop`) are also excluded because instanceof narrowing can
// change the resolved type at different positions within the same
// method body. For example, `$this->pet` may resolve to `Dog`
// inside `if ($this->pet instanceof Dog)` but to `Cat` after
// `if (!$this->pet instanceof Cat) { return; }`.
//
// Call expressions and static accesses are safe to cache because
// their return types are deterministic (method signatures don't
// change based on narrowing context).
let is_cacheable_chain = match expr {
SubjectExpr::CallExpr { .. }
| SubjectExpr::MethodCall { .. }
| SubjectExpr::StaticMethodCall { .. }
| SubjectExpr::StaticAccess { .. } => true,
// PropertyChain is only cacheable when the base is NOT a
// bare variable — e.g. `$this->method()->prop` (CallExpr
// base) is safe, but `$this->pet` (This/Variable base) is
// subject to narrowing.
SubjectExpr::PropertyChain { base, .. } => !matches!(
base.as_ref(),
SubjectExpr::This
| SubjectExpr::SelfKw
| SubjectExpr::StaticKw
| SubjectExpr::Parent
| SubjectExpr::Variable(_)
),
_ => false,
};
if is_cacheable_chain {
let cache_key = expr.to_subject_text();
let cached = CHAIN_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 result = resolve_target_classes_expr_inner(expr, access_kind, ctx);
CHAIN_CACHE.with(|cell| {
let mut borrow = cell.borrow_mut();
if let Some(ref mut map) = *borrow {
map.insert(cache_key, result.clone());
}
});
return result;
}
resolve_target_classes_expr_inner(expr, access_kind, ctx)
}
/// Inner implementation of [`resolve_target_classes_expr`] without
/// chain caching. The outer function handles cache lookup/store.
fn resolve_target_classes_expr_inner(
expr: &SubjectExpr,
access_kind: AccessKind,
ctx: &ResolutionCtx<'_>,
) -> Vec<ResolvedType> {
thread_local! {
static RESOLVE_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
let depth = RESOLVE_DEPTH.with(|d| {
let v = d.get() + 1;
d.set(v);
v
});
// Maximum nesting depth for `resolve_target_classes_expr_inner`.
// Breaks infinite recursion between subject resolution, call-return
// resolution, and variable resolution that can occur on files with
// deeply intertwined class hierarchies and virtual members.
const MAX_RESOLVE_TARGET_DEPTH: u32 = 60;
if depth > MAX_RESOLVE_TARGET_DEPTH {
RESOLVE_DEPTH.with(|d| d.set(depth - 1));
return vec![];
}
let result = resolve_target_classes_expr_inner_impl(expr, access_kind, ctx);
RESOLVE_DEPTH.with(|d| d.set(depth - 1));
result
}
fn resolve_target_classes_expr_inner_impl(
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 {
// ── Keywords that always mean "current class" ────────────
SubjectExpr::This => {
// `$this` is not available inside static methods.
if current_class.is_some() && ctx.is_in_static_method {
return vec![];
}
// Check for `@param-closure-this` override: when the cursor
// is inside a closure passed as an argument to a function
// whose parameter carries `@param-closure-this`, resolve
// `$this` to the declared type instead of the lexical class.
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(),
// ── `parent::` — resolve to the current class's parent ──
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_arc(Arc::clone(cls))];
}
return class_loader(parent_name)
.map(ResolvedType::from_arc)
.into_iter()
.collect();
}
vec![]
}
// ── Inline array literal with index access ──────────────
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
}
// ── Enum case / static member access ────────────────────
SubjectExpr::StaticAccess { class, member } => {
// Handle self/static/parent keywords — SubjectExpr::parse
// produces StaticAccess for "self::MONTH", "static::FOO",
// etc., but "self"/"static"/"parent" are keywords, not
// class names, so find_class_by_name / class_loader won't
// find them.
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) {
// parent — resolve via all_classes first, then class_loader
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()
}
};
// When the member is a static property (starts with `$`),
// resolve to the property's declared type instead of the
// owning class. This makes `self::$instance->method()`
// resolve `method()` on the property's type, not on the
// class that declares the static property.
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_arc).collect(),
);
}
if !results.is_empty() {
return results;
}
}
owner_classes
.into_iter()
.map(ResolvedType::from_arc)
.collect()
}
// ── Bare class name ─────────────────────────────────────
SubjectExpr::ClassName(name) => {
if let Some(cls) = find_class_by_name(all_classes, name) {
return vec![ResolvedType::from_arc(Arc::clone(cls))];
}
class_loader(name)
.map(ResolvedType::from_arc)
.into_iter()
.collect()
}
// ── `new ClassName` (without trailing call parens) ───────
SubjectExpr::NewExpr { class_name } => {
if let Some(cls) = find_class_by_name(all_classes, class_name) {
return vec![ResolvedType::from_arc(Arc::clone(cls))];
}
class_loader(class_name)
.map(ResolvedType::from_arc)
.into_iter()
.collect()
}
// ── Call expression ─────────────────────────────────────
SubjectExpr::CallExpr { callee, args_text } => {
let mut hint: Option<PhpType> = None;
let classes = Backend::resolve_call_return_types_expr_with_hint(
callee,
args_text,
ctx,
Some(&mut hint),
);
// Use the raw return type hint only when at least one
// resolved class has template parameters — non-generic
// classes don't benefit from it.
if let Some(h) = hint
&& classes.iter().any(|c| !c.template_params.is_empty())
{
return ResolvedType::from_classes_with_hint(classes, h);
}
classes.into_iter().map(ResolvedType::from_arc).collect()
}
// ── Property chain ──────────────────────────────────────
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);
}
// ── Property-level narrowing ────────────────────────
// When the property chain resolves to a union (or a
// broad interface type), an enclosing `instanceof`
// check like `if ($this->prop instanceof Foo)` should
// narrow the result set, just as it does for plain
// variables. Build the full access path (e.g.
// `$this->timeline`) and run the narrowing walk.
//
// This also handles untyped properties: when the
// property has no type hint, `results` is empty but
// an `instanceof` check or `assert()` can still
// provide a type via `apply_instanceof_inclusion`.
//
// Use a dummy class when outside a class body so that
// property narrowing works in standalone functions and
// top-level code (e.g. `$arg->value instanceof Foo`
// inside a foreach).
{
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(ResolvedType::from_arc)
.collect()
}
// ── Array access on variable or call expression ─────────
SubjectExpr::ArrayAccess { base, segments } => {
// Check if the scope has a narrowed type for this array
// access (e.g. `$row['page']` narrowed via `instanceof`).
if let Some(scope_resolver) = ctx.scope_var_resolver {
// Build the scope key with double-quote format used by
// `expr_to_subject_key` (e.g. `$row["page"]`).
let scope_key = {
let mut k = base.to_subject_text();
for seg in segments {
match seg {
BracketSegment::StringKey(s) => {
k.push_str(&format!("[\"{}\"]", s));
}
BracketSegment::ElementAccess => {
k.push_str("[]");
}
}
}
k
};
let from_scope = scope_resolver(&scope_key);
if !from_scope.is_empty() {
return from_scope;
}
}
// When no scope resolver is available (top-level completion),
// try resolving the full array access key through the forward
// walker. This picks up instanceof narrowing on array elements
// (e.g. `$row['page'] instanceof Page` narrows `$row["page"]`).
if ctx.scope_var_resolver.is_none() && matches!(base.as_ref(), SubjectExpr::Variable(_))
{
let scope_key = {
let mut k = base.to_subject_text();
for seg in segments {
match seg {
BracketSegment::StringKey(s) => {
k.push_str(&format!("[\"{}\"]", s));
}
BracketSegment::ElementAccess => {
k.push_str("[]");
}
}
}
k
};
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(
&scope_key,
effective_class,
all_classes,
ctx.content,
ctx.cursor_offset,
class_loader,
Loaders::with_function(ctx.function_loader),
);
if !resolved.is_empty() {
return resolved;
}
}
// When the base is a call expression (e.g. `$c->items()[0]`),
// resolve the call's raw return type and use it as a candidate
// for array-segment walking. This mirrors the variable path
// but sources the raw type from the method/function signature
// instead of from docblock annotations or assignments.
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_arc).collect();
}
}
// If raw-type approach didn't work, fall back to resolving
// the call normally (handles cases like `getItems()[0]`
// where the return type is already a class with ArrayAccess).
return vec![];
}
let base_var = base.to_subject_text();
// Build candidate raw types from multiple strategies.
// Each is tried as a complete pipeline (raw type →
// segment walk → ClassInfo); the first that succeeds
// through all segments wins.
// ── Property chain raw type ─────────────────────────
// When the base is a property chain (e.g. `$this->cache`,
// `$obj->items`), resolve the owning class and extract
// the property's raw type hint. This preserves generic
// parameters like `array<string, IntCollection>` or
// `Collection<int, Translation>` that would be lost if
// we resolved through `type_hint_to_classes_typed` first.
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,
)
.map(|t| crate::util::resolve_php_type_names(&t, ctx.class_loader));
// resolve_variable_types is designed for bare `$variable` names;
// property chains like `$this->query->joins` are handled by the
// property_raw_type strategy above. Skip this strategy for
// non-variable expressions (chains, array access, comparisons,
// null coalescing, boolean expressions) to avoid polluting
// the scope cache with unsupported keys.
let is_bare_variable = !base_var.contains("->")
&& !base_var.contains("::")
&& !base_var.contains('[')
&& !base_var.contains("===")
&& !base_var.contains("&&")
&& !base_var.contains("??")
&& !base_var.contains("||");
let ast_type: Option<PhpType> = if is_bare_variable {
// When a scope_var_resolver is available (i.e. we are
// inside the forward walker), read the variable type
// from the in-progress ScopeState instead of calling
// resolve_variable_types which would re-enter the
// forward walker and cause stack overflow.
if let Some(scope_resolver) = ctx.scope_var_resolver {
let prefixed = if base_var.starts_with('$') {
base_var.clone()
} else {
format!("${}", base_var)
};
let from_scope = scope_resolver(&prefixed);
if from_scope.is_empty() {
None
} else {
Some(ResolvedType::types_joined(&from_scope))
}
} else {
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))
}
}
} else {
None
};
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_arc).collect();
}
// Segment walk failed — the base type does not have
// array-shape, generic, or iterable annotations that
// cover bracket access. Return empty: `$var['key']` is
// never the same type as `$var`.
vec![]
}
// ── Bare variable ───────────────────────────────────────
SubjectExpr::Variable(var_name) => resolve_variable_fallback(var_name, access_kind, ctx),
// ── Callee-only variants (MethodCall, StaticMethodCall,
// FunctionCall) should not appear as top-level subjects;
// they are wrapped in CallExpr. If they do appear
// (e.g. from a partial parse), treat as class name. ────
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_arc(Arc::clone(cls))];
}
class_loader(&text)
.map(ResolvedType::from_arc)
.into_iter()
.collect()
}
}
}
/// Extract the raw return type string from a call expression's callee.
///
/// Given a `CallExpr`'s callee and arguments, resolves the owning class
/// (for method/static-method calls) or the function info (for standalone
/// functions), finds the matching method/function, and returns its raw
/// return type string (e.g. `"Item[]"`). This is used by the
/// `ArrayAccess` handler to strip array dimensions and resolve the
/// element type when the base of `[0]` is a call expression.
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 {
// Use a fully-resolved class so that inherited docblock
// return types (e.g. `list<Pen>` from an interface or
// parent) are visible instead of the bare native hint.
let merged = crate::virtual_members::resolve_class_fully_maybe_cached(
cls,
ctx.class_loader,
ctx.resolved_class_cache,
);
let found = merged.get_method_ci(method);
if let Some(m) = found {
if let Some(ref ret) = m.return_type {
return Some(ret.clone());
}
// Method exists but has no return type.
// Only fall through to __call for virtual methods
// (from @method tags or @mixin). Real methods are
// invoked directly at runtime, not through __call.
if !m.is_virtual {
continue;
}
}
// __call fallback: method not found, or virtual method
// without a return type. Use __call's return type so
// that chains through dynamic calls (e.g. Builder
// where{Column}) preserve the type.
if let Some(m) = merged.get_method_ci("__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.get_method_ci(method);
if let Some(m) = found {
if let Some(ref ret) = m.return_type {
return Some(ret.clone());
}
// Method exists but has no return type.
// Only fall through to __callStatic for virtual methods.
if !m.is_virtual {
return None;
}
}
// __callStatic fallback: method not found, or virtual
// method without a return type.
if let Some(m) = merged.get_method_ci("__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,
}
}
// ─── Enriched subject resolution for diagnostics ────────────────────────────
/// The outcome of resolving a subject for diagnostic purposes.
///
/// [`resolve_target_classes`] only returns `Vec<Arc<ClassInfo>>` and
/// silently drops scalar types and type-string-only entries.
/// Diagnostics need to know *why* resolution returned empty — was the
/// subject a scalar type (runtime crash), an unresolvable class name
/// (likely typo / missing import), or truly untyped? This enum
/// carries that distinction so the diagnostic collector can emit the
/// right message and severity.
///
/// ## Architectural invariant
///