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
+74-2Lines changed: 74 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,7 +8,7 @@ PHPantom is a language server that provides completion and go-to-definition for
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
-
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.
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.
@@ -145,6 +145,78 @@ Variable go-to-definition (`$var` → jump to definition) uses three layers:
145
145
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.
146
146
3.**Text-based search** (`resolve_variable_definition_text`): the original heuristic line scanner. Only activated when the AST parse fails.
147
147
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
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
+
148
220
## Symbol Resolution: `find_or_load_class`
149
221
150
222
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