Skip to content

Commit 0d8b4cb

Browse files
committed
Implement Go To Type
1 parent 7d4231b commit 0d8b4cb

13 files changed

Lines changed: 1549 additions & 43 deletions

File tree

docs/ARCHITECTURE.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,31 @@ Variable go-to-definition (`$var` → jump to definition) uses two layers:
193193
1. **Symbol map** (`var_defs`): the primary path. Finds the most recent `VarDefSite` before the cursor within the enclosing scope. When the cursor is physically on a definition token (parameter, foreach binding, catch variable), it returns `None` so the caller can fall through to type-hint resolution.
194194
2. **AST-based search** (`resolve_variable_definition_ast`): parses the file and walks the enclosing scope to find the definition site. Handles destructuring (`[$a, $b] = ...`, `list($a, $b) = ...`) and nested scopes correctly. Used as a fallback when the symbol map doesn't have a match. Returns `None` when the AST parse fails.
195195

196+
## Go-to-Type-Definition Architecture
197+
198+
"Go to Type Definition" (`textDocument/typeDefinition`) jumps from a symbol to the class declaration of its **resolved type**, rather than to the definition site. For example, if `$user` is typed as `User`, go-to-definition jumps to the `$user = ...` assignment, while go-to-type-definition jumps to `class User { … }`.
199+
200+
The handler (`definition/type_definition.rs`) reuses the same symbol-map lookup as go-to-definition and the same type-resolution pipelines as hover and completion:
201+
202+
1. **Symbol map lookup.** Convert the cursor position to a byte offset and binary-search the symbol map (same as go-to-definition, including the offset-1 retry for end-of-token cursors).
203+
204+
2. **Type resolution by symbol kind:**
205+
206+
| `SymbolKind` | Resolution path |
207+
|---|---|
208+
| `Variable` | `resolve_variable_type_string` (type-string path) with fallback to `resolve_variable_types` (ClassInfo path). `$this` resolves to the enclosing class. |
209+
| `MemberAccess` | `resolve_target_classes` finds the subject's class, then the method's `return_type` or the property's `type_hint` is extracted. `self`/`static`/`$this` in return types are replaced with the owning class name. |
210+
| `SelfStaticParent` | `self`/`static` resolve to the enclosing class; `parent` resolves to the parent class. |
211+
| `ClassReference` | The type is the class itself. |
212+
| `FunctionCall` | The function's `return_type` is extracted. |
213+
| `ClassDeclaration`, `MemberDeclaration`, `ConstantReference` | No type definition target; returns `None`. |
214+
215+
3. **Type string to class names.** `extract_class_names_from_type_string` splits union types at depth-0 `|` separators, strips `?` (nullable), generic parameters (`<…>`), array shapes (`{…}`), and trailing `[]`. Scalar types (`int`, `string`, `array`, `void`, `mixed`, `bool`, `float`, `null`, `false`, `true`, `never`, `callable`, `iterable`, `resource`, `object`) are excluded since they have no user-navigable declaration.
216+
217+
4. **Class name to location.** Each surviving class name is resolved via `resolve_class_reference` (the same function go-to-definition uses for class references), which tries same-file AST lookup, cross-file `class_index` + `ast_map`, Composer classmap, PSR-4, and template parameter fallback. Duplicate locations are deduplicated.
218+
219+
For union types, the handler returns multiple locations (one per non-scalar class in the union), which editors display as a peek list.
220+
196221
## Signature Help Architecture
197222

198223
Signature help shows parameter hints when the cursor is inside the parentheses

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
- **Interface template inheritance.** When a class implements an interface whose methods use `@template`, `@param class-string<T>`, or `@return T`, the implementing class's overridden methods now inherit the template parameters, bindings, conditional return types, and type assertions.
2929
- **Generic `@phpstan-assert` with `class-string<T>`.** `@phpstan-assert T $value` combined with a `@template T` bound via `class-string<T>` now resolves the narrowed type from the call-site argument. Also works on static method calls like `Assert::instanceOf($value, Foo::class)`.
3030
- **Property-level narrowing.** `if ($this->prop instanceof Foo)` now narrows the type of `$this->prop` inside the then-body, else-body, and after guard clauses. `assert($this->prop instanceof Foo)` also works. Previously only plain variables participated in instanceof narrowing.
31+
- **Go to Type Definition.** `textDocument/typeDefinition` jumps from a variable, property access, method call, or function call to the class declaration of its resolved type. For example, if `$user` is typed as `User`, go-to-definition jumps to the assignment while go-to-type-definition jumps to `class User`. Union types produce multiple locations (one per class). Scalar types are filtered out. Works across files via PSR-4 resolution.
3132
- **Inline `&&` short-circuit narrowing.** In `if ($x instanceof Foo && $x->bar())`, the right-hand side of `&&` now sees the narrowed type from the left-hand side. Previously narrowing only applied inside the if body, so method calls within the condition itself were checked against the original (broader) type.
3233
- **Compound negated guard clause narrowing.** After `if (!$x instanceof A && !$x instanceof B) { return; }`, the surviving code narrows `$x` to `A|B`. Previously only single negated instanceof guard clauses were recognized.
3334
- **Invoked closure and arrow function return types.** `(fn(): Foo => new Foo())()` and `(function(): Bar { return new Bar(); })()` now resolve to the return type of the closure or arrow function.

