Skip to content

Commit c2a8a77

Browse files
committed
Address failing go-to-definition cases
1 parent 412118f commit c2a8a77

15 files changed

Lines changed: 1856 additions & 71 deletions

docs/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **Docblock navigation.** Go-to-definition and hover now work on class names inside callable type annotations (`\Closure(Request): Response`), and Ctrl+Click on object shape properties (`$profile->name` from `@return object{name: string}`) jumps to the key inside the docblock.
13+
14+
### Fixed
15+
16+
- **GTD for `@method`/`@property` on interfaces.** Go-to-definition now walks implemented interfaces (own and from parents) before checking `@mixin` classes, so virtual members declared on interfaces resolve correctly.
17+
- **`?->` null-safe chain resolution.** The `rfind("->")` split incorrectly matched the `->` inside `?->`, leaving a trailing `?` on the left-hand side. Fixed at all seven call sites across resolver, text resolution, handler, foreach resolution, and signature help.
18+
- **`(new Canvas())->easel` property access.** Parenthesized `new` expressions on the left side of `->` now resolve correctly for variable type inference.
19+
- **Array function resolution.** `array_pop`, `array_filter`, `array_values`, `end`, and `array_map` now resolve element types correctly for go-to-definition and completion when the array comes from a method call chain or a `$var->prop` access.
20+
1021
## [0.4.0] - 2026-03-01
1122

1223
### Added

docs/todo.md

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -788,13 +788,32 @@ branches in `TypeSpecifier::specifyTypesInCondition`.
788788

789789
---
790790

791+
### 24. Go-to-definition for array shape keys via bracket access
792+
**Impact: Low · Effort: Medium**
793+
794+
Array shape keys accessed via bracket notation (`$status['code']`)
795+
have no go-to-definition support. The type comes from a
796+
`@phpstan-type` / `@phpstan-import-type` alias or a direct
797+
`@var` / `@return` annotation resolved to
798+
`array{code: int, label: string}`, but Ctrl+Click on the string
799+
key inside `['code']` does nothing.
800+
801+
Object shape properties (`$profile->name` from
802+
`@return object{name: string}`) already jump to the property key
803+
in the docblock. Extending the same approach to bracket-access
804+
array shapes would require detecting the array key context in the
805+
GTD path (similar to array shape completion) and searching for the
806+
key inside the matching `array{…}` annotation.
807+
808+
---
809+
791810
<!-- ============================================================ -->
792811
<!-- TIER 5 — LOW IMPACT -->
793812
<!-- ============================================================ -->
794813

795814
## Low Impact
796815

797-
### 24. Short-name collisions in `find_implementors`
816+
### 25. Short-name collisions in `find_implementors`
798817
**Impact: Low · Effort: Low**
799818

800819
`class_implements_or_extends` matches interfaces by both short name and
@@ -810,7 +829,7 @@ before comparison.
810829

811830
---
812831

813-
### 25. Fiber type resolution
832+
### 26. Fiber type resolution
814833
**Impact: Low · Effort: Low**
815834

816835
`Generator<TKey, TValue, TSend, TReturn>` has dedicated support for
@@ -825,7 +844,7 @@ Generator extraction in `docblock/types.rs`.
825844

826845
---
827846

828-
### 26. Non-empty-string propagation through string functions
847+
### 27. Non-empty-string propagation through string functions
829848
**Impact: Low · Effort: Low**
830849

831850
PHPStan tracks `non-empty-string` through string-manipulating
@@ -843,7 +862,7 @@ See `NonEmptyStringFunctionsReturnTypeExtension` in PHPStan.
843862

844863
---
845864

846-
### 27. `Closure::bind()` / `Closure::fromCallable()` return type preservation
865+
### 28. `Closure::bind()` / `Closure::fromCallable()` return type preservation
847866
**Impact: Low · Effort: Low-Medium**
848867

849868
Variables holding closure literals, arrow functions, and first-class
@@ -859,7 +878,7 @@ See `ClosureBindDynamicReturnTypeExtension` and
859878

