Skip to content

Commit ddec3d6

Browse files
committed
Implement Find All References
1 parent 24d8388 commit ddec3d6

28 files changed

Lines changed: 2493 additions & 456 deletions

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mago-syntax = "1.8"
1515
mago-database = "1.8"
1616
mago-span = "1.8"
1717
bumpalo = "3"
18+
ignore = "0.4"
1819

1920
[dev-dependencies]
2021
datatest-stable = "0.2"

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ PHPantom focuses on deep type intelligence. Here's how it compares:
2424
| Generator body types ||| 🚧 |||
2525
| Go-to-definition ||||||
2626
| Go-to-implementation | 🚧 |||||
27-
| Hover | 🚧 |||||
27+
| Hover | |||||
2828
| Signature help ||||||
29-
| Find references | |||||
30-
| Diagnostics | |||||
29+
| Find references | |||||
30+
| Diagnostics | 🚧 |||||
3131
| Rename / refactoring ||||||
3232
| Time to ready | **10 ms** | 1 min 25 s | 3 min 17 s | 15 min 39 s | 19 min 38 s |
3333
| RAM usage | **7 MB** | 520 MB | 3.9 GB | 498 MB | 2.0 GB |

docs/ARCHITECTURE.md

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,25 @@ This document explains how PHPantom resolves PHP symbols — classes, interfaces
44

55
## Overview
66

7-
PHPantom is a language server that provides completion and go-to-definition for PHP projects. It works by:
7+
PHPantom is a language server for PHP projects. It provides completion, go-to-definition, go-to-implementation, find references, hover, signature help, and diagnostics. It works by:
88

99
1. **Parsing** PHP files into lightweight `ClassInfo` / `FunctionInfo` structures (not a full AST — just the information needed for IDE features).
1010
2. **Caching** parsed results in an in-memory `ast_map` keyed by file URI.
1111
3. **Building** a precomputed symbol map (`symbol_maps`) during parsing for O(log n) go-to-definition lookups and call-site detection for signature help.
1212
4. **Resolving** symbols on demand through a multi-phase lookup chain.
1313
5. **Merging** inherited members (from parent classes, traits, interfaces, and mixins) at resolution time.
1414

15+
### Multi-file scanning
16+
17+
Most features (completion, go-to-definition, hover, signature help, diagnostics) use maps or known file names and never walk directories. Only two features do multi-file scanning:
18+
19+
- **Go-to-implementation** scans for concrete classes that implement an interface or extend an abstract class (see `find_implementors`). Walks PSR-4 source directories only.
20+
- **Find References** scans for all occurrences of a symbol across the project (see `ensure_workspace_indexed`). Walks the entire workspace root.
21+
22+
Both features follow the same principle for vendor code: the Composer classmap is the source of truth. Vendor directories are never walked. User PSR-4 roots from `composer.json` are walked because user files change between `dump-autoload` runs.
23+
24+
The two walkers differ in scope because GTI only needs class declarations (which live in PSR-4 roots), while Find References needs any usage of a symbol, which could be in a standalone script, config file, or `index.php` at the project root.
25+
1526
## Module Layout
1627