docs/todo.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ feature surface grows.
6262
| 20 | Workspace Symbols (`workspace/symbol`) | Low-Medium | LSP Features | [lsp-features.md §5](todo/lsp-features.md#5-workspace-symbols-workspacesymbol) |
6363
| 21 | Folding Ranges (`textDocument/foldingRange`) | Low | LSP Features | [lsp-features.md §12](todo/lsp-features.md#12-folding-ranges-textdocumentfoldingrange) |
6464
| 22 | Selection Ranges (`textDocument/selectionRange`) | Low | LSP Features | [lsp-features.md §13](todo/lsp-features.md#13-selection-ranges-textdocumentselectionrange) |
65-
| 23 | Type Definition (`textDocument/typeDefinition`) | Low | LSP Features | [lsp-features.md §14](todo/lsp-features.md#14-type-definition-textdocumenttypedefinition) |
6665
| 81 | Work-done progress for GTI and Find References | Low | LSP Features | [lsp-features.md §18](todo/lsp-features.md#18-work-done-progress-for-gti-and-find-references) |
6766
| 101 | Argument count diagnostic | Low | Diagnostics | [diagnostics.md §7](todo/diagnostics.md#7-argument-count-diagnostic) |
6867
| 105 | Merged classmap + self-scan | Low | Indexing | [indexing.md §1.5](todo/indexing.md#phase-15-merged-classmap--self-scan) |

docs/todo/lsp-features.md

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -412,39 +412,6 @@ then the full `if` block, then the enclosing method).
412412

413413
---
414414

415-
## 14. Type Definition (`textDocument/typeDefinition`)
416-
**Impact: Medium · Effort: Low**
417-
418-
"Go to Type Definition" jumps from a variable or expression to the
419-
class of its resolved type, rather than to the definition site. For
420-
example, if `$user` is typed as `User`, go-to-definition jumps to the
421-
`$user = ...` assignment, while go-to-type-definition jumps to the
422-
`User` class declaration.
423-
424-
The variable type resolution infrastructure already exists. The handler
425-
resolves the variable's type (or the expression type for property
426-
accesses and method calls), strips nullability and generic parameters,
427-
and looks up the class definition location.
428-
429-
**Implementation:**
430-
431-
1. **Register the capability** — set `type_definition_provider:
432-
Some(TypeDefinitionProviderCapability::Simple(true))` in
433-
`ServerCapabilities`.
434-
435-
2. **Handler:** Given a position:
436-
- If the symbol under the cursor is a variable, resolve its type via
437-
the existing variable resolution pipeline.
438-
- If it's a property access or method call, resolve the subject type.
439-
- Strip `?`, `|null`, and generic parameters from the resolved type
440-
string to get the base class name.
441-
- Look up the class via `find_or_load_class` and return its
442-
definition location.
443-
- For union types, return multiple locations (one per class in the
444-
union).
445-
446-
---
447-
448415
## 15. Document Links (`textDocument/documentLink`)
449416
**Impact: Low-Medium · Effort: Low**
450417

example.php

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
/**
44
* PHP Showcase
55
*
6-
* A single-file playground for every completion and go-to-definition feature.
7-
* Trigger completion after -> / :: / $, or Ctrl+Click for go-to-definition.
6+
* A single-file playground for every completion, go-to-definition, and
7+
* go-to-type-definition feature. Trigger completion after -> / :: / $,
8+
* Ctrl+Click for go-to-definition, or use "Go to Type Definition" to
9+
* jump to the class declaration of a variable's resolved type.
810
*
911
* Layout:
1012
* 1. DEMOS — open any demo() method and try completion inside it
@@ -1685,6 +1687,39 @@ class GtdNotFoundException extends \RuntimeException {}
16851687
class GtdAccessException extends \RuntimeException {}
16861688

16871689

1690+
// ── Go-to-Type-Definition ───────────────────────────────────────────────────
1691+
// "Go to Type Definition" jumps to the *type's* class declaration, not the
1692+
// variable's definition site. Compare with regular Go-to-Definition:
1693+
// • Go-to-Definition on $user → jumps to the $user = ... assignment
1694+
// • Go-to-Type-Definition on $user → jumps to class User { ... }
1695+
//
1696+
// Try: place the cursor on $target, $result, or $pet below and invoke
1697+
// "Go to Type Definition" (often bound to a secondary shortcut or
1698+
// right-click menu). Union types produce a peek list with all classes.
1699+
1700+
class GoToTypeDefinitionDemo
1701+
{
1702+
public function demo(): void
1703+
{
1704+
$target = new GtdTarget();
1705+
$target; // Type Definition → GtdTarget
1706+
1707+
$result = $this->getResult();
1708+
$result; // Type Definition → GtdResult
1709+
1710+
$pet = $this->getPet();
1711+
$pet; // Type Definition → GtdAlpha | GtdBeta (two locations)
1712+
1713+
$this; // Type Definition → GoToTypeDefinitionDemo
1714+
}
1715+
1716+
public function getResult(): GtdResult { return new GtdResult(); }
1717+
1718+
/** @return GtdAlpha|GtdBeta */
1719+
public function getPet(): GtdAlpha|GtdBeta { return new GtdAlpha(); }
1720+
}
1721+
1722+
16881723
// ── Go-to-Implementation ────────────────────────────────────────────────────
16891724
// All implementors are defined right after the demo so "Go to Implementations"
16901725
// lands within a few lines.

src/completion/source/helpers.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,19 @@ fn walk_array_segments_and_resolve(
348348
) -> Option<Vec<ClassInfo>> {
349349
let mut current_type = raw_type.to_string();
350350

351+
// Expand type aliases before walking segments. The raw type may
352+
// be an alias name like `UserData` that resolves to
353+
// `array{name: string, pen: Pen}`. Without expansion the
354+
// segment walk would fail to extract shape values.
355+
if let Some(expanded) = crate::completion::type_resolution::resolve_type_alias(
356+
&current_type,
357+
current_class_name,
358+
all_classes,
359+
class_loader,
360+
) {
361+
current_type = expanded;
362+
}
363+
351364
for seg in segments {
352365
match seg {
353366
BracketSegment::StringKey(key) => {
@@ -357,6 +370,19 @@ fn walk_array_segments_and_resolve(
357370
current_type = docblock::types::extract_generic_value_type(&current_type)?;
358371
}
359372
}
373+
374+
// After each segment, the resulting type might itself be an
375+
// alias (e.g. a shape value defined as another alias).
376+
// Expand again so the next segment (or the final resolution)
377+
// sees the concrete type.
378+
if let Some(expanded) = crate::completion::type_resolution::resolve_type_alias(
379+
&current_type,
380+
current_class_name,
381+
all_classes,
382+
class_loader,
383+
) {
384+
current_type = expanded;
385+
}
360386
}
361387

362388
let cleaned = docblock::clean_type(&current_type);

src/definition/mod.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
/// Goto definition and go-to-implementation support.
1+
/// Goto definition, go-to-implementation, and go-to-type-definition support.
22
///
3-
/// This module contains the logic for resolving "go to definition" and
4-
/// "go to implementation" requests, allowing users to jump from a symbol
5-
/// reference to its definition or concrete implementations.
3+
/// This module contains the logic for resolving "go to definition",
4+
/// "go to implementation", and "go to type definition" requests, allowing
5+
/// users to jump from a symbol reference to its definition, concrete
6+
/// implementations, or the declaration of its resolved type.
67
///
78
/// The [`point_location`] helper constructs a zero-width `Location`
89
/// (start == end), which is the standard shape for "go to definition"
@@ -23,6 +24,14 @@
2324
/// - **Method calls on interfaces/abstract classes**: jumps to the concrete
2425
/// method implementations in all implementing/extending classes
2526
///
27+
/// Supported symbols (type definition):
28+
/// - **Variables**: `$var` jumps to the class declaration of the resolved type
29+
/// - **Member access**: `$var->method()` jumps to the return type's class
30+
/// - **Properties**: `$var->prop` jumps to the property type's class
31+
/// - **`self`/`static`/`parent`/`$this`**: jumps to the enclosing or parent class
32+
/// - **Function calls**: `foo()` jumps to the return type's class
33+
/// - For union types, multiple locations are returned (one per class)
34+
///
2635
/// - [`resolve`]: Core entry points — word extraction, name resolution,
2736
/// same-file / PSR-4 definition lookup, `self`/`static`/`parent` handling,
2837
/// and standalone function definition resolution.
@@ -35,11 +44,15 @@
3544
/// - [`implementation`]: Go-to-implementation — finding concrete classes that
3645
/// implement an interface or extend an abstract class, and locating the
3746
/// concrete method definitions within those classes.
47+
/// - [`type_definition`]: Go-to-type-definition — resolving the type of a
48+
/// variable, expression, or member access, then jumping to the class
49+
/// declaration of that type.
3850
use tower_lsp::lsp_types::{Location, Position, Range, Url};
3951

4052
mod implementation;
4153
pub(crate) mod member;
4254
mod resolve;
55+
mod type_definition;
4356
mod variable;
4457

4558
/// Build an LSP `Location` with a zero-width range (start == end).

src/definition/resolve.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ impl Backend {
285285
/// When `is_fqn` is `true`, the name is already fully-qualified
286286
/// (the original PHP source used a leading `\`) and should be used
287287
/// as-is without namespace resolution.
288-
fn resolve_class_reference(
288+
pub(super) fn resolve_class_reference(
289289
&self,
290290
uri: &str,
291291
content: &str,

0 commit comments

Comments
 (0)