@@ -217,6 +217,15 @@ impl Backend {
217217 use_map : & HashMap < String , String > ,
218218 namespace : & Option < String > ,
219219 ) {
220+ // Collect type alias names from ALL classes in the file up-front.
221+ // A type alias defined on one class can be referenced from methods
222+ // in a different class in the same file, so we must skip all of
223+ // them to avoid mangling alias names into FQN form.
224+ let all_alias_names: Vec < String > = classes
225+ . iter ( )
226+ . flat_map ( |c| c. type_aliases . keys ( ) . cloned ( ) )
227+ . collect ( ) ;
228+
220229 for class in classes. iter_mut ( ) {
221230 if let Some ( ref parent) = class. parent_class {
222231 let resolved = Self :: resolve_name ( parent, use_map, namespace) ;
@@ -243,6 +252,71 @@ impl Backend {
243252 Self :: resolve_generics_type_args ( & mut class. extends_generics , use_map, namespace) ;
244253 Self :: resolve_generics_type_args ( & mut class. implements_generics , use_map, namespace) ;
245254 Self :: resolve_generics_type_args ( & mut class. use_generics , use_map, namespace) ;
255+
256+ // Resolve class-like names in method return types and property
257+ // type hints so that cross-file resolution works correctly.
258+ // For example, if a method returns `Country` and the file has
259+ // `use Luxplus\Core\Enums\Country`, the return type becomes
260+ // the FQN `Luxplus\Core\Enums\Country`.
261+ //
262+ // Template params and type alias names are excluded to avoid
263+ // mangling generic types and locally-defined type aliases.
264+ // We collect alias names from ALL classes in the file because
265+ // a type alias defined on one class may be referenced from a
266+ // method in a different class in the same file.
267+ let template_params = & class. template_params ;
268+ let skip_names: Vec < String > = template_params
269+ . iter ( )
270+ . cloned ( )
271+ . chain ( all_alias_names. iter ( ) . cloned ( ) )
272+ . collect ( ) ;
273+
274+ // Also resolve class-like names inside type alias definitions
275+ // so that `@phpstan-type ActiveUser User` where `User` is
276+ // imported via `use App\Models\User` becomes `App\Models\User`.
277+ // Skip imported aliases (`from:ClassName:OriginalName`) — those
278+ // are internal references, not type strings.
279+ for def in class. type_aliases . values_mut ( ) {
280+ if let Some ( rest) = def. strip_prefix ( "from:" )
281+ && let Some ( ( class_name, original) ) = rest. split_once ( ':' )
282+ {
283+ // Imported alias — resolve the class name portion.
284+ // Format: `from:ClassName:OriginalName`
285+ let resolved_class = Self :: resolve_name ( class_name, use_map, namespace) ;
286+ * def = format ! ( "from:{}:{}" , resolved_class, original) ;
287+ continue ;
288+ }
289+ let resolved = Self :: resolve_type_string ( def, use_map, namespace, & skip_names) ;
290+ if resolved != * def {
291+ * def = resolved;
292+ }
293+ }
294+
295+ for method in & mut class. methods {
296+ if let Some ( ref ret) = method. return_type {
297+ let resolved = Self :: resolve_type_string ( ret, use_map, namespace, & skip_names) ;
298+ if resolved != * ret {
299+ method. return_type = Some ( resolved) ;
300+ }
301+ }
302+ for param in & mut method. parameters {
303+ if let Some ( ref hint) = param. type_hint {
304+ let resolved =
305+ Self :: resolve_type_string ( hint, use_map, namespace, & skip_names) ;
306+ if resolved != * hint {
307+ param. type_hint = Some ( resolved) ;
308+ }
309+ }
310+ }
311+ }
312+ for prop in & mut class. properties {
313+ if let Some ( ref hint) = prop. type_hint {
314+ let resolved = Self :: resolve_type_string ( hint, use_map, namespace, & skip_names) ;
315+ if resolved != * hint {
316+ prop. type_hint = Some ( resolved) ;
317+ }
318+ }
319+ }
246320 }
247321 }
248322
@@ -277,6 +351,139 @@ impl Backend {
277351 }
278352 }
279353
354+ /// Resolve class-like identifiers within a type string to their
355+ /// fully-qualified forms.
356+ ///
357+ /// Walks through the type string token-by-token, identifies class-like
358+ /// identifiers (words that are not scalars, keywords, or template
359+ /// params), and resolves each one via `resolve_name`.
360+ ///
361+ /// Handles complex type strings including unions (`A|B`), intersections
362+ /// (`A&B`), nullable (`?A`), generics (`Collection<int, User>`), and
363+ /// array shapes (`array{name: string, user: User}`).
364+ ///
365+ /// # Examples
366+ /// - `"Country"` → `"Luxplus\\Core\\Enums\\Country"` (via use map)
367+ /// - `"?Country"` → `"?Luxplus\\Core\\Enums\\Country"`
368+ /// - `"Country|null"` → `"Luxplus\\Core\\Enums\\Country|null"`
369+ /// - `"Collection<int, User>"` → `"App\\Collection<int, App\\User>"`
370+ /// - `"T"` (template param) → `"T"` (unchanged)
371+ fn resolve_type_string (
372+ type_str : & str ,
373+ use_map : & HashMap < String , String > ,
374+ namespace : & Option < String > ,
375+ skip_names : & [ String ] ,
376+ ) -> String {
377+ // Keywords that should never be resolved as class names.
378+ const TYPE_KEYWORDS : & [ & str ] = & [
379+ "self" ,
380+ "static" ,
381+ "parent" ,
382+ "$this" ,
383+ "mixed" ,
384+ "object" ,
385+ "void" ,
386+ "never" ,
387+ "null" ,
388+ "true" ,
389+ "false" ,
390+ "class-string" ,
391+ "list" ,
392+ "non-empty-list" ,
393+ "non-empty-array" ,
394+ "positive-int" ,
395+ "negative-int" ,
396+ "non-empty-string" ,
397+ "numeric-string" ,
398+ "class" ,
399+ "callable" ,
400+ "key-of" ,
401+ "value-of" ,
402+ ] ;
403+
404+ let mut result = String :: with_capacity ( type_str. len ( ) ) ;
405+ let bytes = type_str. as_bytes ( ) ;
406+ let len = bytes. len ( ) ;
407+ let mut i = 0 ;
408+
409+ // Track brace depth so we can distinguish array shape keys
410+ // (identifiers before `:` inside `{…}`) from type names.
411+ let mut brace_depth: u32 = 0 ;
412+ // Whether we are in "key position" inside a shape (before the `:`).
413+ // Reset to true after each `,` or `{` at the current brace level.
414+ let mut in_shape_key = false ;
415+
416+ while i < len {
417+ let c = bytes[ i] as char ;
418+
419+ // Start of an identifier (letter, underscore, or backslash for FQN)
420+ if c. is_ascii_alphabetic ( ) || c == '_' || c == '\\' {
421+ let start = i;
422+ // Consume the full identifier including namespace separators
423+ while i < len
424+ && ( bytes[ i] . is_ascii_alphanumeric ( ) || bytes[ i] == b'_' || bytes[ i] == b'\\' )
425+ {
426+ i += 1 ;
427+ }
428+ let word = & type_str[ start..i] ;
429+
430+ // Inside `{…}` in key position, identifiers are array shape
431+ // keys (e.g. `name` in `array{name: string}`), not types.
432+ if brace_depth > 0 && in_shape_key {
433+ result. push_str ( word) ;
434+ continue ;
435+ }
436+
437+ let lower = word. to_ascii_lowercase ( ) ;
438+ if is_scalar ( word)
439+ || TYPE_KEYWORDS . contains ( & lower. as_str ( ) )
440+ || skip_names. iter ( ) . any ( |s| s == word)
441+ || word. starts_with ( '\\' )
442+ {
443+ // Leave as-is: scalar, keyword, template param,
444+ // type alias name, or already fully-qualified.
445+ result. push_str ( word) ;
446+ } else {
447+ result. push_str ( & Self :: resolve_name ( word, use_map, namespace) ) ;
448+ }
449+ } else if c == '$' {
450+ // Variable reference like `$this` — consume fully
451+ let start = i;
452+ i += 1 ;
453+ while i < len && ( bytes[ i] . is_ascii_alphanumeric ( ) || bytes[ i] == b'_' ) {
454+ i += 1 ;
455+ }
456+ result. push_str ( & type_str[ start..i] ) ;
457+ } else {
458+ // Track brace depth and key/value position for array shapes.
459+ match c {
460+ '{' => {
461+ brace_depth += 1 ;
462+ in_shape_key = true ;
463+ }
464+ '}' => {
465+ brace_depth = brace_depth. saturating_sub ( 1 ) ;
466+ in_shape_key = brace_depth > 0 ;
467+ }
468+ ':' if brace_depth > 0 => {
469+ // Colon separates key from value type — switch
470+ // to value position where identifiers ARE types.
471+ in_shape_key = false ;
472+ }
473+ ',' if brace_depth > 0 => {
474+ // Comma separates entries — next identifier is a key.
475+ in_shape_key = true ;
476+ }
477+ _ => { }
478+ }
479+ result. push ( c) ;
480+ i += 1 ;
481+ }
482+ }
483+
484+ result
485+ }
486+
280487 /// Resolve a class name to its fully-qualified form given a use_map and
281488 /// namespace context.
282489 fn resolve_name (
0 commit comments