|
1 | 1 | # PHPantom — Remaining Work |
2 | 2 |
|
3 | | -> Last updated: 2026-02-20 |
| 3 | +> Last updated: 2026-02-21 |
| 4 | +
|
| 5 | +--- |
| 6 | + |
| 7 | +## Completion Gaps |
| 8 | + |
| 9 | +### 16. Multi-line method chain completion |
| 10 | +**Priority: High** |
| 11 | + |
| 12 | +Subject extraction in `completion/target.rs` and `subject_extraction.rs` |
| 13 | +operates on the current line only. Chains that span multiple lines produce |
| 14 | +no completions: |
| 15 | + |
| 16 | +```php |
| 17 | +$this->getRepository() |
| 18 | + ->findAll() |
| 19 | + ->filter(fn($u) => $u->active) |
| 20 | + -> // ← cursor here: no completion |
| 21 | +``` |
| 22 | + |
| 23 | +`extract_completion_target` sees only whitespace and `->` on the cursor |
| 24 | +line and `extract_arrow_subject` finds nothing meaningful to the left. |
| 25 | +This is extremely common in Laravel/Symfony code with fluent builders, |
| 26 | +query chains, and collection pipelines. |
| 27 | + |
| 28 | +The same limitation affects go-to-definition: `extract_member_access_context` |
| 29 | +in `definition/member.rs` also works on the current line, so Ctrl-clicking |
| 30 | +a method in a multi-line chain cannot resolve the owning class. |
| 31 | + |
| 32 | +**Fix:** before extracting the subject, collapse continuation lines around |
| 33 | +the cursor. Lines that start with `->` or `?->` (after optional |
| 34 | +whitespace) should be joined with the preceding line, then the extraction |
| 35 | +runs on the flattened text. |
| 36 | + |
| 37 | +### 17. Switch statement variable type tracking |
| 38 | +**Priority: Medium** |
| 39 | + |
| 40 | +`walk_statements_for_assignments` in `completion/variable_resolution.rs` |
| 41 | +handles `If`, `Foreach`, `While`, `For`, `DoWhile`, `Try`, `Block`, and |
| 42 | +`Expression`, but `Statement::Switch` falls into the `_ => {}` catch-all |
| 43 | +and is silently skipped. Variables assigned inside switch cases are |
| 44 | +invisible to the type resolver: |
| 45 | + |
| 46 | +```php |
| 47 | +switch ($type) { |
| 48 | + case 'user': |
| 49 | + $result = new User(); |
| 50 | + break; |
| 51 | + case 'admin': |
| 52 | + $result = new Admin(); |
| 53 | + break; |
| 54 | +} |
| 55 | +$result-> // ← no completion |
| 56 | +``` |
| 57 | + |
| 58 | +The variable *name* collector in `completion/variable_completion.rs` |
| 59 | +already handles `Statement::Switch`, so `$result` appears in variable |
| 60 | +name suggestions, but its type is not resolved. |
| 61 | + |
| 62 | +**Fix:** add a `Statement::Switch` arm to `walk_statements_for_assignments` |
| 63 | +that iterates the switch cases and recurses into each case's statement |
| 64 | +list with `conditional = true` (same pattern as `if` branches). |
| 65 | + |
| 66 | +### 18. `?->` chaining loses intermediate segments |
| 67 | +**Priority: Medium** |
| 68 | + |
| 69 | +In `extract_arrow_subject` (`subject_extraction.rs`), when a `?->` is |
| 70 | +encountered mid-chain, the code calls `extract_simple_variable` instead |
| 71 | +of recursing with `extract_arrow_subject`. The `->` path recurses |
| 72 | +correctly, but `?->` does not: |
| 73 | + |
| 74 | +```php |
| 75 | +$user->getAddress()?->getCity()-> // extracts "$user?->getCity", loses "->getAddress()" |
| 76 | +``` |
| 77 | + |
| 78 | +**Fix:** change the `?->` branch to call `extract_arrow_subject(chars, inner_arrow)` |
| 79 | +instead of `extract_simple_variable(chars, inner_arrow)`, mirroring what |
| 80 | +the `->` branch does. |
| 81 | + |
| 82 | +### 19. `static` return type not resolved to concrete class at call sites |
| 83 | +**Priority: Medium** |
| 84 | + |
| 85 | +When a method declares `@return static` (common in builder/factory |
| 86 | +patterns), `type_hint_to_classes` resolves `static` to |
| 87 | +`owning_class_name` — the class that *declares* the method, not the |
| 88 | +class it is called on: |
| 89 | + |
| 90 | +```php |
| 91 | +class Builder { |
| 92 | + /** @return static */ |
| 93 | + public function configure(): static { return $this; } |
| 94 | +} |
| 95 | +class AppBuilder extends Builder {} |
| 96 | + |
| 97 | +$builder = new AppBuilder(); |
| 98 | +$builder->configure()-> // resolves to Builder, not AppBuilder |
| 99 | +``` |
| 100 | + |
| 101 | +The resolution works correctly when the subject is `$this` or `self`, |
| 102 | +but when the method return type is `static` and the call is on a variable |
| 103 | +typed as a subclass, the declaring class is used instead of the |
| 104 | +variable's concrete type. |
| 105 | + |
| 106 | +**Fix:** when `resolve_method_return_types_with_args` encounters a |
| 107 | +`static` (or `$this`) return type, substitute the caller's class name |
| 108 | +(the class the subject resolved to) rather than the class that declares |
| 109 | +the method. |
| 110 | + |
| 111 | +### 15. `unset()` tracking |
| 112 | +**Priority: Medium** |
| 113 | + |
| 114 | +`unset($var)` removes a variable from scope, and `unset($arr['key'])` removes |
| 115 | +a key from an array shape. Neither is tracked today. |
| 116 | + |
| 117 | +- **Variable scope.** After `unset($x)`, the variable `$x` should no longer |
| 118 | + appear in variable name suggestions, and `$x->` should not resolve to the |
| 119 | + type it had before the `unset`. |
| 120 | +- **Array shape keys.** After `unset($config['host'])`, the key `host` should |
| 121 | + no longer appear in `$config['` key completions, and the inferred shape |
| 122 | + should reflect its removal. |
| 123 | + |
| 124 | +Both cases require the assignment/variable scanner in |
| 125 | +`completion/variable_resolution.rs` to recognise `unset(...)` statements |
| 126 | +and update its tracking accordingly. |
| 127 | + |
| 128 | +### 20. Non-`$this` property access in text-based assignment path |
| 129 | +**Priority: Low** |
| 130 | + |
| 131 | +In `extract_raw_type_from_assignment_text` (`completion/resolver.rs`), |
| 132 | +property access on the RHS is only handled for `$this->propName`. When |
| 133 | +the RHS is `$otherVar->propName`, it falls through to `None`: |
| 134 | + |
| 135 | +```php |
| 136 | +$user = getUser(); |
| 137 | +$address = $user->address; // text-based path returns None |
| 138 | +$address-> // ← no completion (unless the AST-based path catches it) |
| 139 | +``` |
| 140 | + |
| 141 | +The AST-based path in `resolve_rhs_expression` handles this correctly, |
| 142 | +so the gap only surfaces in the text-based fallback used for intermediate |
| 143 | +chained assignments and some edge cases. |
| 144 | + |
| 145 | +**Fix:** after the `$this->propName` check, add a branch that resolves |
| 146 | +`$var->propName` by first resolving `$var`'s type via |
| 147 | +`extract_raw_type_from_assignment_text` (recursively), then looking up |
| 148 | +the property on the resulting class. |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## Go-to-Definition Gaps |
| 153 | + |
| 154 | +### 21. No reverse jump: implementation → interface method declaration |
| 155 | +**Priority: Medium** |
| 156 | + |
| 157 | +Go-to-implementation lets you jump from an interface method to its concrete |
| 158 | +implementations, but there is no way to jump from a concrete implementation |
| 159 | +*back* to the interface or abstract method it satisfies. For example, |
| 160 | +clicking `handle()` in a class that `implements Handler` cannot jump to |
| 161 | +`Handler::handle()`. |
| 162 | + |
| 163 | +This would be a natural extension of `find_declaring_class` in |
| 164 | +`definition/member.rs`: when the cursor is on a method *definition* (not |
| 165 | +a call), check whether any implemented interface or parent abstract class |
| 166 | +declares a method with the same name, and offer that as a definition |
| 167 | +target. |
4 | 168 |
|
5 | 169 | --- |
6 | 170 |
|
@@ -34,27 +198,6 @@ before comparison. |
34 | 198 |
|
35 | 199 | --- |
36 | 200 |
|
37 | | -## Completion Gaps |
38 | | - |
39 | | -### 15. `unset()` tracking |
40 | | -**Priority: Medium** |
41 | | - |
42 | | -`unset($var)` removes a variable from scope, and `unset($arr['key'])` removes |
43 | | -a key from an array shape. Neither is tracked today. |
44 | | - |
45 | | -- **Variable scope.** After `unset($x)`, the variable `$x` should no longer |
46 | | - appear in variable name suggestions, and `$x->` should not resolve to the |
47 | | - type it had before the `unset`. |
48 | | -- **Array shape keys.** After `unset($config['host'])`, the key `host` should |
49 | | - no longer appear in `$config['` key completions, and the inferred shape |
50 | | - should reflect its removal. |
51 | | - |
52 | | -Both cases require the assignment/variable scanner in |
53 | | -`completion/variable_resolution.rs` to recognise `unset(...)` statements |
54 | | -and update its tracking accordingly. |
55 | | - |
56 | | ---- |
57 | | - |
58 | 201 | ## Missing LSP Features |
59 | 202 |
|
60 | 203 | ### 6. Hover (`textDocument/hover`) |
|
0 commit comments