Skip to content

Commit 74f29b6

Browse files
committed
Fix regressions
1 parent 13d7848 commit 74f29b6

12 files changed

Lines changed: 833 additions & 155 deletions

docs/todo.md

Lines changed: 23 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -653,66 +653,13 @@ These functions have return type semantics that don't fit into either
653653

654654
---
655655

656-
### 19. Array element access after method call on variable produces no completion
657-
**Impact: Medium · Effort: Low-Medium**
658-
659-
When a method returning `Pen[]` is called on a variable (not `$this`),
660-
array element access on the result produces no completions. In the
661-
`ArrayAccessDemo`:
662-
663-
```php
664-
$src = new ScaffoldingArrayAccess();
665-
$pens = $src->fetchAll(); // Pen[] from method return
666-
$pens[0]->write(); // no completions (should resolve to Pen)
667-
668-
$first = $pens[0];
669-
$first->color(); // no completions (should resolve to Pen)
670-
```
671-
672-
The same pattern works when the method is on `$this` (the demo worked
673-
before refactoring to `$src = new ...`). The variable resolution
674-
pipeline likely doesn't follow the chain `$src``ScaffoldingArrayAccess`
675-
`fetchAll()``Pen[]` → element type when `$src` is a local variable
676-
rather than `$this`.
677-
678-
---
679-
680-
### 20. Closure and arrow-function member completion fails in namespace context
681-
**Impact: Medium · Effort: Low-Medium**
682-
683-
None of the examples in `ClosureMembersDemo` produce completions.
684-
Closure literals (`function() { ... }`), arrow functions (`fn() => ...`),
685-
and chained `bindTo()` results should all resolve to the `Closure` class
686-
and offer members like `bindTo()` and `call()`.
687-
688-
The feature has passing unit tests (which define a minimal inline
689-
`Closure` class), so the failure is specific to the `example.php`
690-
context where `Closure` comes from stubs and the code is inside a
691-
`namespace Demo { }` block. The stub resolution or namespace-qualified
692-
lookup for the built-in `Closure` class may not be working in this
693-
context.
694-
695-
```php
696-
$typedClosure = function(Pen $p): string { return $p->write(); };
697-
$typedClosure->bindTo($this); // no completions (should offer Closure members)
698-
699-
$typedArrow = fn(int $x): float => $x * 1.5;
700-
$typedArrow->bindTo($this); // no completions
701-
702-
$fn = function(): void {};
703-
$bound = $fn->bindTo($this);
704-
$bound->call($this); // no completions
705-
```
706-
707-
---
708-
709656
<!-- ============================================================ -->
710657
<!-- TIER 4 — LOW-MEDIUM IMPACT -->
711658
<!-- ============================================================ -->
712659

713660
## Low-Medium Impact
714661

715-
### 21. Asymmetric visibility (PHP 8.4)
662+
### 19. Asymmetric visibility (PHP 8.4)
716663
**Impact: Low-Medium · Effort: Low**
717664

718665
Separate from property hooks, PHP 8.4 allows asymmetric visibility on
@@ -742,7 +689,7 @@ is just to store the value; context-aware filtering can follow later.
742689

743690
---
744691

745-
### 22. `str_contains` / `str_starts_with` / `str_ends_with` → non-empty-string narrowing
692+
### 20. `str_contains` / `str_starts_with` / `str_ends_with` → non-empty-string narrowing
746693
**Impact: Low-Medium · Effort: Low**
747694

748695
When `str_contains($haystack, $needle)` appears in a condition and
@@ -759,7 +706,7 @@ See `StrContainingTypeSpecifyingExtension` in PHPStan.
759706

760707
---
761708

762-
### 23. `count` / `sizeof` comparison → non-empty-array narrowing
709+
### 21. `count` / `sizeof` comparison → non-empty-array narrowing
763710
**Impact: Low-Medium · Effort: Low**
764711

765712
`if (count($arr) > 0)` or `if (count($arr) >= 1)` narrows `$arr` to
@@ -777,7 +724,7 @@ branches in `TypeSpecifier::specifyTypesInCondition`.
777724

