Core dispatch for [`resolve_target_classes`], operating on a pre-parsed [`SubjectExpr`].
(
expr: &SubjectExpr,
access_kind: AccessKind,
ctx: &ResolutionCtx<'_>,
)
| 345 | /// Core dispatch for [`resolve_target_classes`], operating on a |
| 346 | /// pre-parsed [`SubjectExpr`]. |
| 347 | pub(crate) fn resolve_target_classes_expr( |
| 348 | expr: &SubjectExpr, |
| 349 | access_kind: AccessKind, |
| 350 | ctx: &ResolutionCtx<'_>, |
| 351 | ) -> Vec<ResolvedType> { |
| 352 | // ── Chain cache lookup ─────────────────────────────────────── |
| 353 | // During diagnostic passes the chain cache is active and stores |
| 354 | // results by subject text. This eliminates O(depth²) re-resolution |
| 355 | // of shared chain prefixes (e.g. `$model->where(...)` resolved once |
| 356 | // and reused by `$model->where(...)->whereNotNull(...)` etc.). |
| 357 | // |
| 358 | // The cache is NOT used for variable-only subjects (no `->` or `::` |
| 359 | // in the expression) because those are context-sensitive: the same |
| 360 | // `$var` may resolve to different types at different cursor offsets |
| 361 | // due to reassignment or narrowing. |
| 362 | // |
| 363 | // PropertyChain expressions rooted in a variable (e.g. `$this->pet`, |
| 364 | // `$obj->prop`) are also excluded because instanceof narrowing can |
| 365 | // change the resolved type at different positions within the same |
| 366 | // method body. For example, `$this->pet` may resolve to `Dog` |
| 367 | // inside `if ($this->pet instanceof Dog)` but to `Cat` after |
| 368 | // `if (!$this->pet instanceof Cat) { return; }`. |
| 369 | // |
| 370 | // Call expressions and static accesses are safe to cache because |
| 371 | // their return types are deterministic (method signatures don't |
| 372 | // change based on narrowing context). |
| 373 | let is_cacheable_chain = match expr { |
| 374 | SubjectExpr::CallExpr { .. } |
| 375 | | SubjectExpr::MethodCall { .. } |
| 376 | | SubjectExpr::StaticMethodCall { .. } |
| 377 | | SubjectExpr::StaticAccess { .. } => true, |
| 378 | // PropertyChain is only cacheable when the base is NOT a |
| 379 | // bare variable — e.g. `$this->method()->prop` (CallExpr |
| 380 | // base) is safe, but `$this->pet` (This/Variable base) is |
| 381 | // subject to narrowing. |
| 382 | SubjectExpr::PropertyChain { base, .. } => !matches!( |
| 383 | base.as_ref(), |
| 384 | SubjectExpr::This |
| 385 | | SubjectExpr::SelfKw |
| 386 | | SubjectExpr::StaticKw |
| 387 | | SubjectExpr::Parent |
| 388 | | SubjectExpr::Variable(_) |
| 389 | ), |
| 390 | _ => false, |
| 391 | }; |
| 392 | if is_cacheable_chain { |
| 393 | let cache_key = expr.to_subject_text(); |
| 394 | let cached = CHAIN_CACHE.with(|cell| { |
| 395 | let borrow = cell.borrow(); |
| 396 | borrow.as_ref().and_then(|map| map.get(&cache_key).cloned()) |
| 397 | }); |
| 398 | if let Some(result) = cached { |
| 399 | return result; |
| 400 | } |
| 401 | |
| 402 | let result = resolve_target_classes_expr_inner(expr, access_kind, ctx); |
| 403 | |
| 404 | CHAIN_CACHE.with(|cell| { |
no test coverage detected