Skip to content

Commit c062cac

Browse files
committed
Implement implementation call
1 parent 1342f93 commit c062cac

12 files changed

Lines changed: 2452 additions & 12 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ PHPantom focuses on completion and go-to-definition and aims to do them really w
1313
|---|---|---|---|---|---|
1414
| Completion ||||||
1515
| Go-to-definition ||||||
16+
| Go-to-implementation ||||||
1617
| `@mixin` completion || 💰 ||||
1718
| `@phpstan-assert` narrowing ||||| ⚠️ partial |
1819
| Conditional return types ||||||
@@ -47,6 +48,7 @@ PHPantom understands Composer projects out of the box:
4748
- **Classmap and file autoloading.** `autoload_classmap.php` and `autoload_files.php`.
4849
- **Embedded PHP stubs** from [phpstorm-stubs](https://github.com/JetBrains/phpstorm-stubs) bundled in the binary, no runtime downloads needed.
4950
- **`require_once` discovery.** Functions from required files are available for completion.
51+
- **Go-to-implementation.** Jump from an interface or abstract class to all concrete implementations. Scans open files, classmap, PSR-4 directories, and embedded stubs.
5052

5153
> [!IMPORTANT]
5254
> Run `composer install -o` (or `composer dump-autoload -o`) in your project to generate the optimized autoload files PHPantom needs for cross-file class resolution.

docs/ARCHITECTURE.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,15 @@ src/
5555
│ ├── mod.rs # Submodule declarations
5656
│ ├── resolve.rs # Core go-to-definition resolution (classes, functions)
5757
│ ├── member.rs # Member-access resolution (->method, ::$prop, ::CONST)
58-
│ └── variable.rs # Variable definition resolution ($var jump-to-definition)
58+
│ ├── variable.rs # Variable definition resolution ($var jump-to-definition)
59+
│ └── implementation.rs # Go-to-implementation (interface/abstract → concrete classes)
5960
build.rs # Parses PhpStormStubsMap.php, generates stub index
6061
stubs/ # Composer vendor dir for jetbrains/phpstorm-stubs
6162
tests/
6263
├── common/mod.rs # Shared test helpers and minimal PHP stubs
6364
├── completion_*.rs # Completion integration tests (by feature area)
6465
├── definition_*.rs # Go-to-definition integration tests
66+
├── implementation.rs # Go-to-implementation integration tests
6567
├── docblock_*.rs # Docblock parsing and type tests
6668
├── parser.rs # PHP parser / AST extraction tests
6769
├── composer.rs # Composer integration tests
@@ -248,6 +250,66 @@ When resolving a standalone function call (e.g. `app()`, `date_create()`), the l
248250

249251
This ensures that user-defined overrides or polyfills always win over built-in stubs.
250252

253+
## Go-to-Implementation: `find_implementors`
254+
255+
When the user invokes go-to-implementation on an interface or abstract class, PHPantom scans for concrete classes that implement or extend it. The scan runs five phases, each progressively wider:
256+
257+
```
258+
find_implementors("Cacheable", "App\\Contracts\\Cacheable")
259+
260+
├── Phase 1: ast_map (already-parsed classes)
261+
│ Iterates every ClassInfo in every file already in memory.
262+
│ Checks interfaces list and parent_class chain against the target.
263+
│ ↓ continue
264+
265+
├── Phase 2: class_index (FQN → URI entries not yet covered)
266+
│ Loads classes via class_loader for entries not seen in Phase 1.
267+
│ ↓ continue
268+
269+
├── Phase 3: classmap files (string pre-filter → parse)
270+
│ Collects unique file paths from the Composer classmap.
271+
│ Skips files already in ast_map.
272+
│ Reads each file's raw source and checks contains(target_short).
273+
│ Only matching files are parsed via parse_and_cache_file.
274+
│ Every class in a parsed file is checked (not just the classmap FQN).
275+
│ ↓ continue
276+
277+
├── Phase 4: embedded stubs (string pre-filter → lazy parse)
278+
│ Checks each stub's static source string for contains(target_short).
279+
│ Matching stubs are loaded via class_loader (parsed and cached).
280+
│ ↓ continue
281+
282+
├── Phase 5: PSR-4 directory walk (user code only)
283+
│ Recursively collects all .php files under every PSR-4 root.
284+
│ Skips files already covered by the classmap (Phase 3) or ast_map.
285+
│ Reads raw source, applies the same string pre-filter.
286+
│ Matching files are parsed via parse_and_cache_file.
287+
│ Discovers classes in projects without `composer dump-autoload -o`.
288+
│ ↓ done
289+
290+
└── Vec<ClassInfo> (concrete implementors only)
291+
```
292+
293+
### Phase 5 Scope: User Code Only (by design)
294+
295+
Phase 5 walks PSR-4 roots from `composer.json` (`autoload` and `autoload-dev`), **not** from `vendor/composer/autoload_psr4.php`. This means it only discovers classes in the user's own source directories (e.g. `src/`, `app/`, `tests/`), not in vendor dependencies.
296+
297+
This is intentional. Vendor dependencies are managed by Composer and don't change during development — they are fully covered by the classmap (`composer dump-autoload -o`). The user's own files, on the other hand, change constantly and may not be in the classmap yet. Phase 5 exists specifically to catch those newly-created or not-yet-indexed user classes.
298+
299+
Do not "fix" this by adding vendor PSR-4 roots to the Phase 5 walk — that would scan tens of thousands of vendor files on every go-to-implementation request for no benefit, since Phase 3 already covers them via the classmap.
300+
301+
### String Pre-Filter
302+
303+
Phases 3–5 avoid expensive parsing by first reading the raw file content and checking whether it contains the target class's short name. A file that doesn't mention `"Cacheable"` anywhere in its source can't possibly implement the `Cacheable` interface, so it's skipped without parsing. This keeps the scan fast even for large projects with thousands of files.
304+
305+
### Caching
306+
307+
`parse_and_cache_file` follows the same pattern as `find_or_load_class`: it parses the PHP file, resolves parent/interface names via `resolve_parent_class_names`, and stores the results in `ast_map`, `use_map`, and `namespace_map`. This means files discovered during a go-to-implementation scan are immediately available for subsequent completion, definition, and implementation lookups without re-parsing.
308+
309+
### Member-Level Implementation
310+
311+
When the cursor is on a method call (e.g. `$repo->find()`), `resolve_member_implementations` first resolves the subject to candidate classes. If any candidate is an interface or abstract class, `find_implementors` is called and each implementor is checked for the specific method. Only classes that directly define (override) the method are returned — inherited-but-not-overridden methods are excluded.
312+
251313
## Name Resolution
252314

253315
PHP class names go through resolution at parse time (`resolve_parent_class_names`):

example.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,19 @@ function typeHintGtdReturn(): Response { return new Response(200); } // Ctrl+Cli
394394

395395
// Extends / implements — Ctrl+Click User, Renderable, or Loggable in scaffolding below.
396396

397+
398+
// ── Go-to-Implementation ────────────────────────────────────────────────────
399+
// Right-click → "Go to Implementations" (or editor shortcut) on an interface
400+
// or abstract class name to jump to all concrete classes that implement it.
401+
// Also works on method calls typed as an interface/abstract class.
402+
403+
// Try: Go-to-Implementation on "Renderable" → jumps to User and HtmlReport
404+
// Go-to-Implementation on "format" below → jumps to format() in each implementor
405+
function renderDemo(Renderable $item): string {
406+
return $item->format('<b>{name}</b>');
407+
}
408+
409+
397410
// Docblock type references — Ctrl+Click class names inside these annotations:
398411
/**
399412
* @param TypedCollection<int, User> $items Ctrl+Click User or TypedCollection
@@ -1259,6 +1272,19 @@ interface Loggable
12591272
public function log(string $message): void;
12601273
}
12611274

1275+
class HtmlReport implements Renderable
1276+
{
1277+
public function format(string $template): string
1278+
{
1279+
return '<div>' . $template . '</div>';
1280+
}
1281+
1282+
public function __toString(): string
1283+
{
1284+
return $this->format('report');
1285+
}
1286+
}
1287+
12621288
// ─── Traits ─────────────────────────────────────────────────────────────────
12631289

12641290
trait JsonSerializer {

0 commit comments

Comments
 (0)