778725
## Low Impact
779726

780-
### 24. Short-name collisions in `find_implementors`
727+
### 22. Short-name collisions in `find_implementors`
781728
**Impact: Low · Effort: Low**
782729

783730
`class_implements_or_extends` matches interfaces by both short name and
@@ -793,7 +740,7 @@ before comparison.
793740

794741
---
795742

796-
### 25. Fiber type resolution
743+
### 23. Fiber type resolution
797744
**Impact: Low · Effort: Low**
798745

799746
`Generator<TKey, TValue, TSend, TReturn>` has dedicated support for
@@ -808,7 +755,7 @@ Generator extraction in `docblock/types.rs`.
808755

809756
---
810757

811-
### 26. Non-empty-string propagation through string functions
758+
### 24. Non-empty-string propagation through string functions
812759
**Impact: Low · Effort: Low**
813760

814761
PHPStan tracks `non-empty-string` through string-manipulating
@@ -826,7 +773,7 @@ See `NonEmptyStringFunctionsReturnTypeExtension` in PHPStan.
826773

827774
---
828775

829-
### 27. `Closure::bind()` / `Closure::fromCallable()` return type preservation
776+
### 25. `Closure::bind()` / `Closure::fromCallable()` return type preservation
830777
**Impact: Low · Effort: Low-Medium**
831778

832779
Variables holding closure literals, arrow functions, and first-class
@@ -842,7 +789,7 @@ See `ClosureBindDynamicReturnTypeExtension` and
842789

843790
---
844791

845-
### 28. Remove deprecated text-search fallbacks
792+
### 26. Remove deprecated text-search fallbacks
846793
**Impact: Low · Effort: Medium**
847794

848795
The go-to-definition subsystem now uses the precomputed `SymbolMap` as
@@ -874,58 +821,7 @@ would let that deprecated function be removed entirely.
874821

875822
---
876823

877-
### 29. Go-to-definition jumps to itself at definition sites
878-
**Impact: Low · Effort: Low**
879-
880-
Ctrl+Click on a symbol at its own definition site offers GTD and jumps
881-
to itself instead of suppressing the action. Known cases:
882-
883-
1. **`define()` constant name.** `define('APP_VERSION', '1.0.0')` is
884-
the definition site for `APP_VERSION`. Ctrl+Click on the constant
885-
name inside the `define()` call jumps to itself.
886-
887-
2. **Method declarations.** `public function paramTypes(GtdAlpha $item): void {}`
888-
is the definition site for `paramTypes`. Ctrl+Click on the method
889-
name in the declaration jumps to itself.
890-
891-
The fix should detect that the cursor position matches (or is within)
892-
the stored definition offset and return `None` instead of a
893-
self-referential `Location`. This check could be a single guard at the
894-
top of the GTD handler: if the resolved `Location` points to the same
895-
URI and range that the cursor is already in, return `None`.
896-
897-
---
898-
899-
### 30. Return type hint completion missing leading space before type
900-
**Impact: Low · Effort: Low**
901-
902-
When completing a return type hint on a function that has no return type
903-
yet, the inserted text omits the space before the type name. For example,
904-
accepting `string` on:
905-
906-
```php
907-
function typeHintDemo(User $user, string $name) { return $user; }
908-
```
909-
910-
produces `:string` jammed against the closing parenthesis:
911-
912-
```php
913-
function typeHintDemo(User $user, string $name):string { return $user; }
914-
```
915-
916-
instead of the correct:
917-
918-
```php
919-
function typeHintDemo(User $user, string $name): string { return $user; }
920-
```
921-
922-
The fix should ensure the completion item's `textEdit` (or
923-
`additionalTextEdits`) includes the `: ` prefix with a space when
924-
inserting a return type after the parameter list.
925-
926-
---
927-
928-
### 31. Non-array functions with dynamic return types
824+
### 27. Non-array functions with dynamic return types
929825
**Impact: Low · Effort: High**
930826

931827
PHPStan also provides dynamic return type extensions for many non-array
@@ -956,22 +852,22 @@ return types (less impactful for class-based completion).
956852

