Skip to content

Commit 58b4b65

Browse files
committed
Fix template substitution for nested class-string<T> arguments
1 parent f0a41f0 commit 58b4b65

6 files changed

Lines changed: 40 additions & 31 deletions

File tree

src/completion/call_resolution.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1137,11 +1137,24 @@ impl Backend {
11371137
let binding_mode = classify_template_binding(tpl_name, param_hint);
11381138

11391139
match binding_mode {
1140-
TemplateBindingMode::Direct | TemplateBindingMode::GenericWrapper(..) => {
1140+
TemplateBindingMode::Direct => {
11411141
if let Some(resolved_type) = Self::resolve_arg_text_to_type(arg_text, ctx) {
11421142
subs.insert(tpl_name.clone(), resolved_type);
11431143
}
11441144
}
1145+
TemplateBindingMode::GenericWrapper(ref wrapper_name, tpl_position) => {
1146+
if let Some(resolved_type) = Self::resolve_arg_text_to_type(arg_text, ctx) {
1147+
// Special handling for class-string<T> to avoid double-wrapping
1148+
if wrapper_name == "class-string"
1149+
&& tpl_position == 0
1150+
&& let Some(inner) = resolved_type.unwrap_class_string_inner()
1151+
{
1152+
subs.insert(tpl_name.clone(), inner.clone());
1153+
continue;
1154+
}
1155+
subs.insert(tpl_name.clone(), resolved_type);
1156+
}
1157+
}
11451158
TemplateBindingMode::CallableReturnType => {
11461159
// `@param callable(...): T $cb` — extract the closure's
11471160
// return type annotation from the argument text.

src/completion/types/conditional.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,7 @@ pub fn split_text_args(text: &str) -> Vec<&str> {
338338
let mut in_double_quote = false;
339339
let mut prev_was_backslash = false;
340340

341-
let mut chars = text.char_indices().peekable();
342-
while let Some((i, ch)) = chars.next() {
341+
for (i, ch) in text.char_indices() {
343342
if prev_was_backslash {
344343
prev_was_backslash = false;
345344
continue;

src/completion/variable/rhs_resolution.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,9 +1106,18 @@ pub(crate) fn build_function_template_subs(
11061106
subs.insert(tpl_name.clone(), concrete);
11071107
continue;
11081108
}
1109-
// Fall back to direct resolution for non-array wrappers
1110-
// or when raw type extraction fails.
1111-
if let Some(resolved_type) = Backend::resolve_arg_text_to_type(arg_text, rctx) {
1109+
// Special case: unwrap class-string<class-string<T>> to class-string<T>
1110+
if wrapper_name == "class-string" && tpl_position == 0 {
1111+
if let Some(resolved_type) = Backend::resolve_arg_text_to_type(arg_text, rctx) {
1112+
if let Some(inner) = resolved_type.unwrap_class_string_inner() {
1113+
subs.insert(tpl_name.clone(), inner.clone());
1114+
} else {
1115+
subs.insert(tpl_name.clone(), resolved_type);
1116+
}
1117+
}
1118+
} else if let Some(resolved_type) =
1119+
Backend::resolve_arg_text_to_type(arg_text, rctx)
1120+
{
11121121
subs.insert(tpl_name.clone(), resolved_type);
11131122
}
11141123
}

src/php_type.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,14 @@ impl PhpType {
15211521
}
15221522
}
15231523

1524+
/// If this is a `class-string<T>`, returns `Some(&T)`. Otherwise, returns `None`.
1525+
pub fn unwrap_class_string_inner(&self) -> Option<&PhpType> {
1526+
match self {
1527+
PhpType::ClassString(Some(inner)) => Some(inner.as_ref()),
1528+
_ => None,
1529+
}
1530+
}
1531+
15241532
/// Like [`all_members_scalar`] but uses the narrow
15251533
/// [`is_primitive_scalar`] check.
15261534
///

tests/unit/conditional_split_args.rs

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,39 +29,19 @@ fn test_split_text_args_double_quoted_string_with_comma() {
2929
#[test]
3030
fn test_split_text_args_nested_quotes_and_escapes() {
3131
let args = split_text_args(r#""a,\"b,c\",d", 'x,\'y,z\'', foo)"#);
32-
assert_eq!(
33-
args,
34-
vec![
35-
r#""a,\"b,c\",d""#,
36-
" 'x,\\'y,z\\''",
37-
" foo)"
38-
]
39-
);
32+
assert_eq!(args, vec![r#""a,\"b,c\",d""#, " 'x,\\'y,z\\''", " foo)"]);
4033
}
4134

4235
#[test]
4336
fn test_split_text_args_mixed_quotes_and_brackets() {
4437
let args = split_text_args(r#"array('a,b', ["x,y"]), "foo,bar""#);
45-
assert_eq!(
46-
args,
47-
vec![
48-
r#"array('a,b', ["x,y"])"#,
49-
r#" "foo,bar""#
50-
]
51-
);
38+
assert_eq!(args, vec![r#"array('a,b', ["x,y"])"#, r#" "foo,bar""#]);
5239
}
5340

5441
#[test]
5542
fn test_split_text_args_escaped_quotes() {
5643
let args = split_text_args(r#"'foo\,bar', "baz\"qux", plain"#);
57-
assert_eq!(
58-
args,
59-
vec![
60-
r#"'foo\,bar'"#,
61-
r#" "baz\"qux""#,
62-
" plain"
63-
]
64-
);
44+
assert_eq!(args, vec![r#"'foo\,bar'"#, r#" "baz\"qux""#, " plain"]);
6545
}
6646

6747
#[test]
@@ -71,4 +51,4 @@ fn test_split_text_args_empty_and_whitespace() {
7151

7252
let args = split_text_args("");
7353
assert_eq!(args, Vec::<&str>::new());
74-
}
54+
}

tests/unit/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
mod composer;
2+
mod conditional_split_args;
23
mod docblock_parsing;
34
mod monorepo;
45
mod named_args_internals;
56
mod phpdoc_internals;
6-
mod conditional_split_args;

0 commit comments

Comments
 (0)