Skip to content

Commit f4e410a

Browse files
committed
Replace type_strings_joined with types_joined and to_string
1 parent 5d4f2a0 commit f4e410a

13 files changed

Lines changed: 40 additions & 96 deletions

File tree

docs/todo.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ within the same impact tier.
2323

2424
| # | Item | Impact | Effort |
2525
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------- |
26-
| 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 |
2726
| T30 | [Thread `PhpType` through remaining string-based API boundaries](todo/type-inference.md#t30-thread-phptype-through-remaining-string-based-api-boundaries) | Medium | Medium |
2827
| | **Release 0.7.0** | | |
2928

@@ -97,10 +96,10 @@ unlikely to move the needle for most users.
9796
| C10 | [Deprecation markers on class-name completions from all sources](todo/completion.md#c10-deprecation-markers-on-class-name-completions-from-all-sources) | Low | Low |
9897
| | **[Type Inference](todo/type-inference.md)** | | |
9998
| T25 | [Forward-walking scope model](todo/type-inference.md#t25-forward-walking-scope-model-for-variable-type-resolution) (eliminate backward-scanning depth limit) | High | Very High |
100-
| T19 | [Structured type representation](todo/type-inference.md#t19-structured-type-representation) (partially complete; remaining API boundary cleanup tracked as T29–T30) | High | Very High |
99+
| T19 | [Structured type representation](todo/type-inference.md#t19-structured-type-representation) (partially complete; remaining API boundary cleanup tracked as T30) | High | Very High |
101100
| T20 | [Type narrowing reconciliation engine](todo/type-inference.md#t20-type-narrowing-reconciliation-engine) (sure/sureNot tracking, AND/OR algebra) | Medium-High | High |
102101
| T6 | `Closure::bind()` / `Closure::fromCallable()` return type preservation | Low-Medium | Low-Medium |
103-
| T12 | [Intersection types flattened to unions by `type_strings_joined`](todo/type-inference.md#t12-intersection-types-flattened-to-unions-by-type_strings_joined) (subsumed by T29) | Low-Medium | Low |
102+
| T12 | [Intersection types flattened to unions during resolution](todo/type-inference.md#t12-intersection-types-flattened-to-unions-by-type_strings_joined) | Low-Medium | Low |
104103
| T13 | [Closure variables lose callable signature detail](todo/type-inference.md#t13-closure-variables-lose-callable-signature-detail) | Low-Medium | Medium |
105104
| T4 | Non-empty-\* type narrowing and propagation | Low | Low |
106105
| T5 | Fiber type resolution | Low | Low |

docs/todo/type-inference.md

Lines changed: 6 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -350,10 +350,12 @@ function measure(Countable&Serializable $thing): void {
350350
// `Countable&Serializable $thing`.
351351
```
352352

353-
**No longer blocked.** `PhpType::Intersection` already exists.
354-
`ResolvedType::types_joined()` returns a `PhpType` that preserves
355-
intersection structure. This will be naturally fixed when T29 migrates
356-
all `type_strings_joined` call sites to `types_joined`.
353+
**Remaining work.** `PhpType::Intersection` already exists and
354+
`ResolvedType::types_joined()` returns a structured `PhpType`, but
355+
the resolution pipeline still produces separate `ResolvedType` entries
356+
for each part of an intersection. The fix is to make the pipeline emit
357+
a single `ResolvedType` with `PhpType::Intersection` when the source
358+
type is an intersection.
357359

358360
**After fixing:** verify that extract function docblock generation
359361
preserves intersection types in both the native hint and the `@param`
@@ -594,52 +596,6 @@ progresses.
594596

595597
---
596598

597-
## T29. Migrate `type_strings_joined` call sites to `types_joined`
598-
**Impact: Medium · Effort: Low**
599-
600-
`ResolvedType::types_joined()` returns a structured `PhpType` and was
601-
created as the replacement for `type_strings_joined()`, which joins
602-
types into a `String` with `|` separators. The string variant is still
603-
called in ~12 places across the resolution pipeline:
604-
605-
- `resolver.rs` (2 call sites)
606-
- `resolution.rs` / variable resolution (3 call sites)
607-
- `rhs_resolution.rs` (1 call site)
608-
- `call_resolution.rs` (1 call site)
609-
- `foreach_resolution.rs` (1 call site)
610-
- `array_shape.rs` (1 call site)
611-
- `fix_return_type.rs` (1 call site)
612-
613-
Each produces a `String` that is later re-parsed by downstream
614-
consumers.
615-
616-
**What to change:**
617-
618-
1. At each call site, replace `type_strings_joined` with
619-
`types_joined` and thread the resulting `PhpType` through to the
620-
consumer.
621-
622-
2. Where the consumer is a function that accepts `&str` (e.g.
623-
`check_unresolvable_class_name`), change that function to accept
624-
`&PhpType`.
625-
626-
3. Once all call sites are migrated, remove `type_strings_joined`
627-
entirely (or mark it `#[deprecated]` first if external tests use
628-
it).
629-
630-
**Files:** `src/completion/resolver.rs`,
631-
`src/completion/variable/resolution.rs`,
632-
`src/completion/variable/rhs_resolution.rs`,
633-
`src/completion/call_resolution.rs`,
634-
`src/completion/variable/foreach_resolution.rs`,
635-
`src/completion/array_shape.rs`,
636-
`src/code_actions/phpstan/fix_return_type.rs`,
637-
`src/types.rs`.
638-
639-
**Part of:** T19.
640-
641-
---
642-
643599
## T30. Thread `PhpType` through remaining string-based API boundaries
644600
**Impact: Medium · Effort: Medium**
645601

src/code_actions/phpstan/fix_return_type.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ pub(crate) fn infer_return_type(
465465
class_loader,
466466
Loaders::with_function(function_loader),
467467
);
468-
let type_str = ResolvedType::type_strings_joined(&results);
468+
let type_str = ResolvedType::types_joined(&results).to_string();
469469
if !type_str.is_empty() {
470470
return_types.push(type_str);
471471
continue;

src/completion/array_shape.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ impl Backend {
547547
if resolved.is_empty() {
548548
None
549549
} else {
550-
Some(ResolvedType::type_strings_joined(&resolved))
550+
Some(ResolvedType::types_joined(&resolved).to_string())
551551
}
552552
}
553553
}

src/completion/call_resolution.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1402,7 +1402,7 @@ impl Backend {
14021402
Loaders::with_function(ctx.function_loader),
14031403
);
14041404
if !resolved.is_empty() {
1405-
return Some(ResolvedType::type_strings_joined(&resolved));
1405+
return Some(ResolvedType::types_joined(&resolved).to_string());
14061406
}
14071407
return None;
14081408
}

src/completion/resolver.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,8 +1137,7 @@ pub(crate) fn resolve_subject_outcome(
11371137
}
11381138

11391139
// Non-scalar, non-class type — check for unresolvable class.
1140-
let raw_type = ResolvedType::type_strings_joined(&resolved);
1141-
if let Some(unresolved) = check_unresolvable_class_name(&raw_type, ctx.class_loader) {
1140+
if let Some(unresolved) = check_unresolvable_class_name(&joined, ctx.class_loader) {
11421141
return SubjectOutcome::UnresolvableClass(unresolved);
11431142
}
11441143
return SubjectOutcome::Untyped;
@@ -1161,7 +1160,8 @@ pub(crate) fn resolve_subject_outcome(
11611160
&& let Some(fl) = ctx.function_loader
11621161
&& let Some(func_info) = fl(fn_name.as_str())
11631162
&& let Some(raw_type) = func_info.return_type_str()
1164-
&& let Some(unresolved) = check_unresolvable_class_name(&raw_type, ctx.class_loader)
1163+
&& let Some(unresolved) =
1164+
check_unresolvable_class_name(&PhpType::parse(&raw_type), ctx.class_loader)
11651165
{
11661166
return SubjectOutcome::UnresolvableClass(unresolved);
11671167
}
@@ -1197,7 +1197,8 @@ pub(crate) fn resolve_subject_outcome(
11971197
ctx.class_loader,
11981198
Loaders::with_function(ctx.function_loader),
11991199
)
1200-
&& let Some(unresolved) = check_unresolvable_class_name(&raw_type, ctx.class_loader)
1200+
&& let Some(unresolved) =
1201+
check_unresolvable_class_name(&PhpType::parse(&raw_type), ctx.class_loader)
12011202
{
12021203
return SubjectOutcome::UnresolvableClass(unresolved);
12031204
}
@@ -1289,15 +1290,14 @@ fn resolve_call_scalar_return(
12891290
/// find it. Returns `None` for scalars, unions, shapes, and types
12901291
/// that resolve successfully.
12911292
fn check_unresolvable_class_name(
1292-
raw_type: &str,
1293+
raw_type: &PhpType,
12931294
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
12941295
) -> Option<String> {
1295-
let parsed = PhpType::parse(raw_type);
1296-
if parsed.all_members_scalar() {
1296+
if raw_type.all_members_scalar() {
12971297
return None;
12981298
}
12991299

1300-
let effective = parsed.non_null_type().unwrap_or_else(|| parsed.clone());
1300+
let effective = raw_type.non_null_type().unwrap_or_else(|| raw_type.clone());
13011301
let base = effective.base_name()?;
13021302

13031303
if class_loader(base).is_none() {

src/completion/variable/foreach_resolution.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::completion::resolver::{Loaders, VarResolutionCtx};
2424

2525
/// Resolve an expression's type string via the unified pipeline.
2626
///
27-
/// Wraps `resolve_rhs_expression` + `type_strings_joined` into a single
27+
/// Wraps `resolve_rhs_expression` + `types_joined` into a single
2828
/// `Option<String>` suitable for callers that previously used
2929
/// `extract_rhs_iterable_raw_type`. Returns `None` when the unified
3030
/// pipeline produces no results or an empty type string.
@@ -36,7 +36,7 @@ pub(in crate::completion) fn resolve_expression_type_string<'b>(
3636
if resolved.is_empty() {
3737
return None;
3838
}
39-
let ts = ResolvedType::type_strings_joined(&resolved);
39+
let ts = ResolvedType::types_joined(&resolved).to_string();
4040
if ts.is_empty() { None } else { Some(ts) }
4141
}
4242

src/completion/variable/resolution.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3106,7 +3106,7 @@ pub(in crate::completion) fn check_expression_for_assignment<'b>(
31063106
let resolved =
31073107
super::rhs_resolution::resolve_rhs_expression(assignment.rhs, &rhs_ctx);
31083108
let value_type = if !resolved.is_empty() {
3109-
ResolvedType::type_strings_joined(&resolved)
3109+
ResolvedType::types_joined(&resolved).to_string()
31103110
} else {
31113111
"mixed".to_string()
31123112
};
@@ -3135,7 +3135,7 @@ pub(in crate::completion) fn check_expression_for_assignment<'b>(
31353135
let resolved =
31363136
super::rhs_resolution::resolve_rhs_expression(assignment.rhs, &rhs_ctx);
31373137
let value_type = if !resolved.is_empty() {
3138-
ResolvedType::type_strings_joined(&resolved)
3138+
ResolvedType::types_joined(&resolved).to_string()
31393139
} else {
31403140
"mixed".to_string()
31413141
};
@@ -3175,7 +3175,7 @@ pub(in crate::completion) fn check_expression_for_assignment<'b>(
31753175
let rhs_ctx = ctx.with_cursor_offset(assignment.span().start.offset);
31763176
let resolved = super::rhs_resolution::resolve_rhs_expression(assignment.rhs, &rhs_ctx);
31773177
let value_type = if !resolved.is_empty() {
3178-
ResolvedType::type_strings_joined(&resolved)
3178+
ResolvedType::types_joined(&resolved).to_string()
31793179
} else {
31803180
"mixed".to_string()
31813181
};
@@ -3428,7 +3428,7 @@ fn infer_array_key_type(index: &Expression<'_>, ctx: &VarResolutionCtx<'_>) -> S
34283428
// Resolve the expression type through the standard pipeline.
34293429
let resolved = super::rhs_resolution::resolve_rhs_expression(index, ctx);
34303430
if !resolved.is_empty() {
3431-
let joined = ResolvedType::type_strings_joined(&resolved);
3431+
let joined = ResolvedType::types_joined(&resolved);
34323432
let key_str = joined.to_string();
34333433
// Normalise the resolved type to a valid array key type.
34343434
// PHP array keys are always int or string; bool and null are

src/completion/variable/resolution_tests.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ function test() {
408408
);
409409

410410
assert!(!results.is_empty(), "Should resolve $data to a type");
411-
let ts = ResolvedType::type_strings_joined(&results);
411+
let ts = ResolvedType::types_joined(&results).to_string();
412412
assert!(
413413
ts.contains("name: string"),
414414
"Shape should contain 'name: string', got: {ts}"
@@ -443,7 +443,7 @@ function test() {
443443
);
444444

445445
assert!(!results.is_empty(), "Should resolve $config to a type");
446-
let ts = ResolvedType::type_strings_joined(&results);
446+
let ts = ResolvedType::types_joined(&results).to_string();
447447
// The base array{host: string} should be merged with the new key.
448448
assert!(
449449
ts.contains("port: int"),
@@ -475,7 +475,7 @@ function test() {
475475
);
476476

477477
assert!(!results.is_empty(), "Should resolve $data to a type");
478-
let ts = ResolvedType::type_strings_joined(&results);
478+
let ts = ResolvedType::types_joined(&results).to_string();
479479
assert!(
480480
ts.contains("value: int"),
481481
"Shape key 'value' should be overridden to int, got: {ts}"
@@ -523,7 +523,7 @@ function test() {
523523
);
524524

525525
assert!(!results.is_empty(), "Should resolve $items to a type");
526-
let ts = ResolvedType::type_strings_joined(&results);
526+
let ts = ResolvedType::types_joined(&results).to_string();
527527
assert!(
528528
ts.contains("User"),
529529
"List element type should contain User, got: {ts}"
@@ -558,7 +558,7 @@ function test() {
558558
);
559559

560560
assert!(!results.is_empty(), "Should resolve $items to a type");
561-
let ts = ResolvedType::type_strings_joined(&results);
561+
let ts = ResolvedType::types_joined(&results).to_string();
562562
assert!(
563563
ts.contains("string") && ts.contains("int"),
564564
"List should contain string|int union, got: {ts}"
@@ -589,7 +589,7 @@ function test() {
589589
);
590590

591591
assert!(!results.is_empty(), "Should resolve $items to a type");
592-
let ts = ResolvedType::type_strings_joined(&results);
592+
let ts = ResolvedType::types_joined(&results).to_string();
593593
assert_eq!(
594594
ts, "list<string>",
595595
"Duplicate pushes of same type should not duplicate, got: {ts}"
@@ -622,7 +622,7 @@ function test() {
622622
);
623623

624624
assert!(!results.is_empty(), "Should resolve $x to a type");
625-
let ts = ResolvedType::type_strings_joined(&results);
625+
let ts = ResolvedType::types_joined(&results).to_string();
626626
assert_eq!(
627627
ts, "list<string>",
628628
"Reassignment should reset; only 'string' push should remain, got: {ts}"
@@ -658,7 +658,7 @@ function test() {
658658
let ts = if results.is_empty() {
659659
"array".to_string()
660660
} else {
661-
ResolvedType::type_strings_joined(&results)
661+
ResolvedType::types_joined(&results).to_string()
662662
};
663663
assert!(
664664
!ts.contains('{'),

src/completion/variable/rhs_resolution.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1153,7 +1153,7 @@ fn resolve_arg_variable_raw_type(
11531153
if resolved.is_empty() {
11541154
None
11551155
} else {
1156-
Some(ResolvedType::type_strings_joined(&resolved))
1156+
Some(ResolvedType::types_joined(&resolved).to_string())
11571157
}
11581158
}
11591159

0 commit comments

Comments
 (0)