957853
---
958854

959-
### 32. Diagnostics
855+
### 28. Diagnostics
960856
**Impact: Low (large scope) · Effort: Very High**
961857

962858
No error reporting (undefined methods, type mismatches, etc.).
963859

964860
---
965861

966-
### 33. Code Actions
862+
### 29. Code Actions
967863
**Impact: Low · Effort: Very High**
968864

969865
No quick fixes or refactoring suggestions. No `codeActionProvider` in
970866
`ServerCapabilities`, no `textDocument/codeAction` handler, and no
971867
`WorkspaceEdit` generation infrastructure beyond trivial `TextEdit`s for
972868
use-statement insertion.
973869

974-
#### 33a. Extract Function refactoring
870+
#### 29a. Extract Function refactoring
975871

976872
Select a range of statements inside a method/function and extract them into a
977873
new function. The LSP would need to:
@@ -1023,18 +919,14 @@ new function. The LSP would need to:
1023919
| 16 | Partial result streaming | Medium | Medium-High |
1024920
| 17 | Rename | Medium | Medium-High |
1025921
| 18 | Array functions (new code paths) | Medium | High |
1026-
| 19 | Array element access on variable method chain | Medium | Low-Medium |
1027-
| 20 | Closure member completion in namespace context | Medium | Low-Medium |
1028-
| 21 | Asymmetric visibility (PHP 8.4) | Low-Medium | Low |
1029-
| 22 | `str_contains` / `str_starts_with` narrowing | Low-Medium | Low |
1030-
| 23 | `count` / `sizeof` → non-empty-array | Low-Medium | Low |
1031-
| 24 | Short-name collisions | Low | Low |
1032-
| 25 | Fiber type resolution | Low | Low |
1033-
| 26 | Non-empty-string propagation | Low | Low |
1034-
| 27 | `Closure::bind()` preservation | Low | Low-Medium |
1035-
| 28 | Remove deprecated text-search fallbacks | Low | Medium |
1036-
| 29 | GTD jumps to itself at definition sites | Low | Low |
1037-
| 30 | Return type hint missing leading space | Low | Low |
1038-
| 31 | Non-array dynamic return types | Low | High |
1039-
| 32 | Diagnostics | Low | Very High |
1040-
| 33 | Code Actions / Extract Function | Low | Very High |
922+
| 19 | Asymmetric visibility (PHP 8.4) | Low-Medium | Low |
923+
| 20 | `str_contains` / `str_starts_with` narrowing | Low-Medium | Low |
924+
| 21 | `count` / `sizeof` → non-empty-array | Low-Medium | Low |
925+
| 22 | Short-name collisions | Low | Low |
926+
| 23 | Fiber type resolution | Low | Low |
927+
| 24 | Non-empty-string propagation | Low | Low |
928+
| 25 | `Closure::bind()` preservation | Low | Low-Medium |
929+
| 26 | Remove deprecated text-search fallbacks | Low | Medium |
930+
| 27 | Non-array dynamic return types | Low | High |
931+
| 28 | Diagnostics | Low | Very High |
932+
| 29 | Code Actions / Extract Function | Low | Very High |