860879
---
861880

862-
### 28. Remove deprecated text-search fallbacks
881+
### 29. Remove deprecated text-search fallbacks
863882
**Impact: Low · Effort: Medium**
864883

865884
The go-to-definition subsystem now uses the precomputed `SymbolMap` as
@@ -891,7 +910,7 @@ would let that deprecated function be removed entirely.
891910

892911
---
893912

894-
### 29. Non-array functions with dynamic return types
913+
### 30. Non-array functions with dynamic return types
895914
**Impact: Low · Effort: High**
896915

897916
PHPStan also provides dynamic return type extensions for many non-array
@@ -922,7 +941,7 @@ return types (less impactful for class-based completion).
922941

923942
---
924943

925-
### 30. Language construct signature help and hover
944+
### 31. Language construct signature help and hover
926945
**Impact: Low · Effort: Low**
927946

928947
PHP language constructs that use parentheses (`unset()`, `isset()`, `empty()`,
@@ -939,22 +958,22 @@ need a similar hardcoded lookup.
939958

940959
---
941960

942-
### 31. Diagnostics
961+
### 32. Diagnostics
943962
**Impact: Low (large scope) · Effort: Very High**
944963

945964
No error reporting (undefined methods, type mismatches, etc.).
946965

947966
---
948967

949-
### 32. Code Actions
968+
### 33. Code Actions
950969
**Impact: Low · Effort: Very High**
951970

952971
No quick fixes or refactoring suggestions. No `codeActionProvider` in
953972
`ServerCapabilities`, no `textDocument/codeAction` handler, and no
954973
`WorkspaceEdit` generation infrastructure beyond trivial `TextEdit`s for
955974
use-statement insertion.
956975

957-
#### 32a. Extract Function refactoring
976+
#### 33a. Extract Function refactoring
958977

959978
Select a range of statements inside a method/function and extract them into a
960979
new function. The LSP would need to:
@@ -975,7 +994,7 @@ new function. The LSP would need to:
975994
|---|---|
976995
| Hover (§1) | "Resolve type at arbitrary position" — needed to type params |
977996
| Document Symbols (§12) | AST range → symbol mapping — needed to find enclosing function and valid insertion points |
978-
| Find References (§8) | Variable usage tracking across a scope — the same "which variables are used where" analysis |
997+
| Find References (§7) | Variable usage tracking across a scope — the same "which variables are used where" analysis |
979998
| Simple code actions (add use stmt, implement interface) | Builds the code action + `WorkspaceEdit` plumbing |
980999

9811000
---

example.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ public function demo(): void
411411
{
412412
/** @var object{title: string, score: float} $item */
413413
$item = getUnknownValue();
414-
$item->title; // object shape property
415-
$item->score;
414+
$item->title; // Ctrl+Click → jumps to `title:` in docblock above
415+
$item->score; // Ctrl+Click → jumps to `score:` in docblock above
416416
}
417417
}
418418

@@ -733,11 +733,11 @@ public function demo(): void
733733

734734
// Object shapes
735735
$profile = $this->getProfile();
736-
$profile->name; // object{name: string, ...}
736+
$profile->name; // Ctrl+Click → jumps to `name:` in @return docblock
737737

738738
$result = $this->getResult();
739-
$result->tool->write(); // nested objectPen
740-
$result->meta->page; // nested object shape
739+
$result->tool->write(); // Ctrl+Click `tool`jumps to `tool:` in @return docblock
740+
$result->meta->page; // Ctrl+Click `meta` → jumps to `meta:` in @return docblock
741741
}
742742