1728
```
@@ -33,7 +44,7 @@ src/
3344
│ ├── laravel.rs # LaravelModelProvider (relationships, scopes, casts, accessors)
3445
│ └── phpdoc.rs # PHPDocProvider (@method, @property, @property-read, @property-write, @mixin)
3546
├── subject_extraction.rs # Shared helpers for extracting subjects before ->, ?->, ::
36-
├── util.rs # Position conversion, class lookup, logging
47+
├── util.rs # Position conversion, class lookup, logging, directory walkers (collect_php_files, collect_php_files_gitignore)
3748
├── parser/
3849
│ ├── mod.rs # Top-level parse entry points (parse_php, parse_functions, …)
3950
│ ├── classes.rs # Class, interface, trait, enum, and anonymous class extraction
@@ -95,6 +106,13 @@ src/
95106
│ │ ├── var_definition.rs # AST walk finding variable definition sites
96107
│ │ └── type_hint.rs # AST walk extracting type hints at definition sites
97108
│ └── implementation.rs # Go-to-implementation (interface/abstract → concrete classes)
109+
├── references/
110+
│ ├── mod.rs # Find References handler: same-file and cross-file symbol scanning
111+
│ └── tests.rs # Unit tests for find-references
112+
├── diagnostics/
113+
│ ├── mod.rs # Diagnostic collection and publishing (skips vendor files)
114+
│ ├── deprecated.rs # @deprecated usage diagnostics (strikethrough)
115+
│ └── unused_imports.rs # Unused use-statement dimming
98116
build.rs # Parses PhpStormStubsMap.php, generates stub index
99117
stubs/ # Composer vendor dir for jetbrains/phpstorm-stubs
100118
tests/
@@ -126,7 +144,8 @@ During `update_ast`, every navigable symbol occurrence in a file is recorded as
126144
| `Variable` | `$variable` tokens (both usage and definition sites) |
127145
| `FunctionCall` | Standalone function call names |
128146
| `SelfStaticParent` | `self`, `static`, `parent` keywords in navigable contexts |
129-
| `ConstantReference` | Constant names (reserved for future use) |
147+
| `ConstantReference` | Constant names (`define()` name, class constant access, standalone constant reference) |
148+
| `MemberDeclaration` | Method, property, or constant name at its declaration site (not navigable for go-to-definition, but needed for find-references) |
130149

131150
When a go-to-definition request arrives, `resolve_definition` converts the cursor position to a byte offset and does a binary search on the symbol map. If a `SymbolSpan` is found, it dispatches directly to the appropriate resolution path — no text scanning needed. If the offset falls in a gap (whitespace, string interior, comment interior, etc.), the request is instantly rejected.
132151

@@ -238,20 +257,25 @@ When the LSP needs to resolve a class name (e.g. during completion on `Iterator:
238257
```
239258
find_or_load_class("Iterator")
240259
241-
├── Phase 0: class_index (FQN → URI)
242-
│ Fast lookup for classes indexed by fully-qualified name.
243-
│ Handles classes that don't follow PSR-4 (e.g. Composer autoload_files).
244-
│ ↓ miss
245-
246260
├── Phase 1: ast_map scan
247-
│ Searches all already-parsed files by short class name.
261+
│ Searches all already-parsed files by short class name + namespace.
248262
│ This is where cached results from previous phases are found on
249-
│ subsequent lookups — a stub parsed in Phase 3 is cached here and
250-
│ found in Phase 1 next time.
263+
│ subsequent lookups — a classmap file parsed in Phase 1.5 or a
264+
│ stub parsed in Phase 3 is cached here and found in Phase 1
265+
│ next time.
266+
│ ↓ miss
267+
268+
├── Phase 1.5: Composer classmap (FQN → file path)
269+
│ Direct hash lookup in the classmap parsed from
270+
│ vendor/composer/autoload_classmap.php. More targeted than PSR-4
271+
│ and covers classes that don't follow PSR-4 conventions. When the
272+
│ user runs `composer dump-autoload -o`, *all* classes (including
273+
│ vendor) end up in the classmap, giving complete coverage.
251274
│ ↓ miss
252275
253-
├── Phase 2: PSR-4 resolution (user code)
254-
│ Uses Composer PSR-4 mappings to locate the file on disk.
276+
├── Phase 2: PSR-4 resolution
277+
│ Uses PSR-4 mappings from composer.json to locate the file on disk.
278+
│ These mappings only cover user code (vendor PSR-4 is not loaded).
255279
│ Example: "App\Models\User" → workspace/src/Models/User.php
256280
│ Reads, parses, resolves names, caches in ast_map.
257281
│ ↓ miss
@@ -269,6 +293,7 @@ find_or_load_class("Iterator")
269293

270294
Every phase that successfully parses a file caches the result in `ast_map`. This means:
271295

296+
- Phase 1.5 (classmap) files are parsed once, then found via Phase 1.
272297
- Phase 2 (PSR-4) files are parsed once, then found via Phase 1.
273298
- Phase 3 (stubs) are parsed once, then found via Phase 1.
274299
- Files opened in the editor are parsed on `didOpen`/`didChange` and always in Phase 1.
@@ -476,9 +501,12 @@ At resolution time, `merge_traits_into` loads the `UnitEnum` or `BackedEnum` stu
476501
`composer.rs` parses:
477502

478503
- `composer.json``autoload.psr-4` and `autoload-dev.psr-4` mappings
479-
- `vendor/composer/autoload_psr4.php` → vendor package mappings
480504

481-
These mappings are used by Phase 2 of `find_or_load_class` to locate PHP files on disk from fully-qualified class names.
505+
PSR-4 mappings come exclusively from the project's own `composer.json`. Vendor PSR-4 (`vendor/composer/autoload_psr4.php`) is not loaded. The Composer classmap is the sole source of truth for vendor code.
506+
507+
**Design principle:** if the classmap is missing or stale, vendor classes fail to resolve visibly rather than being silently papered over by PSR-4. This makes the problem obvious to the user (fix: run `composer dump-autoload`). User PSR-4 roots are walked by Go-to-implementation (Phase 5) and Find References because user files change between `dump-autoload` runs.
508+
509+
**Vendor dir detection:** the `config.vendor-dir` setting is read from `composer.json` once during `initialized` (via `parse_composer_json`, which returns both the PSR-4 mappings and the vendor dir name). The vendor dir name is cached on `Backend.vendor_dir_name` and a `file://` URI prefix is stored in `Backend.vendor_uri_prefix` for fast vendor-file detection at runtime.
482510

483511
### Autoload Files
484512

