Skip to content

Commit 5dfc456

Browse files
committed
Accept &[PhpType] in generic-arg resolution
1 parent dd2265d commit 5dfc456

7 files changed

Lines changed: 63 additions & 98 deletions

File tree

docs/todo.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ within the same impact tier.
2424
| # | Item | Impact | Effort |
2525
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------- |
2626
| T27 | [Keep substituted return types as `PhpType` in RHS resolution](todo/type-inference.md#t27-keep-substituted-return-types-as-phptype-in-rhs-resolution) | High | Low |
27-
| T28 | [Accept `&[PhpType]` in generic-arg resolution](todo/type-inference.md#t28-accept-phptype-in-generic-arg-resolution) | Medium | Medium |
2827
| T29 | [Migrate `type_strings_joined` call sites to `types_joined`](todo/type-inference.md#t29-migrate-type_strings_joined-call-sites-to-types_joined) | Medium | Low |
2928
| T30 | [Thread `PhpType` through remaining string-based API boundaries](todo/type-inference.md#t30-thread-phptype-through-remaining-string-based-api-boundaries) | Medium | Medium |
3029
| | **Release 0.7.0** | | |

docs/todo/type-inference.md

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -627,40 +627,6 @@ This runs for every method/static call in an assignment RHS.
627627

628628
---
629629

630-
## T28. Accept `&[PhpType]` in generic-arg resolution
631-
**Impact: Medium · Effort: Medium**
632-
633-
In `completion/types/resolution.rs`, when handling
634-
`PhpType::Generic(name, args)`, the args are converted back to strings
635-
so they can be passed to `build_generic_subs()` and
636-
`apply_generic_args()` in `inheritance.rs`, which accept `&[&str]`.
637-
Those functions then re-parse each arg via `PhpType::parse()`.
638-
639-
The same pattern appears in `resolve_named_type()` where template
640-
bound lookups stringify a `PhpType` to re-enter the string-based
641-
resolution path (~L425, L428).
642-
643-
**What to change:**
644-
645-
1. Change `build_generic_subs()` and `apply_generic_args()` in
646-
`inheritance.rs` to accept `&[PhpType]` instead of `&[&str]`.
647-
Remove the internal `PhpType::parse()` calls.
648-
649-
2. Update `type_hint_to_classes_typed_depth()` to pass the generic
650-
args as `&[PhpType]` directly instead of stringifying them.
651-
652-
3. Update `resolve_named_type()` to use `PhpType` values for template
653-
bound lookups instead of round-tripping through strings.
654-
655-
4. Remove the `apply_substitution()` string shim in `inheritance.rs`
656-
(~L972-1000) once all callers work with `PhpType` directly.
657-
658-
**Files:** `src/inheritance.rs`, `src/completion/types/resolution.rs`.
659-
660-
**Part of:** T19.
661-
662-
---
663-
664630
## T29. Migrate `type_strings_joined` call sites to `types_joined`
665631
**Impact: Medium · Effort: Low**
666632

src/completion/types/resolution.rs

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -224,17 +224,14 @@ fn type_hint_to_classes_typed_depth(
224224
),
225225

226226
// ── Generic type ───────────────────────────────────────────
227-
PhpType::Generic(name, args) => {
228-
let arg_strings: Vec<String> = args.iter().map(|a| a.to_string()).collect();
229-
resolve_named_type(
230-
name,
231-
&arg_strings,
232-
owning_class_name,
233-
all_classes,
234-
class_loader,
235-
depth,
236-
)
237-
}
227+
PhpType::Generic(name, args) => resolve_named_type(
228+
name,
229+
args,
230+
owning_class_name,
231+
all_classes,
232+
class_loader,
233+
depth,
234+
),
238235

239236
// ── Array slice (T[]) ──────────────────────────────────────
240237
// Not a class type itself; skip.
@@ -260,7 +257,7 @@ fn type_hint_to_classes_typed_depth(
260257
/// substitution.
261258
fn resolve_named_type(
262259
name: &str,
263-
generic_arg_strings: &[String],
260+
generic_args: &[PhpType],
264261
owning_class_name: &str,
265262
all_classes: &[Arc<ClassInfo>],
266263
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
@@ -280,11 +277,10 @@ fn resolve_named_type(
280277

281278
// ── self / static / $this ──────────────────────────────────────
282279
if matches!(name, "self" | "static" | "$this") {
283-
if !generic_arg_strings.is_empty() {
280+
if !generic_args.is_empty() {
284281
// `self<RuleError>` → rewrite to `OwningClass<RuleError>`.
285-
let args_str = generic_arg_strings.join(", ");
286-
let rewritten = format!("{}<{}>", owning_class_name, args_str);
287-
return type_hint_to_classes_depth(
282+
let rewritten = PhpType::Generic(owning_class_name.to_string(), generic_args.to_vec());
283+
return type_hint_to_classes_typed_depth(
288284
&rewritten,
289285
owning_class_name,
290286
all_classes,
@@ -321,18 +317,26 @@ fn resolve_named_type(
321317
}
322318

323319
// ── Resolve static/self/$this inside generic arguments ────────
324-
let resolved_generic_args: Vec<String> = generic_arg_strings
325-
.iter()
326-
.map(|arg| {
327-
let trimmed = arg.trim();
328-
if trimmed == "static" || trimmed == "self" || trimmed == "$this" {
329-
owning_class_name.to_string()
330-
} else {
331-
trimmed.to_string()
332-
}
333-
})
334-
.collect();
335-
let generic_args: Vec<&str> = resolved_generic_args.iter().map(|s| s.as_str()).collect();
320+
let resolved_generic_args: Vec<PhpType>;
321+
let generic_args: &[PhpType] = if generic_args.iter().any(
322+
|a| matches!(a, PhpType::Named(n) if matches!(n.as_str(), "static" | "self" | "$this")),
323+
) {
324+
resolved_generic_args = generic_args
325+
.iter()
326+
.map(|arg| {
327+
if let PhpType::Named(n) = arg
328+
&& matches!(n.as_str(), "static" | "self" | "$this")
329+
{
330+
PhpType::Named(owning_class_name.to_string())
331+
} else {
332+
arg.clone()
333+
}
334+
})
335+
.collect();
336+
&resolved_generic_args
337+
} else {
338+
generic_args
339+
};
336340

337341
let short = short_name(name);
338342

@@ -347,7 +351,7 @@ fn resolve_named_type(
347351
let cls = laravel::try_swap_custom_collection(
348352
cls,
349353
name,
350-
&generic_args,
354+
generic_args,
351355
all_classes,
352356
class_loader,
353357
);
@@ -360,7 +364,7 @@ fn resolve_named_type(
360364
} else {
361365
virtual_members::resolve_class_fully(&cls, class_loader)
362366
};
363-
let mut result = apply_generic_args(&resolved, &generic_args);
367+
let mut result = apply_generic_args(&resolved, generic_args);
364368

365369
// ── Template-param mixin resolution ────────────────
366370
// When a class declares `@mixin TParam` where `TParam`
@@ -369,7 +373,7 @@ fn resolve_named_type(
369373
// is not yet known. Now that generic args are concrete,
370374
// resolve those mixins and merge their members.
371375
if cls.mixins.iter().any(|m| cls.template_params.contains(m)) {
372-
let subs = build_generic_subs(&cls, &generic_args);
376+
let subs = build_generic_subs(&cls, generic_args);
373377
if !subs.is_empty() {
374378
let mixin_members = virtual_members::phpdoc::resolve_template_param_mixins(
375379
&cls,
@@ -387,7 +391,7 @@ fn resolve_named_type(
387391
&mut result,
388392
&cls,
389393
name,
390-
&generic_args,
394+
generic_args,
391395
class_loader,
392396
);
393397

@@ -401,7 +405,7 @@ fn resolve_named_type(
401405
laravel::try_inject_mixin_builder_scopes(
402406
&mut result,
403407
&cls,
404-
&generic_args,
408+
generic_args,
405409
class_loader,
406410
);
407411

@@ -425,9 +429,8 @@ fn resolve_named_type(
425429
&& owner.template_params.contains(&short.to_string())
426430
&& let Some(bound) = owner.template_param_bounds.get(short)
427431
{
428-
let bound_str = bound.to_string();
429-
return type_hint_to_classes_depth(
430-
&bound_str,
432+
return type_hint_to_classes_typed_depth(
433+
bound,
431434
owning_class_name,
432435
all_classes,
433436
class_loader,

src/completion/variable/rhs_resolution.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -456,17 +456,11 @@ fn resolve_rhs_instantiation(
456456
let rctx = ctx.as_resolution_ctx();
457457
let subs = build_constructor_template_subs(cls, ctor, &text_args, &rctx, ctx);
458458
if !subs.is_empty() {
459-
let type_arg_strings: Vec<String> = cls
459+
let type_args: Vec<PhpType> = cls
460460
.template_params
461461
.iter()
462-
.map(|p| {
463-
subs.get(p)
464-
.map(|s| s.to_string())
465-
.unwrap_or_else(|| p.clone())
466-
})
462+
.map(|p| subs.get(p).cloned().unwrap_or_else(|| PhpType::parse(p)))
467463
.collect();
468-
let type_args: Vec<&str> =
469-
type_arg_strings.iter().map(|s| s.as_str()).collect();
470464
let resolved =
471465
crate::virtual_members::resolve_class_fully(cls, ctx.class_loader);
472466
let mut substituted =

src/inheritance.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,7 +1009,7 @@ pub(crate) fn apply_substitution<'a>(
10091009
/// `template_params` or `type_args` is empty).
10101010
pub(crate) fn build_generic_subs(
10111011
class: &ClassInfo,
1012-
type_args: &[&str],
1012+
type_args: &[PhpType],
10131013
) -> HashMap<String, PhpType> {
10141014
if class.template_params.is_empty() || type_args.is_empty() {
10151015
return HashMap::new();
@@ -1044,7 +1044,7 @@ pub(crate) fn build_generic_subs(
10441044
continue;
10451045
}
10461046
if let Some(arg) = type_args.get(i - offset) {
1047-
subs.insert(param_name.clone(), PhpType::parse(arg));
1047+
subs.insert(param_name.clone(), arg.clone());
10481048
}
10491049
}
10501050

@@ -1064,10 +1064,10 @@ pub(crate) fn build_generic_subs(
10641064
/// # Example
10651065
///
10661066
/// Given a `Collection` class with `@template TKey` and `@template TValue`,
1067-
/// calling `apply_generic_args(&collection_class, &["int", "User"])` will
1068-
/// substitute every occurrence of `TKey` with `int` and `TValue` with `User`
1067+
/// calling `apply_generic_args(&collection_class, &[PhpType::parse("int"), PhpType::parse("User")])`
1068+
/// will substitute every occurrence of `TKey` with `int` and `TValue` with `User`
10691069
/// in the class's methods and properties.
1070-
pub(crate) fn apply_generic_args(class: &ClassInfo, type_args: &[&str]) -> ClassInfo {
1070+
pub(crate) fn apply_generic_args(class: &ClassInfo, type_args: &[PhpType]) -> ClassInfo {
10711071
let subs = build_generic_subs(class, type_args);
10721072

10731073
if subs.is_empty() {

src/inheritance_tests.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ fn test_apply_generic_args_right_aligns_single_arg_for_collection() {
453453
..ClassInfo::default()
454454
};
455455

456-
let result = apply_generic_args(&collection, &["SectionTranslation"]);
456+
let result = apply_generic_args(&collection, &[PhpType::parse("SectionTranslation")]);
457457

458458
let first = result
459459
.methods
@@ -478,7 +478,10 @@ fn test_apply_generic_args_no_right_align_when_all_args_provided() {
478478
..ClassInfo::default()
479479
};
480480

481-
let result = apply_generic_args(&collection, &["int", "User"]);
481+
let result = apply_generic_args(
482+
&collection,
483+
&[PhpType::parse("int"), PhpType::parse("User")],
484+
);
482485

483486
let first = result
484487
.methods
@@ -499,7 +502,7 @@ fn test_apply_generic_args_no_right_align_without_key_bound() {
499502
..ClassInfo::default()
500503
};
501504

502-
let result = apply_generic_args(&cls, &["Foo"]);
505+
let result = apply_generic_args(&cls, &[PhpType::parse("Foo")]);
503506

504507
let first = result
505508
.methods
@@ -524,7 +527,7 @@ fn test_apply_generic_args_right_align_with_int_bound() {
524527
..ClassInfo::default()
525528
};
526529

527-
let result = apply_generic_args(&cls, &["Product"]);
530+
let result = apply_generic_args(&cls, &[PhpType::parse("Product")]);
528531

529532
let get = result
530533
.methods

src/virtual_members/laravel/mod.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ use casts::cast_type_to_php_type;
113113
pub use factory::LaravelFactoryProvider;
114114
pub(crate) use factory::{factory_to_model_fqn, model_to_factory_fqn};
115115

116+
use crate::php_type::PhpType;
116117
use crate::types::{ClassInfo, PropertyInfo};
117118

118119
use super::{ResolvedClassCache, VirtualMemberProvider, VirtualMembers};
@@ -151,7 +152,7 @@ pub const ELOQUENT_BUILDER_FQN: &str = "Illuminate\\Database\\Eloquent\\Builder"
151152
pub(crate) fn try_swap_custom_collection(
152153
cls: ClassInfo,
153154
base_fqn: &str,
154-
generic_args: &[&str],
155+
generic_args: &[PhpType],
155156
all_classes: &[Arc<ClassInfo>],
156157
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
157158
) -> ClassInfo {
@@ -160,10 +161,10 @@ pub(crate) fn try_swap_custom_collection(
160161
}
161162

162163
// The last generic arg is typically the model type.
163-
let model_arg = generic_args.last().unwrap();
164-
let model_class = find_class_in(all_classes, model_arg)
164+
let model_arg = generic_args.last().unwrap().to_string();
165+
let model_class = find_class_in(all_classes, &model_arg)
165166
.cloned()
166-
.or_else(|| class_loader(model_arg).map(Arc::unwrap_or_clone));
167+
.or_else(|| class_loader(&model_arg).map(Arc::unwrap_or_clone));
167168

168169
if let Some(ref mc) = model_class
169170
&& let Some(coll_name) = mc.laravel().and_then(|l| l.custom_collection.as_ref())
@@ -200,17 +201,17 @@ pub(crate) fn try_inject_builder_scopes(
200201
result: &mut ClassInfo,
201202
raw_cls: &ClassInfo,
202203
base_fqn: &str,
203-
generic_args: &[&str],
204+
generic_args: &[PhpType],
204205
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
205206
) {
206207
if !is_eloquent_builder_fqn(base_fqn, raw_cls) || generic_args.is_empty() {
207208
return;
208209
}
209210

210211
// The first (or only) generic arg is the model type.
211-
let model_arg = generic_args.first().unwrap();
212+
let model_arg = generic_args.first().unwrap().to_string();
212213

213-
inject_scopes_and_model_methods(result, model_arg, class_loader);
214+
inject_scopes_and_model_methods(result, &model_arg, class_loader);
214215
}
215216

216217
/// Inject scope methods and model virtual methods onto a class that has
@@ -232,12 +233,11 @@ pub(crate) fn try_inject_builder_scopes(
232233
pub(crate) fn try_inject_mixin_builder_scopes(
233234
result: &mut ClassInfo,
234235
raw_cls: &ClassInfo,
235-
generic_args: &[&str],
236+
generic_args: &[PhpType],
236237
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
237238
) {
238239
use std::collections::HashMap;
239240

240-
use crate::php_type::PhpType;
241241
use crate::types::MAX_INHERITANCE_DEPTH;
242242
use crate::util::short_name;
243243

@@ -252,7 +252,7 @@ pub(crate) fn try_inject_mixin_builder_scopes(
252252
let mut root_subs: HashMap<String, PhpType> = HashMap::new();
253253
for (i, param_name) in raw_cls.template_params.iter().enumerate() {
254254
if let Some(arg) = generic_args.get(i) {
255-
root_subs.insert(param_name.clone(), PhpType::parse(arg));
255+
root_subs.insert(param_name.clone(), arg.clone());
256256
}
257257
}
258258

0 commit comments

Comments
 (0)