@@ -57,6 +57,20 @@ struct VarResolutionCtx<'a> {
5757 function_loader : FunctionLoaderFn < ' a > ,
5858}
5959
60+ /// Bundles the common parameters threaded through call-expression
61+ /// return-type resolution.
62+ ///
63+ /// This keeps the argument count of [`resolve_call_return_types`] under
64+ /// clippy's `too_many_arguments` threshold.
65+ struct CallResolutionCtx < ' a > {
66+ current_class : Option < & ' a ClassInfo > ,
67+ all_classes : & ' a [ ClassInfo ] ,
68+ content : & ' a str ,
69+ cursor_offset : u32 ,
70+ class_loader : & ' a dyn Fn ( & str ) -> Option < ClassInfo > ,
71+ function_loader : FunctionLoaderFn < ' a > ,
72+ }
73+
6074impl Backend {
6175 /// Determine which class (if any) the completion subject refers to.
6276 ///
@@ -129,6 +143,21 @@ impl Backend {
129143 return vec ! [ ] ;
130144 }
131145
146+ // ── Enum case / static member access: `ClassName::CaseName` ──
147+ // When an enum case or static member is used with `->`, resolve to
148+ // the class/enum itself (e.g. `Status::Active->label()` → `Status`).
149+ if !subject. starts_with ( '$' )
150+ && subject. contains ( "::" )
151+ && !subject. ends_with ( ')' )
152+ && let Some ( ( class_part, _case_part) ) = subject. split_once ( "::" )
153+ {
154+ let lookup = class_part. rsplit ( '\\' ) . next ( ) . unwrap_or ( class_part) ;
155+ if let Some ( cls) = all_classes. iter ( ) . find ( |c| c. name == lookup) {
156+ return vec ! [ cls. clone( ) ] ;
157+ }
158+ return class_loader ( class_part) . into_iter ( ) . collect ( ) ;
159+ }
160+
132161 // ── Bare class name (for `::` or `->` from `new ClassName()`) ──
133162 if !subject. starts_with ( '$' )
134163 && !subject. contains ( "->" )
@@ -150,14 +179,15 @@ impl Backend {
150179 if subject. ends_with ( ')' )
151180 && let Some ( ( call_body, args_text) ) = split_call_subject ( subject)
152181 {
153- return Self :: resolve_call_return_types (
154- call_body,
155- args_text,
182+ let ctx = CallResolutionCtx {
156183 current_class,
157184 all_classes,
185+ content,
186+ cursor_offset,
158187 class_loader,
159188 function_loader,
160- ) ;
189+ } ;
190+ return Self :: resolve_call_return_types ( call_body, args_text, & ctx) ;
161191 }
162192
163193 // ── Property-chain: $this->prop or $this?->prop ──
@@ -177,18 +207,38 @@ impl Backend {
177207
178208 // ── Variable like `$var` — resolve via assignments / parameter hints ──
179209 if subject. starts_with ( '$' ) {
180- if let Some ( cc) = current_class {
181- return Self :: resolve_variable_types (
182- subject,
183- cc,
184- all_classes,
185- content,
186- cursor_offset,
187- class_loader,
188- function_loader,
189- ) ;
190- }
191- return vec ! [ ] ;
210+ // When the cursor is inside a class, use the enclosing class
211+ // for `self`/`static` resolution in type hints. When in
212+ // top-level code (`current_class` is `None`), use a dummy
213+ // empty class so that assignment scanning still works.
214+ let dummy_class;
215+ let effective_class = match current_class {
216+ Some ( cc) => cc,
217+ None => {
218+ dummy_class = ClassInfo {
219+ name : String :: new ( ) ,
220+ methods : vec ! [ ] ,
221+ properties : vec ! [ ] ,
222+ constants : vec ! [ ] ,
223+ start_offset : 0 ,
224+ end_offset : 0 ,
225+ parent_class : None ,
226+ used_traits : vec ! [ ] ,
227+ mixins : vec ! [ ] ,
228+ is_final : false ,
229+ } ;
230+ & dummy_class
231+ }
232+ } ;
233+ return Self :: resolve_variable_types (
234+ subject,
235+ effective_class,
236+ all_classes,
237+ content,
238+ cursor_offset,
239+ class_loader,
240+ function_loader,
241+ ) ;
192242 }
193243
194244 vec ! [ ]
@@ -209,11 +259,12 @@ impl Backend {
209259 fn resolve_call_return_types (
210260 call_body : & str ,
211261 text_args : & str ,
212- current_class : Option < & ClassInfo > ,
213- all_classes : & [ ClassInfo ] ,
214- class_loader : & dyn Fn ( & str ) -> Option < ClassInfo > ,
215- function_loader : FunctionLoaderFn < ' _ > ,
262+ ctx : & CallResolutionCtx < ' _ > ,
216263 ) -> Vec < ClassInfo > {
264+ let current_class = ctx. current_class ;
265+ let all_classes = ctx. all_classes ;
266+ let class_loader = ctx. class_loader ;
267+ let function_loader = ctx. function_loader ;
217268 // ── Instance method call: $this->method / $var->method ──
218269 if let Some ( pos) = call_body. rfind ( "->" ) {
219270 let lhs = & call_body[ ..pos] ;
@@ -235,14 +286,7 @@ impl Backend {
235286 // `$this->getFactory()->create(…)`).
236287 // Recursively resolve it.
237288 if let Some ( ( inner_body, inner_args) ) = split_call_subject ( lhs) {
238- Self :: resolve_call_return_types (
239- inner_body,
240- inner_args,
241- current_class,
242- all_classes,
243- class_loader,
244- function_loader,
245- )
289+ Self :: resolve_call_return_types ( inner_body, inner_args, ctx)
246290 } else {
247291 vec ! [ ]
248292 }
@@ -253,8 +297,23 @@ impl Backend {
253297 current_class
254298 . map ( |cc| Self :: resolve_property_types ( prop, cc, all_classes, class_loader) )
255299 . unwrap_or_default ( )
300+ } else if lhs. starts_with ( '$' ) {
301+ // Bare variable like `$profile` — resolve its type via
302+ // assignment scanning so that chains like
303+ // `$profile->getUser()->getEmail()` work in both
304+ // class-method and top-level contexts.
305+ Self :: resolve_target_classes (
306+ lhs,
307+ AccessKind :: Arrow ,
308+ ctx. current_class ,
309+ ctx. all_classes ,
310+ ctx. content ,
311+ ctx. cursor_offset ,
312+ ctx. class_loader ,
313+ ctx. function_loader ,
314+ )
256315 } else {
257- // Could be a variable — for now, skip complex chains
316+ // Unknown LHS form — skip
258317 vec ! [ ]
259318 } ;
260319
@@ -536,7 +595,11 @@ impl Backend {
536595 statements : impl Iterator < Item = & ' b Statement < ' b > > ,
537596 ctx : & VarResolutionCtx < ' _ > ,
538597 ) -> Vec < ClassInfo > {
539- for stmt in statements {
598+ // Collect so we can iterate twice: once to check class bodies,
599+ // once (if needed) to walk top-level statements.
600+ let stmts: Vec < & Statement > = statements. collect ( ) ;
601+
602+ for & stmt in & stmts {
540603 match stmt {
541604 Statement :: Class ( class) => {
542605 let start = class. left_brace . start . offset ;
@@ -580,7 +643,13 @@ impl Backend {
580643 _ => { }
581644 }
582645 }
583- vec ! [ ]
646+
647+ // The cursor is not inside any class/interface/enum body — it must
648+ // be in top-level code. Walk all top-level statements to find
649+ // variable assignments (e.g. `$user = new User(…);`).
650+ let mut results: Vec < ClassInfo > = Vec :: new ( ) ;
651+ Self :: walk_statements_for_assignments ( stmts. into_iter ( ) , ctx, & mut results, false ) ;
652+ results
584653 }
585654
586655 /// Resolve a variable's type by scanning class-like members for parameter
@@ -1163,13 +1232,16 @@ impl Backend {
11631232 {
11641233 let current_class =
11651234 all_classes. iter ( ) . find ( |c| c. name == current_class_name) ;
1166- let resolved = Self :: resolve_call_return_types (
1167- call_body,
1168- args_text,
1235+ let call_ctx = CallResolutionCtx {
11691236 current_class,
11701237 all_classes,
1238+ content,
1239+ cursor_offset : ctx. cursor_offset ,
11711240 class_loader,
11721241 function_loader,
1242+ } ;
1243+ let resolved = Self :: resolve_call_return_types (
1244+ call_body, args_text, & call_ctx,
11731245 ) ;
11741246 push_results ( results, resolved, conditional) ;
11751247 }
0 commit comments