743743
/** @return array{pen: Pen, pencil: Pencil, active: bool} */
@@ -1410,6 +1410,17 @@ public function returnType(): GtdResult { return new GtdResult(); }
14101410
* @throws GtdNotFoundException Ctrl+Click GtdNotFoundException
14111411
*/
14121412
public function docblockTypes($items) { return $items; }
1413+
1414+
/**
1415+
* Callable types in docblocks. Ctrl+Click on any class name inside the
1416+
* callable signature to jump to its definition. Hover shows the class
1417+
* info instead of treating the whole callable as one token.
1418+
*
1419+
* @param \Closure(GtdAlpha): GtdResult $transform Ctrl+Click GtdAlpha or GtdResult
1420+
* @param callable(GtdAlpha, GtdBeta): GtdResult $merge Ctrl+Click any of the three
1421+
* @return callable(): GtdResult Ctrl+Click GtdResult
1422+
*/
1423+
public function callableDocblockTypes($transform, $merge) { return $merge; }
14131424
}
14141425

14151426
class GtdAlpha { public function label(): string { return 'alpha'; } }

src/completion/foreach_resolution.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,9 @@ impl Backend {
612612
{
613613
// Split at the last `->` to get the object and method name.
614614
if let Some(arrow_pos) = call_body.rfind("->") {
615-
let obj_text = &call_body[..arrow_pos];
615+
let obj_text = call_body[..arrow_pos]
616+
.strip_suffix('?')
617+
.unwrap_or(&call_body[..arrow_pos]);
616618
let method_name = &call_body[arrow_pos + 2..];
617619
let current_class =
618620
all_classes.iter().find(|c| c.name == current_class_name);

src/completion/handler.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1213,7 +1213,8 @@ impl Backend {
12131213

12141214
// ── Instance method: `$subject->method` ─────────────────────
12151215
if let Some(pos) = expr.rfind("->") {
1216-
let subject = &expr[..pos];
1216+
// Strip trailing `?` from subject when the operator was `?->`
1217+
let subject = expr[..pos].strip_suffix('?').unwrap_or(&expr[..pos]);
12171218
let method_name = &expr[pos + 2..];
12181219

12191220
let owner_classes: Vec<crate::ClassInfo> = if subject == "$this"

src/completion/resolver.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,11 @@ impl Backend {
475475
let function_loader = ctx.function_loader;
476476
// ── Instance method call: $this->method / $var->method ──
477477
if let Some(pos) = call_body.rfind("->") {
478-
let lhs = &call_body[..pos];
478+
// Strip trailing `?` from LHS when the operator was `?->`
479+
// (e.g. `$maybe?->getCanvas` splits into lhs=`$maybe?`).
480+
let lhs = call_body[..pos]
481+
.strip_suffix('?')
482+
.unwrap_or(&call_body[..pos]);
479483
let method_name = &call_body[pos + 2..];
480484

481485
// Resolve the left-hand side to a class (recursively handles
@@ -1482,7 +1486,10 @@ impl Backend {
14821486
{
14831487
// Instance method chain: `expr->method()`
14841488
if let Some(pos) = call_body.rfind("->") {
1485-
let lhs = &call_body[..pos];
1489+
// Strip trailing `?` from LHS when the operator was `?->`
1490+
let lhs = call_body[..pos]
1491+
.strip_suffix('?')
1492+
.unwrap_or(&call_body[..pos]);
14861493
let method_name = &call_body[pos + 2..];
14871494

14881495
let lhs_classes = Self::resolve_target_classes(lhs, AccessKind::Arrow, ctx);
@@ -1518,7 +1525,10 @@ impl Backend {
15181525

15191526
// ── Property access: `$this->prop` or `$var->prop` ──────────────
15201527
if let Some(pos) = arg_text.rfind("->") {
1521-
let lhs = &arg_text[..pos];
1528+
// Strip trailing `?` from LHS when the operator was `?->`
1529+
let lhs = arg_text[..pos]
1530+
.strip_suffix('?')
1531+
.unwrap_or(&arg_text[..pos]);
15221532
let prop_name = &arg_text[pos + 2..];
15231533
if !prop_name.is_empty() && prop_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
15241534
let lhs_classes = Self::resolve_target_classes(lhs, AccessKind::Arrow, ctx);

0 commit comments

Comments
 (0)