Skip to content

Commit 481ac8b

Browse files
committed
Refactor type alias expansion to return PhpType instead of String
1 parent f4e410a commit 481ac8b

13 files changed

Lines changed: 176 additions & 250 deletions

File tree

docs/todo.md

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

2424
| # | Item | Impact | Effort |
2525
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------- |
26-
| T30 | [Thread `PhpType` through remaining string-based API boundaries](todo/type-inference.md#t30-thread-phptype-through-remaining-string-based-api-boundaries) | Medium | Medium |
26+
2727
| | **Release 0.7.0** | | |
2828

2929
## Sprint 5 — Polish for office adoption
@@ -96,7 +96,7 @@ unlikely to move the needle for most users.
9696
| 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 |
9797
| | **[Type Inference](todo/type-inference.md)** | | |
9898
| 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 |
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 |
99+
| T19 | [Structured type representation](todo/type-inference.md#t19-structured-type-representation) (partially complete; subtype checking and union simplification remain) | High | Very High |
100100
| T20 | [Type narrowing reconciliation engine](todo/type-inference.md#t20-type-narrowing-reconciliation-engine) (sure/sureNot tracking, AND/OR algebra) | Medium-High | High |
101101
| T6 | `Closure::bind()` / `Closure::fromCallable()` return type preservation | Low-Medium | Low-Medium |
102102
| 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 |

docs/todo/type-inference.md

Lines changed: 5 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -426,16 +426,12 @@ is already used as the primary representation in core data structures
426426
substitution, type narrowing, and inheritance merging all operate on
427427
`PhpType` values.
428428

429-
The remaining work is eliminating `PhpType -> String -> PhpType`
430-
round-trips at internal API boundaries. Many functions still accept
431-
`&str` or return `Option<String>` for types, forcing callers to
432-
stringify and downstream consumers to re-parse. The concrete migration
433-
tasks are tracked as T29 and T30.
429+
The API boundary cleanup (T29, T30) is complete. All core internal
430+
APIs now accept and return `PhpType` values directly.
434431

435-
Subtype checking (`is Cat a subtype of Animal?`), union simplification
436-
(`string|string`, `true|false -> bool`), and intersection distribution
437-
over unions remain unimplemented and are out of scope for the API
438-
boundary cleanup.
432+
The remaining work is subtype checking (`is Cat a subtype of Animal?`),
433+
union simplification (`string|string`, `true|false -> bool`), and
434+
intersection distribution over unions.
439435

440436
---
441437

@@ -594,63 +590,4 @@ statement analyzers. Both converge on the same architecture: the
594590
scope is the single source of truth, populated eagerly as the walk
595591
progresses.
596592

597-
---
598-
599-
## T30. Thread `PhpType` through remaining string-based API boundaries
600-
**Impact: Medium · Effort: Medium**
601-
602-
Several internal APIs accept `&str` or `Option<String>` for types when
603-
their callers already have a `PhpType`. This creates parse-on-entry
604-
overhead and loses structural information. The items below can be done
605-
independently in any order.
606-
607-
### `VarResolutionCtx.enclosing_return_type`
608-
609-
In `resolver.rs` (~L128), `enclosing_return_type` is `Option<String>`.
610-
It is parsed with `PhpType::parse()` every time it is consumed (e.g.
611-
for `generator_send_type()` during yield inference). Change to
612-
`Option<PhpType>` and parse once at construction.
613-
614-
### `resolve_type_alias()` return type
615-
616-
In `completion/types/resolution.rs` (~L462-544), type alias
617-
definitions are stored as `PhpType` in `TypeAliasDef::Local(PhpType)`
618-
but immediately stringified to return `Option<String>`. Change to
619-
return `Option<PhpType>`.
620-
621-
### `is_type_subclass_of()` base name extraction
622-
623-
In `call_resolution.rs` (~L1054-1143), a `PhpType` is stringified
624-
then split on `<` and stripped of `\\` to get the base class name. Use
625-
`PhpType::base_name()` instead.
626-
627-
### Shape/list builders in variable resolution
628-
629-
In `resolution.rs`, `merge_shape_key()`, `merge_push_type()`, and
630-
`merge_keyed_type()` build type strings via `format!("array{{...}}")`
631-
and `format!("list<...>")` that are immediately re-parsed. Build
632-
`PhpType::ArrayShape(entries)` and `PhpType::Generic("list", ...)`
633-
directly.
634-
635-
### Hover formatting entry points
636-
637-
In `hover/formatting.rs`, `build_var_annotation()` and
638-
`build_param_return_section()` accept `Option<&str>` (produced by
639-
`type_hint_str()` which stringifies a `PhpType`), then immediately
640-
re-parse. Change to accept `Option<&PhpType>`.
641-
642-
### `type_hint_to_classes_depth()` string preprocessing
643-
644-
In `completion/types/resolution.rs` (~L88-128), `strip_nullable()`
645-
and parenthesis stripping are done via string manipulation before
646-
parsing. `PhpType::parse()` already handles `?Foo` and `(A&B)|C`
647-
natively, making this preprocessing redundant. Parse first, then
648-
handle alias resolution on the parsed result.
649-
650-
**Files:** `src/completion/resolver.rs`,
651-
`src/completion/types/resolution.rs`,
652-
`src/completion/call_resolution.rs`,
653-
`src/completion/variable/resolution.rs`,
654-
`src/hover/formatting.rs`.
655593

656-
**Part of:** T19.

src/completion/array_shape.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ impl Backend {
339339
effective_classes,
340340
&class_loader,
341341
)
342+
.map(|t| t.to_string())
342343
.unwrap_or(effective_type);
343344

344345
// Parse the array shape entries via PhpType.

src/completion/call_resolution.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,15 +1057,11 @@ fn is_type_subclass_of(
10571057
all_classes: &[Arc<ClassInfo>],
10581058
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
10591059
) -> bool {
1060-
let candidate_str = candidate_type.to_string();
1061-
1062-
// Strip leading backslash and generic parameters for name comparison.
1063-
let candidate_base = candidate_str
1064-
.strip_prefix('\\')
1065-
.unwrap_or(&candidate_str)
1066-
.split('<')
1067-
.next()
1068-
.unwrap_or(&candidate_str);
1060+
// Extract the base class name directly from the type without stringifying.
1061+
let candidate_base = match candidate_type.base_name() {
1062+
Some(name) => name.strip_prefix('\\').unwrap_or(name),
1063+
None => return false, // Not a class type
1064+
};
10691065
let ancestor_base = ancestor_name
10701066
.strip_prefix('\\')
10711067
.unwrap_or(ancestor_name)

src/completion/resolver.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ pub(super) struct VarResolutionCtx<'a> {
125125
/// The `@return` type annotation of the enclosing function/method,
126126
/// if known. Used inside generator bodies to reverse-infer variable
127127
/// types from `Generator<TKey, TValue, TSend, TReturn>`.
128-
pub enclosing_return_type: Option<String>,
128+
pub enclosing_return_type: Option<PhpType>,
129129
/// When `true`, if/else/elseif walking only considers the branch
130130
/// that contains the cursor instead of unioning all branches.
131131
/// This produces the single type visible at the cursor position,
@@ -168,7 +168,7 @@ impl<'a> VarResolutionCtx<'a> {
168168
/// annotation differs from the outer scope.
169169
pub(super) fn with_enclosing_return_type(
170170
&self,
171-
enclosing_return_type: Option<String>,
171+
enclosing_return_type: Option<PhpType>,
172172
) -> VarResolutionCtx<'a> {
173173
VarResolutionCtx {
174174
var_name: self.var_name,

src/completion/source/helpers.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ fn walk_array_segments_and_resolve(
539539
all_classes,
540540
class_loader,
541541
) {
542-
PhpType::parse(&expanded)
542+
expanded
543543
} else {
544544
base_type.clone()
545545
}
@@ -599,7 +599,7 @@ fn walk_array_segments_and_resolve(
599599
all_classes,
600600
class_loader,
601601
) {
602-
current = PhpType::parse(&expanded);
602+
current = expanded;
603603
}
604604
}
605605

src/completion/types/resolution.rs

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -96,34 +96,11 @@ fn type_hint_to_classes_depth(
9696
return vec![];
9797
}
9898

99-
let hint = crate::util::strip_nullable(type_hint);
100-
101-
// Strip surrounding parentheses that appear in DNF types like `(A&B)|C`.
102-
let hint = hint
103-
.strip_prefix('(')
104-
.and_then(|h| h.strip_suffix(')'))
105-
.unwrap_or(hint);
106-
107-
// ── Type alias resolution ──────────────────────────────────────
108-
// Check if `hint` is a type alias defined on the owning class
109-
// (via `@phpstan-type` / `@psalm-type` / `@phpstan-import-type`).
110-
// If so, expand the alias and resolve the underlying definition.
111-
//
112-
// This runs before union/intersection splitting because the alias
113-
// itself may expand to a union or intersection type.
114-
if let Some(alias_def) = resolve_type_alias(hint, owning_class_name, all_classes, class_loader)
115-
{
116-
return type_hint_to_classes_depth(
117-
&alias_def,
118-
owning_class_name,
119-
all_classes,
120-
class_loader,
121-
depth + 1,
122-
);
123-
}
124-
125-
// Parse and delegate to the typed implementation.
126-
let parsed = PhpType::parse(hint);
99+
// Parse first — PhpType::parse handles `?Foo`, `(A&B)|C`, and all
100+
// other syntax natively, making manual string preprocessing redundant.
101+
// Alias resolution is handled by `resolve_named_type` when the parsed
102+
// type turns out to be a `Named` variant.
103+
let parsed = PhpType::parse(type_hint);
127104
type_hint_to_classes_typed_depth(&parsed, owning_class_name, all_classes, class_loader, depth)
128105
}
129106

@@ -264,10 +241,10 @@ fn resolve_named_type(
264241
depth: u8,
265242
) -> Vec<ClassInfo> {
266243
// ── Type alias resolution ──────────────────────────────────────
267-
if let Some(alias_def) = resolve_type_alias(name, owning_class_name, all_classes, class_loader)
244+
if let Some(alias_type) = resolve_type_alias(name, owning_class_name, all_classes, class_loader)
268245
{
269-
return type_hint_to_classes_depth(
270-
&alias_def,
246+
return type_hint_to_classes_typed_depth(
247+
&alias_type,
271248
owning_class_name,
272249
all_classes,
273250
class_loader,
@@ -467,7 +444,7 @@ pub(crate) fn resolve_type_alias(
467444
owning_class_name: &str,
468445
all_classes: &[Arc<ClassInfo>],
469446
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
470-
) -> Option<String> {
447+
) -> Option<PhpType> {
471448
let mut current = hint.to_string();
472449
let mut resolved_any = false;
473450

@@ -490,7 +467,11 @@ pub(crate) fn resolve_type_alias(
490467
}
491468
}
492469

493-
if resolved_any { Some(current) } else { None }
470+
if resolved_any {
471+
Some(PhpType::parse(&current))
472+
} else {
473+
None
474+
}
494475
}
495476

496477
/// Single-level alias lookup (no chaining).

src/completion/variable/foreach_resolution.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ pub(in crate::completion) fn try_resolve_foreach_value_type<'b>(
243243
ctx.all_classes,
244244
ctx.class_loader,
245245
)
246+
.map(|t| t.to_string())
246247
.unwrap_or(rt)
247248
});
248249

@@ -411,6 +412,7 @@ pub(in crate::completion) fn try_resolve_foreach_key_type<'b>(
411412
ctx.all_classes,
412413
ctx.class_loader,
413414
)
415+
.map(|t| t.to_string())
414416
.unwrap_or(rt)
415417
});
416418

@@ -826,6 +828,7 @@ pub(in crate::completion) fn try_resolve_destructured_type<'b>(
826828
all_classes,
827829
class_loader,
828830
)
831+
.map(|t| t.to_string())
829832
.unwrap_or(rt)
830833
});
831834

src/completion/variable/raw_type_inference.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use super::{ARRAY_ELEMENT_FUNCS, ARRAY_PRESERVING_FUNCS};
1313

1414
use crate::docblock;
1515
use crate::parser::extract_hint_string;
16+
use crate::php_type::PhpType;
1617
use crate::types::ClassInfo;
1718

1819
use crate::completion::resolver::VarResolutionCtx;
@@ -328,11 +329,11 @@ fn extract_array_map_element_type(
328329
/// using a variable that is yielded but was not explicitly typed via
329330
/// an assignment or parameter.
330331
pub(in crate::completion) fn try_infer_from_generator_yield(
331-
return_type: &str,
332+
return_type: &PhpType,
332333
ctx: &VarResolutionCtx<'_>,
333334
) -> Vec<ClassInfo> {
334335
// Only applies to Generator return types.
335-
let value_type = match crate::php_type::PhpType::parse(return_type).extract_value_type(false) {
336+
let value_type = match return_type.extract_value_type(false) {
336337
Some(vt) => vt.to_string(),
337338
None => return vec![],
338339
};

0 commit comments

Comments
 (0)