@@ -4,6 +4,143 @@ use common::create_test_backend;
44use tower_lsp:: LanguageServer ;
55use tower_lsp:: lsp_types:: * ;
66
7+ /// Regression test: completion on `collect($var)->` resolves Collection members.
8+ ///
9+ /// The `collect()` helper returns `Collection` and the cursor is right
10+ /// after `->` — the subject is `collect($paymentOptions)` which is a
11+ /// standalone function call whose return type is a class.
12+ #[ tokio:: test]
13+ async fn test_completion_on_function_call_arrow ( ) {
14+ let backend = create_test_backend ( ) ;
15+
16+ let uri = Url :: parse ( "file:///collect_map.php" ) . unwrap ( ) ;
17+ let text = r#"<?php
18+
19+ class Collection {
20+ /** @return static */
21+ public function map(callable $callback): static {}
22+
23+ /** @return static */
24+ public function values(): static {}
25+
26+ /** @return mixed */
27+ public function first(): mixed {}
28+ }
29+
30+ /**
31+ * @return Collection
32+ */
33+ function collect($value = []): Collection
34+ {
35+ return new Collection($value);
36+ }
37+
38+ class PaymentOptionLocale {
39+ public bool $tokens_enabled;
40+ }
41+
42+ class PaymentService {
43+ public function getOptions(array $paymentOptions): void {
44+ $formattedOptions = collect($paymentOptions)->map(function (PaymentOptionLocale $optionLocale) {
45+ return [
46+ 'tokens_enabled' => $optionLocale->tokens_enabled,
47+ ];
48+ })->values();
49+ $formattedOptions->
50+ }
51+ }
52+ "# ;
53+
54+ let open_params = DidOpenTextDocumentParams {
55+ text_document : TextDocumentItem {
56+ uri : uri. clone ( ) ,
57+ language_id : "php" . to_string ( ) ,
58+ version : 1 ,
59+ text : text. to_string ( ) ,
60+ } ,
61+ } ;
62+ backend. did_open ( open_params) . await ;
63+
64+ // ── Test 1: completion after `$formattedOptions->` ──
65+ let target_line = text
66+ . lines ( )
67+ . enumerate ( )
68+ . find ( |( _, l) | l. trim ( ) == "$formattedOptions->" )
69+ . map ( |( i, _) | i)
70+ . expect ( "should find $formattedOptions-> line" ) ;
71+
72+ let completion_params = CompletionParams {
73+ text_document_position : TextDocumentPositionParams {
74+ text_document : TextDocumentIdentifier { uri : uri. clone ( ) } ,
75+ position : Position {
76+ line : target_line as u32 ,
77+ character : 28 ,
78+ } ,
79+ } ,
80+ work_done_progress_params : WorkDoneProgressParams :: default ( ) ,
81+ partial_result_params : PartialResultParams :: default ( ) ,
82+ context : None ,
83+ } ;
84+
85+ let result = backend. completion ( completion_params) . await . unwrap ( ) ;
86+ let items = match result {
87+ Some ( CompletionResponse :: Array ( items) ) => items,
88+ Some ( CompletionResponse :: List ( list) ) => list. items ,
89+ None => vec ! [ ] ,
90+ } ;
91+
92+ let labels: Vec < & str > = items. iter ( ) . map ( |i| i. label . as_str ( ) ) . collect ( ) ;
93+ assert ! (
94+ labels. iter( ) . any( |l| l. starts_with( "map" ) ) ,
95+ "Expected 'map' in completions for Collection, got: {:?}" ,
96+ labels
97+ ) ;
98+ assert ! (
99+ labels. iter( ) . any( |l| l. starts_with( "values" ) ) ,
100+ "Expected 'values' in completions for Collection, got: {:?}" ,
101+ labels
102+ ) ;
103+
104+ // ── Test 2: completion after `collect($paymentOptions)->` ──
105+ // The cursor is right after `->` before `map` on the chained call line.
106+ let chain_line = text
107+ . lines ( )
108+ . enumerate ( )
109+ . find ( |( _, l) | l. contains ( "collect($paymentOptions)->map(" ) )
110+ . map ( |( i, _) | i)
111+ . expect ( "should find collect()->map( line" ) ;
112+
113+ let chain_line_text = text. lines ( ) . nth ( chain_line) . unwrap ( ) ;
114+ let arrow_col = chain_line_text. find ( "->map(" ) . unwrap ( ) as u32 + 2 ; // after `->`
115+
116+ let completion_params2 = CompletionParams {
117+ text_document_position : TextDocumentPositionParams {
118+ text_document : TextDocumentIdentifier { uri } ,
119+ position : Position {
120+ line : chain_line as u32 ,
121+ character : arrow_col + 3 , // after `->map`
122+ } ,
123+ } ,
124+ work_done_progress_params : WorkDoneProgressParams :: default ( ) ,
125+ partial_result_params : PartialResultParams :: default ( ) ,
126+ context : None ,
127+ } ;
128+
129+ // Must not crash and should offer Collection members
130+ let result2 = backend. completion ( completion_params2) . await . unwrap ( ) ;
131+ let items2 = match result2 {
132+ Some ( CompletionResponse :: Array ( items) ) => items,
133+ Some ( CompletionResponse :: List ( list) ) => list. items ,
134+ None => vec ! [ ] ,
135+ } ;
136+ let labels2: Vec < & str > = items2. iter ( ) . map ( |i| i. label . as_str ( ) ) . collect ( ) ;
137+ assert ! (
138+ labels2. iter( ) . any( |l| l. starts_with( "map" ) ) ,
139+ "Expected 'map' in completions on chained collect()->, got: {:?}" ,
140+ labels2
141+ ) ;
142+ }
143+
7144// ─── Method Insert Text with Parameters ─────────────────────────────────────
8145
9146#[ tokio:: test]
0 commit comments