Skip to content

Commit e982a64

Browse files
committed
Named Key Destructuring from Array Shapes
1 parent 3a20746 commit e982a64

3 files changed

Lines changed: 799 additions & 49 deletions

File tree

example.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,57 @@ public function demo(): void
937937
}
938938

939939

940+
// ── Named Key Destructuring from Array Shapes ───────────────────────────────
941+
942+
class DestructuringShapeDemo
943+
{
944+
/**
945+
* @return array{customer: Customer, order: Order, total: float}
946+
*/
947+
public function getInvoice(): array { return []; }
948+
949+
public function namedKeyFromMethodReturn(): void
950+
{
951+
// Try: $cust-> ← offers email, address (Customer members)
952+
['customer' => $cust, 'order' => $ord] = $this->getInvoice();
953+
$cust->email; // Customer from 'customer' key
954+
$ord->total; // Order from 'order' key
955+
}
956+
957+
public function namedKeyFromVariable(): void
958+
{
959+
/** @var array{user: User, profile: UserProfile, active: bool} $data */
960+
$data = getUnknownValue();
961+
962+
// Try: $person-> ← offers getName(), getEmail() (User members)
963+
['user' => $person, 'profile' => $prof] = $data;
964+
$person->getEmail(); // User from 'user' key
965+
$prof->getDisplayName(); // UserProfile from 'profile' key
966+
}
967+
968+
public function positionalFromShape(): void
969+
{
970+
/** @var array{User, Address} $pair */
971+
$pair = getUnknownValue();
972+
973+
// Try: $second-> ← offers city, format() (Address members)
974+
[$first, $second] = $pair;
975+
$first->getEmail(); // User (positional index 0)
976+
$second->format(); // Address (positional index 1)
977+
}
978+
979+
public function listSyntaxNamedKey(): void
980+
{
981+
/** @var array{recipe: Recipe, servings: int} $meal */
982+
$meal = getUnknownValue();
983+
984+
// Try: $r-> ← offers ingredients (Recipe members)
985+
list('recipe' => $r) = $meal;
986+
$r->ingredients; // Recipe from 'recipe' key
987+
}
988+
}
989+
990+
940991
// ═══════════════════════════════════════════════════════════════════════════
941992
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
942993
// ┃ SCAFFOLDING — Supporting definitions below this line. ┃

src/completion/variable_resolution.rs

Lines changed: 146 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)