@@ -581,13 +581,41 @@ impl Backend {
581581 // and push-style `$var[] = expr;` assignments.
582582 let base_entries = parse_array_literal_entries ( rhs_text) ;
583583
584+ // Extract spread element types from the array literal (e.g.
585+ // `[...$users, ...$admins]` → resolve each spread variable's
586+ // iterable element type).
587+ let spread_types = extract_spread_expressions ( rhs_text)
588+ . unwrap_or_default ( )
589+ . iter ( )
590+ . filter_map ( |expr| {
591+ // Only resolve bare variable expressions for now.
592+ if !expr. starts_with ( '$' ) {
593+ return None ;
594+ }
595+ let raw = self . resolve_variable_raw_type (
596+ expr,
597+ content,
598+ cursor_offset,
599+ classes,
600+ file_use_map,
601+ file_namespace,
602+ ) ?;
603+ docblock:: extract_iterable_element_type ( & raw )
604+ } )
605+ . collect :: < Vec < _ > > ( ) ;
606+
584607 // Scan for incremental `$var['key'] = expr;` assignments.
585608 let after_assign = assign_pos + assign_pattern. len ( ) + semi_pos + 1 ; // past the `;`
586609 let incremental =
587610 collect_incremental_key_assignments ( var_name, content, after_assign, cursor_offset) ;
588611
589612 // Scan for push-style `$var[] = expr;` assignments.
590- let push_types = collect_push_assignments ( var_name, content, after_assign, cursor_offset) ;
613+ let mut push_types =
614+ collect_push_assignments ( var_name, content, after_assign, cursor_offset) ;
615+
616+ // Merge spread element types into push types so they participate
617+ // in the `list<…>` inference.
618+ push_types. extend ( spread_types) ;
591619
592620 if base_entries. is_some ( ) || !incremental. is_empty ( ) || !push_types. is_empty ( ) {
593621 let mut entries: Vec < ( String , String ) > = base_entries. unwrap_or_default ( ) ;
@@ -1027,6 +1055,52 @@ pub(super) fn parse_array_literal_entries(rhs: &str) -> Option<Vec<(String, Stri
10271055 Some ( entries)
10281056}
10291057
1058+ /// Extract spread expressions from an array literal.
1059+ ///
1060+ /// Given an array literal like `[...$users, 'key' => 'val', ...$admins]`,
1061+ /// this returns `Some(vec!["$users", "$admins"])`.
1062+ ///
1063+ /// Only elements that start with `...` are collected. Keyed entries and
1064+ /// non-spread positional entries are ignored.
1065+ ///
1066+ /// Returns `None` if `rhs` is not an array literal, or `Some(vec![])` if
1067+ /// it is an array literal but contains no spread elements.
1068+ pub fn extract_spread_expressions ( rhs : & str ) -> Option < Vec < String > > {
1069+ let inner = if rhs. starts_with ( '[' ) && rhs. ends_with ( ']' ) {
1070+ & rhs[ 1 ..rhs. len ( ) - 1 ]
1071+ } else {
1072+ let lower = rhs. to_ascii_lowercase ( ) ;
1073+ if lower. starts_with ( "array(" ) && rhs. ends_with ( ')' ) {
1074+ & rhs[ 6 ..rhs. len ( ) - 1 ]
1075+ } else {
1076+ return None ;
1077+ }
1078+ } ;
1079+
1080+ let inner = inner. trim ( ) ;
1081+ if inner. is_empty ( ) {
1082+ return Some ( vec ! [ ] ) ;
1083+ }
1084+
1085+ let parts = split_array_literal_elements ( inner) ;
1086+ let mut spreads = Vec :: new ( ) ;
1087+
1088+ for part in & parts {
1089+ let part = part. trim ( ) ;
1090+ if part. is_empty ( ) {
1091+ continue ;
1092+ }
1093+ if let Some ( expr) = part. strip_prefix ( "..." ) {
1094+ let expr = expr. trim ( ) ;
1095+ if !expr. is_empty ( ) {
1096+ spreads. push ( expr. to_string ( ) ) ;
1097+ }
1098+ }
1099+ }
1100+
1101+ Some ( spreads)
1102+ }
1103+
10301104/// Split array literal elements on commas at depth 0, respecting
10311105/// `(…)`, `[…]`, `{…}`, `<…>` nesting and quoted strings.
10321106fn split_array_literal_elements ( s : & str ) -> Vec < & str > {
0 commit comments