Skip to content

Commit f0a41f0

Browse files
committed
Refactor argument splitting for template substitution
1 parent e567582 commit f0a41f0

9 files changed

Lines changed: 185 additions & 53 deletions

File tree

src/completion/call_resolution.rs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
/// return types and template substitutions.
1919
/// - [`Backend::build_method_template_subs`]: builds a template
2020
/// substitution map for method-level `@template` parameters from
21-
/// call-site argument text.
21+
/// pre-split call-site argument texts.
2222
use std::collections::HashMap;
2323
use std::sync::Arc;
2424

@@ -374,8 +374,10 @@ impl Backend {
374374

375375
let mut results = Vec::new();
376376
for owner in &lhs_classes {
377+
let split_args = split_text_args(text_args);
378+
let arg_refs = split_args.to_vec();
377379
let template_subs =
378-
Self::build_method_template_subs(owner, method_name, text_args, ctx);
380+
Self::build_method_template_subs(owner, method_name, &arg_refs, ctx);
379381
let var_resolver = build_var_resolver(ctx);
380382
let mr_ctx = MethodReturnCtx {
381383
all_classes: ctx.all_classes,
@@ -414,8 +416,10 @@ impl Backend {
414416
};
415417

416418
if let Some(ref owner) = owner_class {
419+
let split_args = split_text_args(text_args);
420+
let arg_refs = split_args.to_vec();
417421
let template_subs =
418-
Self::build_method_template_subs(owner, method_name, text_args, ctx);
422+
Self::build_method_template_subs(owner, method_name, &arg_refs, ctx);
419423
let var_resolver = build_var_resolver(ctx);
420424
let mr_ctx = MethodReturnCtx {
421425
all_classes: ctx.all_classes,
@@ -519,8 +523,14 @@ impl Backend {
519523
&& func_info.return_type.is_some()
520524
&& !text_args.is_empty()
521525
{
526+
let split_args: Vec<String> = split_text_args(text_args)
527+
.into_iter()
528+
.map(|s| s.to_string())
529+
.collect();
522530
let subs = super::variable::rhs_resolution::build_function_template_subs(
523-
&func_info, text_args, ctx,
531+
&func_info,
532+
&split_args,
533+
ctx,
524534
);
525535

526536
if !subs.is_empty()
@@ -1046,16 +1056,21 @@ impl Backend {
10461056
/// Build a template substitution map for a method-level `@template` call.
10471057
///
10481058
/// Finds the method on the class (or inherited), checks for template
1049-
/// params and bindings, resolves argument types from `text_args` using
1050-
/// the call resolution context, and returns a `HashMap` mapping template
1051-
/// parameter names to their resolved concrete types.
1059+
/// params and bindings, resolves argument types from the pre-split
1060+
/// `arg_texts` slice using the call resolution context, and returns a
1061+
/// `HashMap` mapping template parameter names to their resolved
1062+
/// concrete types.
1063+
///
1064+
/// Callers with an AST `ArgumentList` should extract per-argument text
1065+
/// via [`extract_arg_texts_from_ast`] and convert to `&[&str]`.
1066+
/// Callers with only raw text should use [`split_text_args`] first.
10521067
///
10531068
/// Returns an empty map if the method has no template params, no
10541069
/// bindings, or if argument types cannot be resolved.
10551070
pub(super) fn build_method_template_subs(
10561071
class_info: &ClassInfo,
10571072
method_name: &str,
1058-
text_args: &str,
1073+
arg_texts: &[&str],
10591074
ctx: &ResolutionCtx<'_>,
10601075
) -> HashMap<String, PhpType> {
10611076
// Find the method — first on the class directly, then via inheritance.
@@ -1082,7 +1097,6 @@ impl Backend {
10821097
_ => return HashMap::new(),
10831098
};
10841099

1085-
let args = split_text_args(text_args);
10861100
let mut subs = HashMap::new();
10871101

10881102
for (tpl_name, param_name) in &method.template_bindings {
@@ -1093,7 +1107,7 @@ impl Backend {
10931107
};
10941108

10951109
// Get the corresponding argument text.
1096-
let arg_text = match args.get(param_idx) {
1110+
let arg_text = match arg_texts.get(param_idx) {
10971111
Some(text) => text.trim(),
10981112
None => {
10991113
// No argument was provided at the call site. If the

src/completion/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ pub(crate) mod use_edit;
8484
pub(crate) mod context;
8585
pub mod phpdoc;
8686
pub(crate) mod source;
87-
pub(crate) mod types;
87+
pub mod types;
8888
pub(crate) mod variable;
8989

9090
// ─── Backward-compatible re-exports ─────────────────────────────────────────

src/completion/types/conditional.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,15 +330,40 @@ fn condition_includes_array(condition: &PhpType) -> bool {
330330

331331
/// Split a textual argument list by commas, respecting nested parentheses
332332
/// so that `"foo(a, b), c"` splits into `["foo(a, b)", "c"]`.
333-
pub(crate) fn split_text_args(text: &str) -> Vec<&str> {
333+
pub fn split_text_args(text: &str) -> Vec<&str> {
334334
let mut result = Vec::new();
335335
let mut depth = 0u32;
336336
let mut start = 0;
337-
for (i, ch) in text.char_indices() {
337+
let mut in_single_quote = false;
338+
let mut in_double_quote = false;
339+
let mut prev_was_backslash = false;
340+
341+
let mut chars = text.char_indices().peekable();
342+
while let Some((i, ch)) = chars.next() {
343+
if prev_was_backslash {
344+
prev_was_backslash = false;
345+
continue;
346+
}
338347
match ch {
339-
'(' | '[' => depth += 1,
340-
')' | ']' => depth = depth.saturating_sub(1),
341-
',' if depth == 0 => {
348+
'\\' => {
349+
// Only treat as escape if inside a quote
350+
if in_single_quote || in_double_quote {
351+
prev_was_backslash = true;
352+
}
353+
}
354+
'\'' if !in_double_quote => {
355+
in_single_quote = !in_single_quote;
356+
}
357+
'"' if !in_single_quote => {
358+
in_double_quote = !in_double_quote;
359+
}
360+
'(' | '[' if !in_single_quote && !in_double_quote => {
361+
depth += 1;
362+
}
363+
')' | ']' if !in_single_quote && !in_double_quote => {
364+
depth = depth.saturating_sub(1);
365+
}
366+
',' if depth == 0 && !in_single_quote && !in_double_quote => {
342367
result.push(&text[start..i]);
343368
start = i + 1; // skip the comma
344369
}

src/completion/types/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
/// - **narrowing**: instanceof / assert / custom type guard narrowing
77
/// - **conditional**: PHPStan conditional return type resolution at call sites
88
pub mod conditional;
9-
pub(crate) mod narrowing;
10-
pub(crate) mod resolution;
9+
pub mod narrowing;
10+
pub mod resolution;

src/completion/variable/closure_resolution.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,10 +1501,8 @@ fn infer_callable_params_from_function(
15011501
// concrete element type (`PurchaseFileProduct`).
15021502
if !params.is_empty() && !fi.template_params.is_empty() && !fi.template_bindings.is_empty()
15031503
{
1504-
let text_args = extract_argument_texts(arguments, ctx.content);
1505-
let text_args_joined = text_args.join(", ");
1506-
let subs =
1507-
super::rhs_resolution::build_function_template_subs(&fi, &text_args_joined, &rctx);
1504+
let arg_texts = extract_argument_texts(arguments, ctx.content);
1505+
let subs = super::rhs_resolution::build_function_template_subs(&fi, &arg_texts, &rctx);
15081506
if !subs.is_empty() {
15091507
params = params.into_iter().map(|p| p.substitute(&subs)).collect();
15101508
}

src/completion/variable/raw_type_inference.rs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -251,21 +251,32 @@ pub(in crate::completion) fn resolve_array_func_element_type(
251251
None
252252
}
253253

254-
/// Extract the raw text of a function/method argument list from source.
254+
/// Extract per-argument source text from a parsed `ArgumentList`.
255255
///
256-
/// Returns the text between the parentheses (exclusive), trimmed.
257-
/// For example, an argument list `($user, $role)` returns `"$user, $role"`.
258-
pub(in crate::completion) fn extract_argument_text(
256+
/// Returns one `String` per argument by walking the AST nodes and
257+
/// extracting their spans. This avoids serialising the argument list
258+
/// to a flat string and then re-splitting with `split_text_args`.
259+
pub(in crate::completion) fn extract_arg_texts_from_ast(
259260
argument_list: &mago_syntax::ast::ArgumentList<'_>,
260261
content: &str,
261-
) -> String {
262-
let left = argument_list.left_parenthesis.span().end.offset as usize;
263-
let right = argument_list.right_parenthesis.span().start.offset as usize;
264-
if right > left && right <= content.len() {
265-
content[left..right].trim().to_string()
266-
} else {
267-
String::new()
268-
}
262+
) -> Vec<String> {
263+
argument_list
264+
.arguments
265+
.iter()
266+
.map(|arg| {
267+
let span = match arg {
268+
mago_syntax::ast::argument::Argument::Positional(pos) => pos.value.span(),
269+
mago_syntax::ast::argument::Argument::Named(named) => named.value.span(),
270+
};
271+
let start = span.start.offset as usize;
272+
let end = span.end.offset as usize;
273+
if end <= content.len() {
274+
content[start..end].to_string()
275+
} else {
276+
String::new()
277+
}
278+
})
279+
.collect()
269280
}
270281

271282
/// Extract the output element type for `array_map($callback, $array)`.

src/completion/variable/rhs_resolution.rs

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -477,11 +477,11 @@ fn resolve_rhs_instantiation(
477477
&& !ctor.template_bindings.is_empty()
478478
&& let Some(ref arg_list) = inst.argument_list
479479
{
480-
let text_args =
481-
super::raw_type_inference::extract_argument_text(arg_list, ctx.content);
482-
if !text_args.is_empty() {
480+
let arg_texts =
481+
super::raw_type_inference::extract_arg_texts_from_ast(arg_list, ctx.content);
482+
if !arg_texts.is_empty() {
483483
let rctx = ctx.as_resolution_ctx();
484-
let subs = build_constructor_template_subs(cls, ctor, &text_args, &rctx, ctx);
484+
let subs = build_constructor_template_subs(cls, ctor, &arg_texts, &rctx, ctx);
485485
if !subs.is_empty() {
486486
let type_args: Vec<PhpType> = cls
487487
.template_params
@@ -569,11 +569,10 @@ fn resolve_rhs_instantiation(
569569
fn build_constructor_template_subs(
570570
_class: &ClassInfo,
571571
ctor: &crate::types::MethodInfo,
572-
text_args: &str,
572+
arg_texts: &[String],
573573
rctx: &crate::completion::resolver::ResolutionCtx<'_>,
574574
ctx: &VarResolutionCtx<'_>,
575575
) -> HashMap<String, PhpType> {
576-
let args = crate::completion::conditional_resolution::split_text_args(text_args);
577576
let mut subs = HashMap::new();
578577

579578
for (tpl_name, param_name) in &ctor.template_bindings {
@@ -584,7 +583,7 @@ fn build_constructor_template_subs(
584583
};
585584

586585
// Get the corresponding argument text.
587-
let arg_text = match args.get(param_idx) {
586+
let arg_text = match arg_texts.get(param_idx) {
588587
Some(text) => text.trim(),
589588
None => continue,
590589
};
@@ -828,8 +827,12 @@ fn resolve_generic_wrapper_template(
828827
let paren_end = arg_text.rfind(')')?;
829828
let inner_args = arg_text[paren_start + 1..paren_end].trim();
830829

830+
let wrapper_arg_texts = crate::completion::conditional_resolution::split_text_args(inner_args)
831+
.into_iter()
832+
.map(|s| s.to_string())
833+
.collect::<Vec<_>>();
831834
let wrapper_subs =
832-
build_constructor_template_subs(&wrapper_cls, wrapper_ctor, inner_args, rctx, ctx);
835+
build_constructor_template_subs(&wrapper_cls, wrapper_ctor, &wrapper_arg_texts, rctx, ctx);
833836

834837
// Find the wrapper's template param at the given position and
835838
// look it up in the substitution map.
@@ -1012,10 +1015,9 @@ fn classify_array_index(index: &Expression<'_>) -> ArrayBracketSegment {
10121015
/// positional resolution through the wrapper's generic arguments.
10131016
pub(crate) fn build_function_template_subs(
10141017
func_info: &crate::types::FunctionInfo,
1015-
text_args: &str,
1018+
arg_texts: &[String],
10161019
rctx: &crate::completion::resolver::ResolutionCtx<'_>,
10171020
) -> HashMap<String, PhpType> {
1018-
let args = crate::completion::conditional_resolution::split_text_args(text_args);
10191021
let mut subs = HashMap::new();
10201022

10211023
for (tpl_name, param_name) in &func_info.template_bindings {
@@ -1028,7 +1030,7 @@ pub(crate) fn build_function_template_subs(
10281030
None => continue,
10291031
};
10301032

1031-
let arg_text = match args.get(param_idx) {
1033+
let arg_text = match arg_texts.get(param_idx) {
10321034
Some(text) => text.trim(),
10331035
None => continue,
10341036
};
@@ -1353,11 +1355,13 @@ fn resolve_rhs_function_call<'b>(
13531355
&& !func_info.template_bindings.is_empty()
13541356
&& func_info.return_type.is_some()
13551357
{
1356-
let text_args =
1357-
super::raw_type_inference::extract_argument_text(&func_call.argument_list, content);
1358-
if !text_args.is_empty() {
1358+
let arg_texts = super::raw_type_inference::extract_arg_texts_from_ast(
1359+
&func_call.argument_list,
1360+
content,
1361+
);
1362+
if !arg_texts.is_empty() {
13591363
let rctx = ctx.as_resolution_ctx();
1360-
let subs = build_function_template_subs(&func_info, &text_args, &rctx);
1364+
let subs = build_function_template_subs(&func_info, &arg_texts, &rctx);
13611365
if !subs.is_empty()
13621366
&& let Some(ref ret) = func_info.return_type
13631367
{
@@ -1673,13 +1677,16 @@ fn resolve_rhs_method_call_inner<'b>(
16731677
(classes, resolved)
16741678
};
16751679

1676-
let text_args = super::raw_type_inference::extract_argument_text(argument_list, ctx.content);
1680+
let arg_texts =
1681+
super::raw_type_inference::extract_arg_texts_from_ast(argument_list, ctx.content);
1682+
let arg_refs: Vec<&str> = arg_texts.iter().map(|s| s.as_str()).collect();
1683+
let text_args = arg_texts.join(", ");
16771684
let rctx = ctx.as_resolution_ctx();
16781685
let var_resolver = build_var_resolver_from_ctx(ctx);
16791686

16801687
for owner in &owner_classes {
16811688
let template_subs =
1682-
Backend::build_method_template_subs(owner, &method_name, &text_args, &rctx);
1689+
Backend::build_method_template_subs(owner, &method_name, &arg_refs, &rctx);
16831690
let mr_ctx = MethodReturnCtx {
16841691
all_classes: ctx.all_classes,
16851692
class_loader: ctx.class_loader,
@@ -1894,13 +1901,15 @@ fn resolve_rhs_static_call(
18941901
.map(|c| ClassInfo::clone(c))
18951902
.or_else(|| (ctx.class_loader)(&cls_name).map(Arc::unwrap_or_clone));
18961903
if let Some(ref owner) = owner {
1897-
let text_args = super::raw_type_inference::extract_argument_text(
1904+
let arg_texts = super::raw_type_inference::extract_arg_texts_from_ast(
18981905
&static_call.argument_list,
18991906
ctx.content,
19001907
);
1908+
let arg_refs: Vec<&str> = arg_texts.iter().map(|s| s.as_str()).collect();
1909+
let text_args = arg_texts.join(", ");
19011910
let rctx = ctx.as_resolution_ctx();
19021911
let template_subs =
1903-
Backend::build_method_template_subs(owner, &method_name, &text_args, &rctx);
1912+
Backend::build_method_template_subs(owner, &method_name, &arg_refs, &rctx);
19041913
let var_resolver = build_var_resolver_from_ctx(ctx);
19051914
let mr_ctx = MethodReturnCtx {
19061915
all_classes: ctx.all_classes,

0 commit comments

Comments
 (0)