@@ -535,11 +563,15 @@ find_implementors("Cacheable", "App\\Contracts\\Cacheable")
535563

536564
### Phase 5 Scope: User Code Only (by design)
537565

538-
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.
566+
Phase 5 walks PSR-4 roots from `composer.json` (`autoload` and `autoload-dev`). Since PSR-4 mappings are sourced exclusively from the project's own `composer.json` (vendor PSR-4 is not loaded), Phase 5 inherently only discovers classes in the user's own source directories (e.g. `src/`, `app/`, `tests/`). Vendor dependencies are fully covered by the classmap (Phase 3).
539567

540-
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.
568+
Phase 5 exists to catch newly-created or not-yet-indexed user classes that are missing from the classmap (e.g. the user hasn't run `composer dump-autoload -o`).
541569

542-
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.
570+
Note: `collect_php_files` still receives the vendor dir name because a fallback mapping like `"" => "."` resolves to the workspace root, where the walk must skip the vendor directory (and hidden directories like `.git`).
571+
572+
### Known Limitation: Transitive Implementors
573+
574+
`find_implementors` currently misses classes that transitively implement the target through a concrete intermediate class. For example, if `BaseView implements Renderable` and `HtmlView extends BaseView`, only `BaseView` is found. PhpStorm finds both. See `todo-bugs.md` §4 for the fix plan.
543575

544576
### String Pre-Filter
545577

@@ -553,6 +585,28 @@ Phases 3–5 avoid expensive parsing by first reading the raw file content and c
553585

554586
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.
555587

588+
## Find References: `ensure_workspace_indexed`
589+
590+
When the user invokes "Find All References", PHPantom scans all user files for occurrences of the symbol. Vendor files are excluded (matching PhpStorm's behaviour).
591+
592+
### Indexing
593+
594+
Before scanning, `ensure_workspace_indexed` ensures all user files have symbol maps:
595+
596+
1. **Phase 1: class_index files (user only)** — files already known from `update_ast` calls. Vendor and stub URIs are skipped.
597+
2. **Phase 2: `.gitignore`-aware workspace walk** — uses the `ignore` crate's `WalkBuilder` to recursively discover PHP files under the workspace root, respecting `.gitignore` rules (including nested and global gitignore files). This automatically skips generated/cached directories like `storage/framework/views/` (Laravel blade cache), `var/cache/` (Symfony), and `node_modules/`. The vendor directory is always skipped regardless of `.gitignore` content. Hidden directories are skipped by default.
598+
599+
### Cross-file scanning
600+
601+
The `user_file_symbol_maps()` helper snapshots all symbol maps whose URI does not fall under the vendor directory or the internal stub scheme. Four scanners use this snapshot:
602+
603+
- `find_class_references` — matches `ClassReference` spans by resolved FQN
604+
- `find_member_references` — matches `MemberAccess` and `MemberDeclaration` spans by member name
605+
- `find_function_references` — matches `FunctionCall` spans by resolved FQN
606+
- `find_constant_references` — matches `ConstantReference` spans by name
607+
608+
Variable references (`$this`, local variables) are scoped to the enclosing function/class in the current file only, and do not use the cross-file scan.
609+
556610
## Union Type Completion (by design)
557611

558612
When a variable can hold one of several types (from match arms, ternary

docs/CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- **Find References.** "Find All References" locates every usage of a symbol across the project. Supports classes, interfaces, traits, enums, methods, properties, constants, functions, and variables. Variable references are scoped to the enclosing function or closure. Cross-file scanning lazily indexes user files on demand (vendor and stub files are excluded, matching PhpStorm's behaviour). The workspace walk respects `.gitignore` rules, so generated/cached directories (blade cache, Symfony `var/cache/`, `node_modules/`, etc.) are automatically skipped.
13+
1014
### Changed
1115

1216
- **Faster class resolution.** Fully-resolved classes (inheritance + virtual members) are now cached and reused across completion, hover, and go-to-definition within each request cycle. The cache is automatically cleared whenever a file changes, so results are never stale.
@@ -31,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3135

3236
### Fixed
3337

38+
- **Vendor class resolution simplified.** Vendor PSR-4 mappings (`vendor/composer/autoload_psr4.php`) are no longer loaded. The Composer classmap is the sole source of truth for vendor code. Go-to-definition now checks the classmap for vendor classes instead of relying on vendor PSR-4. If the classmap is missing or stale, vendor classes fail to resolve visibly instead of being silently papered over (fix: run `composer dump-autoload`). The `config.vendor-dir` setting is read once at startup and cached across all features.
3439
- **Named-argument resolution for non-variable subjects.** Named arguments now resolve correctly when the call target is a bare class name, a chain result, or a static method whose class name requires variable/chain resolution.
3540
- **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.
3641
- **`?->` null-safe chain resolution.** The `->` inside `?->` no longer confuses subject splitting across completion, go-to-definition, and signature help.

0 commit comments

Comments
 (0)