You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/ARCHITECTURE.md
+72-18Lines changed: 72 additions & 18 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,14 +4,25 @@ This document explains how PHPantom resolves PHP symbols — classes, interfaces
4
4
5
5
## Overview
6
6
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:
8
8
9
9
1.**Parsing** PHP files into lightweight `ClassInfo` / `FunctionInfo` structures (not a full AST — just the information needed for IDE features).
10
10
2.**Caching** parsed results in an in-memory `ast_map` keyed by file URI.
11
11
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.
12
12
4.**Resolving** symbols on demand through a multi-phase lookup chain.
13
13
5.**Merging** inherited members (from parent classes, traits, interfaces, and mixins) at resolution time.
14
14
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.
|`MemberDeclaration`| Method, property, or constant name at its declaration site (not navigable for go-to-definition, but needed for find-references) |
130
149
131
150
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.
132
151
@@ -238,20 +257,25 @@ When the LSP needs to resolve a class name (e.g. during completion on `Iterator:
238
257
```
239
258
find_or_load_class("Iterator")
240
259
│
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
-
│
246
260
├── 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.
248
262
│ 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
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.
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).
539
567
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`).
541
569
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.
543
575
544
576
### String Pre-Filter
545
577
@@ -553,6 +585,28 @@ Phases 3–5 avoid expensive parsing by first reading the raw file content and c
553
585
554
586
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.
555
587
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
+
556
610
## Union Type Completion (by design)
557
611
558
612
When a variable can hold one of several types (from match arms, ternary
Copy file name to clipboardExpand all lines: docs/CHANGELOG.md
+5Lines changed: 5 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
8
8
## [Unreleased]
9
9
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
+
10
14
### Changed
11
15
12
16
-**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
31
35
32
36
### Fixed
33
37
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.
34
39
-**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.
35
40
-**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.
36
41
-**`?->` null-safe chain resolution.** The `->` inside `?->` no longer confuses subject splitting across completion, go-to-definition, and signature help.
0 commit comments