Skip to content

Commit 6bcf1ba

Browse files
committed
Refactor signature helper to use SymbolMap
1 parent d7d77db commit 6bcf1ba

7 files changed

Lines changed: 1573 additions & 117 deletions

File tree

docs/ARCHITECTURE.md

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ PHPantom is a language server that provides completion and go-to-definition for
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.
11-
3. **Building** a precomputed symbol map (`symbol_maps`) during parsing for O(log n) go-to-definition lookups.
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.
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

@@ -24,7 +24,7 @@ src/
2424
├── stubs.rs # Embedded phpstorm-stubs (build-time generated index)
2525
├── resolution.rs # Multi-phase class/function lookup and name resolution
2626
├── inheritance.rs # Base class inheritance merging (traits, parent chain)
27-
├── symbol_map.rs # Precomputed per-file symbol location map (SymbolSpan, VarDefSite, SymbolMap)
27+
├── symbol_map.rs # Precomputed per-file symbol location map (SymbolSpan, VarDefSite, CallSite, SymbolMap)
2828
├── virtual_members/
2929
│ ├── mod.rs # VirtualMemberProvider trait, VirtualMembers struct, merge logic
3030
│ ├── laravel.rs # LaravelModelProvider (relationships, scopes, casts, accessors)
@@ -145,6 +145,78 @@ Variable go-to-definition (`$var` → jump to definition) uses three layers:
145145
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.
146146
3. **Text-based search** (`resolve_variable_definition_text`): the original heuristic line scanner. Only activated when the AST parse fails.
147147

148+
## Signature Help Architecture
149+
150+
Signature help shows parameter hints when the cursor is inside the parentheses
151+
of a function or method call. Detection uses a **two-tier** strategy:
152+
153+
### Tier 1: Precomputed Call Sites (primary)
154+
155+
During `update_ast`, every call expression in the file is recorded as a
156+
`CallSite` in the `SymbolMap`. Each entry stores:
157+
158+
| Field | Purpose |
159+
|---|---|
160+
| `args_start` | Byte offset immediately after the opening `(` |
161+
| `args_end` | Byte offset of the closing `)` |
162+
| `call_expression` | The call target in `resolve_callable` format (see below) |
163+
| `comma_offsets` | Byte offsets of each top-level comma separator |
164+
165+
Call sites are emitted for all five AST call kinds: `Call::Function`,
166+
`Call::Method`, `Call::NullSafeMethod`, `Call::StaticMethod`, and
167+
`Expression::Instantiation`. The `call_expression` string is built by
168+
`expr_to_subject_text`, which recursively converts the AST expression into
169+
the text format the resolver expects:
170+
171+
- `"functionName"` for standalone function calls
172+
- `"$subject->method"` for instance and null-safe method calls
173+
- `"ClassName::method"` for static method calls
174+
- `"new ClassName"` for constructor calls
175+
176+
When a signature help request arrives, `detect_call_site_from_map` converts
177+
the LSP position to a byte offset and searches the `call_sites` vec in
178+
reverse for the innermost entry whose argument range contains the cursor.
179+
The active parameter index is computed by counting how many precomputed
180+
comma offsets fall before the cursor. This handles nesting, strings
181+
containing commas/parens, and arbitrary chain depth correctly because the
182+
offsets come from the parser's token stream.
183+
184+
### Tier 2: Text-Based Fallback
185+
186+
When the symbol map has no matching call site (typically because the parser
187+
could not recover the call node from incomplete code, e.g. an unclosed `(`
188+
while the user is typing), the text-based scanner
189+
`detect_call_site_text_fallback` fires. It walks backward from the cursor
190+
to find an unmatched `(`, extracts the call expression with simple
191+
character-level scanning, and counts top-level commas. This path handles
192+
simple calls reliably but cannot resolve property chains, method return
193+
chains, or expressions containing balanced parentheses.
194+
195+
If the text fallback also fails to resolve the callable (e.g. because the
196+
class context is missing from the broken AST), a content-patching strategy
197+
inserts `);` at the cursor position and re-parses the file to recover
198+
class context for resolution.
199+
200+
### Call Expression Resolution
201+
202+
`resolve_callable` dispatches on the call expression format:
203+
204+
1. **`new ClassName`** — loads the class via `class_loader`, finds
205+
`__construct`. Returns an empty parameter list when no constructor
206+
is defined.
207+
2. **`$subject->method`** — splits at the last `->`, resolves the subject
208+
via `resolve_target_classes` (which handles `$this`, variables,
209+
property chains, call return chains, array access, etc.), then looks
210+
up the method on the resolved class.
211+
3. **`ClassName::method`** — resolves `self`/`static`/`parent` keywords
212+
and bare class names via `class_loader`, with a fallback to
213+
`resolve_target_classes` for class-string variables
214+
(e.g. `$cls = Pen::class; $cls::make()`).
215+
4. **`functionName`** — resolves via `resolve_function_name` (use map,
216+
namespace, stubs). Falls back to first-class callable resolution:
217+
if the expression is a `$variable`, scans backward for
218+
`$var = target(...)` and recursively resolves the underlying target.
219+
148220
## Symbol Resolution: `find_or_load_class`
149221

150222
When the LSP needs to resolve a class name (e.g. during completion on `Iterator::` or when following a type hint), it calls `find_or_load_class`. This method tries four phases in order, returning as soon as one succeeds:

0 commit comments

Comments
 (0)