|
55 | 55 | │ ├── mod.rs # Submodule declarations |
56 | 56 | │ ├── resolve.rs # Core go-to-definition resolution (classes, functions) |
57 | 57 | │ ├── 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) |
59 | 60 | build.rs # Parses PhpStormStubsMap.php, generates stub index |
60 | 61 | stubs/ # Composer vendor dir for jetbrains/phpstorm-stubs |
61 | 62 | tests/ |
62 | 63 | ├── common/mod.rs # Shared test helpers and minimal PHP stubs |
63 | 64 | ├── completion_*.rs # Completion integration tests (by feature area) |
64 | 65 | ├── definition_*.rs # Go-to-definition integration tests |
| 66 | +├── implementation.rs # Go-to-implementation integration tests |
65 | 67 | ├── docblock_*.rs # Docblock parsing and type tests |
66 | 68 | ├── parser.rs # PHP parser / AST extraction tests |
67 | 69 | ├── composer.rs # Composer integration tests |
@@ -248,6 +250,66 @@ When resolving a standalone function call (e.g. `app()`, `date_create()`), the l |
248 | 250 |
|
249 | 251 | This ensures that user-defined overrides or polyfills always win over built-in stubs. |
250 | 252 |
|
| 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 | + |
251 | 313 | ## Name Resolution |
252 | 314 |
|
253 | 315 | PHP class names go through resolution at parse time (`resolve_parent_class_names`): |
|
0 commit comments