src/completion/handler.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ impl Backend {
499499
position: Position,
500500
) -> Option<CompletionResponse> {
501501
let partial_lower = th_ctx.partial.to_lowercase();
502+
let space_prefix = if th_ctx.needs_space_prefix { " " } else { "" };
502503
let mut items: Vec<CompletionItem> =
503504
crate::completion::type_hint_completion::PHP_NATIVE_TYPES
504505
.iter()
@@ -508,7 +509,7 @@ impl Backend {
508509
label: t.to_string(),
509510
kind: Some(CompletionItemKind::KEYWORD),
510511
detail: Some("PHP built-in type".to_string()),
511-
insert_text: Some(t.to_string()),
512+
insert_text: Some(format!("{}{}", space_prefix, t)),
512513
filter_text: Some(t.to_string()),
513514
sort_text: Some(format!("0_{:03}", idx)),
514515
..CompletionItem::default()
@@ -523,7 +524,26 @@ impl Backend {
523524
ClassNameContext::Any,
524525
position,
525526
);
526-
items.extend(class_items);
527+
528+
// When a leading space is needed (return type after `:` with no
529+
// space), prefix the insert text of each class-name item so that
530+
// the result is `: ClassName` rather than `:ClassName`.
531+
if th_ctx.needs_space_prefix {
532+
for mut item in class_items {
533+
if let Some(ref txt) = item.insert_text {
534+
item.insert_text = Some(format!(" {}", txt));
535+
}
536+
if let Some(CompletionTextEdit::Edit(ref te)) = item.text_edit {
537+
item.text_edit = Some(CompletionTextEdit::Edit(TextEdit {
538+
range: te.range,
539+
new_text: format!(" {}", te.new_text),
540+
}));
541+
}
542+
items.push(item);
543+
}
544+
} else {
545+
items.extend(class_items);
546+
}
527547

528548
if items.is_empty() {
529549
// Even when empty, the caller returns early so we don't fall

src/completion/text_resolution.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ impl Backend {
258258
// Detect before the call expression branch because `fn(...)`
259259
// ends with `)` and would otherwise be resolved as a call.
260260
if rhs_text.ends_with("(...)") && rhs_text.len() > 4 {
261-
return Some("Closure".to_string());
261+
return Some("\\Closure".to_string());
262262
}
263263

264264
// RHS is a call expression — extract the return type.
@@ -282,7 +282,18 @@ impl Backend {
282282
{
283283
rest.contains("->") || rest.contains("::")
284284
} else {
285-
true
285+
// Single-level `$var->method` (bare variable followed
286+
// by exactly one `->`) should NOT be treated as a
287+
// chain — it needs variable resolution first, which
288+
// `resolve_raw_type_from_call_chain` cannot do (it
289+
// lacks `content`/`cursor_offset`). All other
290+
// single-arrow patterns (e.g.
291+
// `(new Foo())->method`) are genuine chains.
292+
let lhs = callee.split("->").next().unwrap_or("");
293+
let lhs = lhs.trim();
294+
let is_bare_var = lhs.starts_with('$')
295+
&& lhs[1..].chars().all(|c| c.is_alphanumeric() || c == '_');
296+
!is_bare_var
286297
}
287298
};
288299
let is_static_chain = !callee.contains("->") && callee.contains("::") && {
@@ -311,6 +322,38 @@ impl Backend {
311322
return Self::resolve_method_return_type(owner, method_name, class_loader);
312323
}
313324

325+
// Method call on a non-`$this` variable: `$var->methodName(…)`
326+
// Resolve the variable's type via assignment scanning, then
327+
// look up the method on the resulting class.
328+
if let Some(arrow_pos) = callee.find("->") {
329+
let var_part = callee[..arrow_pos].trim();
330+
let method_name = callee[arrow_pos + 2..].trim();
331+
if var_part.starts_with('$')
332+
&& var_part != "$this"
333+
&& !method_name.is_empty()
334+
&& method_name.chars().all(|c| c.is_alphanumeric() || c == '_')
335+
&& let Some(var_type) = Self::extract_raw_type_from_assignment_text(
336+
var_part,
337+
content,
338+
cursor_offset,
339+
current_class,
340+
all_classes,
341+
class_loader,
342+
)
343+
{
344+
let clean = crate::docblock::types::clean_type(&var_type);
345+
let lookup = short_name(&clean);
346+
let owner_class = all_classes
347+
.iter()
348+
.find(|c| c.name == lookup)
349+
.cloned()
350+
.or_else(|| class_loader(&clean));
351+
if let Some(owner) = owner_class {
352+
return Self::resolve_method_return_type(&owner, method_name, class_loader);
353+
}
354+
}
355+
}
356+
314357
// Static call: `ClassName::methodName(…)`
315358
if let Some((class_part, method_part)) = callee.rsplit_once("::") {
316359
let resolved_class = if class_part == "self" || class_part == "static" {

0 commit comments

Comments
 (0)