@@ -1048,16 +1048,20 @@ impl Backend {
10481048
10491049 /// Check whether the target variable appears inside an array/list
10501050 /// destructuring LHS and, if so, resolve its type from the RHS's
1051- /// generic element type.
1051+ /// generic element type or array shape entry .
10521052 ///
10531053 /// Supported patterns:
1054- /// - `[$a, $b] = getUsers()` — function call RHS
1055- /// - `list($a, $b) = $users` — variable RHS with `@var`/`@param`
1056- /// - `[$a, $b] = $this->m()` — method/static-method call RHS
1054+ /// - `[$a, $b] = getUsers()` — function call RHS (generic)
1055+ /// - `list($a, $b) = $users` — variable RHS with `@var`/`@param`
1056+ /// - `[$a, $b] = $this->m()` — method/static-method call RHS
1057+ /// - `['user' => $p] = $data` — named key from array shape
1058+ /// - `[0 => $first, 1 => $second] = $data` — numeric key from array shape
10571059 ///
1058- /// The element type is extracted from the raw return/annotation type
1059- /// via `extract_generic_value_type`, so `array<int, User>`, `list<User>`,
1060- /// `User[]`, etc. all work.
1060+ /// When the RHS type is an array shape (`array{key: Type, …}`), the
1061+ /// destructured variable's key is matched against the shape entries.
1062+ /// For positional (value-only) elements, the 0-based index is used as
1063+ /// the key. Falls back to `extract_generic_value_type` for generic
1064+ /// iterable types (`list<User>`, `array<int, User>`, `User[]`).
10611065 fn try_resolve_destructured_type < ' b > (
10621066 assignment : & ' b Assignment < ' b > ,
10631067 ctx : & VarResolutionCtx < ' _ > ,
@@ -1071,20 +1075,42 @@ impl Backend {
10711075 _ => return ,
10721076 } ;
10731077
1074- // ── 2. Check if our target variable is among the elements ───────
1078+ // ── 2. Find our target variable and extract its destructuring key
1079+ //
1080+ // For `KeyValue` elements like `'user' => $person`, extract the
1081+ // string/integer key. For positional `Value` elements, track
1082+ // the 0-based index so we can look up positional shape entries.
10751083 let var_name = ctx. var_name ;
1076- let found = elements. iter ( ) . any ( |elem| {
1077- let value_expr = match elem {
1078- ArrayElement :: Value ( val) => Some ( val. value ) ,
1079- ArrayElement :: KeyValue ( kv) => Some ( kv. value ) ,
1080- _ => None ,
1081- } ;
1082- if let Some ( Expression :: Variable ( Variable :: Direct ( dv) ) ) = value_expr {
1083- dv. name == var_name
1084- } else {
1085- false
1084+ let mut shape_key: Option < String > = None ;
1085+ let mut found = false ;
1086+ let mut positional_index: usize = 0 ;
1087+
1088+ for elem in elements. iter ( ) {
1089+ match elem {
1090+ ArrayElement :: KeyValue ( kv) => {
1091+ if let Expression :: Variable ( Variable :: Direct ( dv) ) = kv. value
1092+ && dv. name == var_name
1093+ {
1094+ found = true ;
1095+ // Extract the key from the LHS expression.
1096+ shape_key = Self :: extract_destructuring_key ( kv. key ) ;
1097+ break ;
1098+ }
1099+ }
1100+ ArrayElement :: Value ( val) => {
1101+ if let Expression :: Variable ( Variable :: Direct ( dv) ) = val. value
1102+ && dv. name == var_name
1103+ {
1104+ found = true ;
1105+ // Use the positional index as the shape key.
1106+ shape_key = Some ( positional_index. to_string ( ) ) ;
1107+ break ;
1108+ }
1109+ positional_index += 1 ;
1110+ }
1111+ _ => { }
10861112 }
1087- } ) ;
1113+ }
10881114 if !found {
10891115 return ;
10901116 }
@@ -1097,56 +1123,127 @@ impl Backend {
10971123 // ── 3. Try inline `/** @var … */` annotation ────────────────────
10981124 // Handles both:
10991125 // `/** @var list<User> */` (no variable name)
1100- // `/** @var array<int, User> $result */` (with variable name — rare )
1126+ // `/** @var array{user: User} $data */` (with variable name)
11011127 let stmt_offset = assignment. span ( ) . start . offset as usize ;
11021128 if let Some ( ( var_type, _var_name_opt) ) =
11031129 docblock:: find_inline_var_docblock ( content, stmt_offset)
1104- && let Some ( element_type) = docblock:: types:: extract_generic_value_type ( & var_type)
11051130 {
1106- let resolved = Self :: type_hint_to_classes (
1107- & element_type,
1108- current_class_name,
1109- all_classes,
1110- class_loader,
1111- ) ;
1112- if !resolved. is_empty ( ) {
1113- if !conditional {
1114- results. clear ( ) ;
1131+ if let Some ( ref key) = shape_key
1132+ && let Some ( entry_type) =
1133+ docblock:: types:: extract_array_shape_value_type ( & var_type, key)
1134+ {
1135+ let resolved = Self :: type_hint_to_classes (
1136+ & entry_type,
1137+ current_class_name,
1138+ all_classes,
1139+ class_loader,
1140+ ) ;
1141+ if !resolved. is_empty ( ) {
1142+ if !conditional {
1143+ results. clear ( ) ;
1144+ }
1145+ for cls in resolved {
1146+ if !results. iter ( ) . any ( |c| c. name == cls. name ) {
1147+ results. push ( cls) ;
1148+ }
1149+ }
1150+ return ;
11151151 }
1116- for cls in resolved {
1117- if !results. iter ( ) . any ( |c| c. name == cls. name ) {
1118- results. push ( cls) ;
1152+ }
1153+
1154+ if let Some ( element_type) = docblock:: types:: extract_generic_value_type ( & var_type) {
1155+ let resolved = Self :: type_hint_to_classes (
1156+ & element_type,
1157+ current_class_name,
1158+ all_classes,
1159+ class_loader,
1160+ ) ;
1161+ if !resolved. is_empty ( ) {
1162+ if !conditional {
1163+ results. clear ( ) ;
11191164 }
1165+ for cls in resolved {
1166+ if !results. iter ( ) . any ( |c| c. name == cls. name ) {
1167+ results. push ( cls) ;
1168+ }
1169+ }
1170+ return ;
11201171 }
1121- return ;
11221172 }
11231173 }
11241174
11251175 // ── 4. Try to extract the raw iterable type from the RHS ────────
11261176 let raw_type: Option < String > = Self :: extract_rhs_iterable_raw_type ( assignment. rhs , ctx) ;
11271177
1128- if let Some ( ref raw) = raw_type
1129- && let Some ( element_type) = docblock:: types:: extract_generic_value_type ( raw)
1130- {
1131- let resolved = Self :: type_hint_to_classes (
1132- & element_type,
1133- current_class_name,
1134- all_classes,
1135- class_loader,
1136- ) ;
1137- if !resolved. is_empty ( ) {
1138- if !conditional {
1139- results. clear ( ) ;
1178+ if let Some ( ref raw) = raw_type {
1179+ // First try array shape lookup with the destructured key.
1180+ if let Some ( ref key) = shape_key
1181+ && let Some ( entry_type) = docblock:: types:: extract_array_shape_value_type ( raw, key)
1182+ {
1183+ let resolved = Self :: type_hint_to_classes (
1184+ & entry_type,
1185+ current_class_name,
1186+ all_classes,
1187+ class_loader,
1188+ ) ;
1189+ if !resolved. is_empty ( ) {
1190+ if !conditional {
1191+ results. clear ( ) ;
1192+ }
1193+ for cls in resolved {
1194+ if !results. iter ( ) . any ( |c| c. name == cls. name ) {
1195+ results. push ( cls) ;
1196+ }
1197+ }
1198+ return ;
11401199 }
1141- for cls in resolved {
1142- if !results. iter ( ) . any ( |c| c. name == cls. name ) {
1143- results. push ( cls) ;
1200+ }
1201+
1202+ // Fall back to generic element type extraction.
1203+ if let Some ( element_type) = docblock:: types:: extract_generic_value_type ( raw) {
1204+ let resolved = Self :: type_hint_to_classes (
1205+ & element_type,
1206+ current_class_name,
1207+ all_classes,
1208+ class_loader,
1209+ ) ;
1210+ if !resolved. is_empty ( ) {
1211+ if !conditional {
1212+ results. clear ( ) ;
1213+ }
1214+ for cls in resolved {
1215+ if !results. iter ( ) . any ( |c| c. name == cls. name ) {
1216+ results. push ( cls) ;
1217+ }
11441218 }
11451219 }
11461220 }
11471221 }
11481222 }
11491223
1224+ /// Extract a string key from a destructuring key expression.
1225+ ///
1226+ /// Handles string literals (`'user'`, `"user"`) and integer literals
1227+ /// (`0`, `1`). Returns `None` for dynamic or unsupported key
1228+ /// expressions.
1229+ fn extract_destructuring_key ( key_expr : & Expression < ' _ > ) -> Option < String > {
1230+ match key_expr {
1231+ Expression :: Literal ( Literal :: String ( lit_str) ) => {
1232+ // `value` strips the quotes; fall back to `raw` trimmed.
1233+ lit_str. value . map ( |v| v. to_string ( ) ) . or_else ( || {
1234+ let raw = lit_str. raw ;
1235+ // Strip surrounding quotes from the raw representation.
1236+ raw. strip_prefix ( '\'' )
1237+ . and_then ( |s| s. strip_suffix ( '\'' ) )
1238+ . or_else ( || raw. strip_prefix ( '"' ) . and_then ( |s| s. strip_suffix ( '"' ) ) )
1239+ . map ( |s| s. to_string ( ) )
1240+ } )
1241+ }
1242+ Expression :: Literal ( Literal :: Integer ( lit_int) ) => Some ( lit_int. raw . to_string ( ) ) ,
1243+ _ => None ,
1244+ }
1245+ }
1246+
11501247 /// Extract the raw iterable type string from an RHS expression.
11511248 ///
11521249 /// Returns the type annotation string (e.g. `"array<int, User>"`,
0 commit comments