Skip to content

Commit 31aaf2e

Browse files
committed
Refactor PhpType usage to prefer structured types over strings
1 parent 79a5025 commit 31aaf2e

14 files changed

Lines changed: 240 additions & 212 deletions

File tree

docs/todo.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ within the same impact tier.
2626
| # | Item | Impact | Effort |
2727
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------ |
2828

29+
2930
| | **Release 0.7.0** | | |
3031

3132
## Sprint 5 — Polish for office adoption

docs/todo/performance.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -804,10 +804,10 @@ so this fast-path would apply to the majority of checks.
804804
flowing through `mago-names` / `mago-syntax` are already atoms.
805805
Using `Atom` or `Arc<str>` for class names in PHPantom's own
806806
data structures would reduce memory and make the subtype cache
807-
keys cheaper to hash and compare. This becomes a natural
808-
consequence of T19 (structured type representation) since each
809-
type node would store an interned name rather than an owned
810-
`String`.
807+
keys cheaper to hash and compare. Now that `PhpType` is the
808+
structured type representation throughout the codebase, interning
809+
the name strings inside each `PhpType` node (replacing owned
810+
`String` with `Atom` or `Arc<str>`) is a natural next step.
811811

812812
---
813813

docs/todo/type-inference.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,9 @@ NonEmptyCountable.
411411
`Psalm/Storage/Assertion/`. PHPStan's `TypeSpecifier` returns
412412
`SpecifiedTypes` with dual sure/sureNot maps.
413413

414-
**Depends on:** T19 (structured types make reconciliation much
415-
simpler, but basic reconciliation can work with strings too).
414+
**Depends on:** The structured type representation (`PhpType`) has
415+
landed, which makes reconciliation much simpler than working with
416+
raw strings.
416417

417418
---
418419

@@ -519,8 +520,8 @@ This eliminates the depth limit entirely and makes the resolution
519520
cost proportional to the number of statements before the cursor,
520521
not the depth of the assignment chain.
521522

522-
**Depends on:** T19 (structured type representation) should land
523-
first so the scope map stores `PhpType` values instead of strings.
523+
**Note:** The structured type representation (`PhpType`) has landed,
524+
so the scope map can store `PhpType` values directly.
524525

525526
**Migration path:** Start with a parallel implementation behind a
526527
feature flag. The existing backward-scanning resolver stays as a

src/code_actions/phpstan/remove_unused_return_type.rs

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,10 @@ fn extract_unused_type(message: &str) -> Option<&str> {
7272
/// Also handles `?Type` (nullable shorthand): `?string` with unused
7373
/// `null` becomes `string`, and `?string` with unused `string` becomes
7474
/// `null`.
75-
fn remove_type_from_union(full_type: &str, unused_type: &str) -> Option<PhpType> {
76-
let parsed = PhpType::parse(full_type);
75+
fn remove_type_from_union(full_type: &PhpType, unused_type: &str) -> Option<PhpType> {
7776
let unused_parsed = PhpType::parse(unused_type);
7877

79-
match &parsed {
78+
match full_type {
8079
PhpType::Union(members) => {
8180
let remaining: Vec<&PhpType> = members
8281
.iter()
@@ -411,7 +410,7 @@ impl Backend {
411410
let native_type = extract_native_return_type(&lines, paren_line, paren_col, brace_line);
412411
let has_native_match = native_type
413412
.as_ref()
414-
.is_some_and(|t| remove_type_from_union(&t.to_string(), unused_type).is_some());
413+
.is_some_and(|t| remove_type_from_union(t, unused_type).is_some());
415414

416415
// Check the docblock @return tag.
417416
let func_line = find_func_keyword_line(&lines, paren_line).unwrap_or(diag_line);
@@ -427,7 +426,7 @@ impl Backend {
427426
};
428427
let has_doc_match = doc_return_type
429428
.as_ref()
430-
.is_some_and(|t| remove_type_from_union(&t.to_string(), unused_type).is_some());
429+
.is_some_and(|t| remove_type_from_union(t, unused_type).is_some());
431430

432431
// Only offer the action if we can actually remove the type
433432
// from at least one location.
@@ -484,7 +483,7 @@ impl Backend {
484483
// ── Update native return type ───────────────────────────────
485484
if let Some(native_type) =
486485
extract_native_return_type(&lines, paren_line, paren_col, brace_line)
487-
&& let Some(new_type) = remove_type_from_union(&native_type.to_string(), unused_type)
486+
&& let Some(new_type) = remove_type_from_union(&native_type, unused_type)
488487
{
489488
// Convert the new type to a valid native hint.
490489
let native_hint = new_type
@@ -513,7 +512,7 @@ impl Backend {
513512
docblock_info.doc_start_line,
514513
docblock_info.doc_end_line,
515514
)
516-
&& let Some(new_type) = remove_type_from_union(&doc_type.to_string(), unused_type)
515+
&& let Some(new_type) = remove_type_from_union(&doc_type, unused_type)
517516
&& let Some(edit) = find_and_replace_return_tag_type(
518517
&lines,
519518
docblock_info.doc_start_line,
@@ -578,7 +577,7 @@ pub(crate) fn is_remove_unused_return_type_stale(
578577

579578
// Check native return type.
580579
if let Some(native_type) = extract_native_return_type(&lines, paren_line, paren_col, brace_line)
581-
&& remove_type_from_union(&native_type.to_string(), unused_type).is_some()
580+
&& remove_type_from_union(&native_type, unused_type).is_some()
582581
{
583582
// The unused type is still present → not stale.
584583
return false;
@@ -594,7 +593,7 @@ pub(crate) fn is_remove_unused_return_type_stale(
594593
docblock_info.doc_start_line,
595594
docblock_info.doc_end_line,
596595
)
597-
&& remove_type_from_union(&doc_type.to_string(), unused_type).is_some()
596+
&& remove_type_from_union(&doc_type, unused_type).is_some()
598597
{
599598
return false;
600599
}
@@ -640,67 +639,73 @@ mod tests {
640639
#[test]
641640
fn removes_null_from_string_null() {
642641
assert_eq!(
643-
remove_type_from_union("string|null", "null"),
642+
remove_type_from_union(&PhpType::parse("string|null"), "null"),
644643
Some(PhpType::parse("string"))
645644
);
646645
}
647646

648647
#[test]
649648
fn removes_string_from_string_null() {
650649
assert_eq!(
651-
remove_type_from_union("string|null", "string"),
650+
remove_type_from_union(&PhpType::parse("string|null"), "string"),
652651
Some(PhpType::parse("null"))
653652
);
654653
}
655654

656655
#[test]
657656
fn removes_from_three_member_union() {
658657
assert_eq!(
659-
remove_type_from_union("string|int|null", "null"),
658+
remove_type_from_union(&PhpType::parse("string|int|null"), "null"),
660659
Some(PhpType::parse("string|int"))
661660
);
662661
}
663662

664663
#[test]
665664
fn removes_middle_member() {
666665
assert_eq!(
667-
remove_type_from_union("string|int|bool", "int"),
666+
remove_type_from_union(&PhpType::parse("string|int|bool"), "int"),
668667
Some(PhpType::parse("string|bool"))
669668
);
670669
}
671670

672671
#[test]
673672
fn removes_from_intersection() {
674673
assert_eq!(
675-
remove_type_from_union("Foo&Bar", "Bar"),
674+
remove_type_from_union(&PhpType::parse("Foo&Bar"), "Bar"),
676675
Some(PhpType::parse("Foo"))
677676
);
678677
}
679678

680679
#[test]
681680
fn removes_null_from_nullable() {
682681
assert_eq!(
683-
remove_type_from_union("?string", "null"),
682+
remove_type_from_union(&PhpType::parse("?string"), "null"),
684683
Some(PhpType::parse("string"))
685684
);
686685
}
687686

688687
#[test]
689688
fn removes_inner_from_nullable() {
690689
assert_eq!(
691-
remove_type_from_union("?string", "string"),
690+
remove_type_from_union(&PhpType::parse("?string"), "string"),
692691
Some(PhpType::parse("null"))
693692
);
694693
}
695694

696695
#[test]
697696
fn returns_none_when_not_found() {
698-
assert_eq!(remove_type_from_union("string|int", "bool"), None);
697+
assert_eq!(
698+
remove_type_from_union(&PhpType::parse("string|int"), "bool"),
699+
None
700+
);
699701
}
700702

701703
#[test]
702704
fn returns_none_for_single_type() {
703-
assert_eq!(remove_type_from_union("string", "string"), None);
705+
assert_eq!(
706+
remove_type_from_union(&PhpType::parse("string"), "string"),
707+
None
708+
);
704709
}
705710

706711
// ── stale detection ────────────────────────────────────────────

src/completion/phpdoc/generation.rs

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ use crate::completion::resolver::FunctionLoaderFn;
5151
use crate::completion::source::comment_position::position_to_byte_offset;
5252
use crate::completion::source::throws_analysis::{self, ThrowsContext};
5353
use crate::completion::use_edit::{analyze_use_block, build_use_edit};
54-
use crate::php_type::{PhpType, is_keyword_type};
54+
use crate::php_type::PhpType;
5555
use crate::types::{ClassInfo, FunctionLoader};
5656
use crate::util::{byte_offset_to_utf16_col, utf16_col_to_byte_offset};
5757

@@ -1739,28 +1739,24 @@ fn property_var_type_snippet(
17391739
}
17401740
Some(th) => {
17411741
let shortened = th.shorten();
1742-
let clean = shortened.to_string();
17431742
// Callable types get a signature placeholder.
17441743
if th.is_callable() {
1745-
let s = format!("(${{{}:{}()}})", *tab_stop, &clean);
1744+
let s = format!("(${{{}:{}()}})", *tab_stop, shortened);
17461745
*tab_stop += 1;
17471746
return s;
17481747
}
1749-
if !matches!(
1750-
th,
1751-
PhpType::Union(_) | PhpType::Intersection(_) | PhpType::Nullable(_)
1752-
) && !is_keyword_type(&clean)
1753-
&& let Some(cls) = class_loader(&clean)
1748+
if let Some(name) = shortened.base_name()
1749+
&& let Some(cls) = class_loader(name)
17541750
&& !cls.template_params.is_empty()
17551751
{
17561752
let mut parts = Vec::new();
17571753
for tp in &cls.template_params {
17581754
parts.push(format!("${{{}:{}}}", *tab_stop, tp));
17591755
*tab_stop += 1;
17601756
}
1761-
return format!("{}<{}>", &clean, parts.join(", "));
1757+
return format!("{}<{}>", name, parts.join(", "));
17621758
}
1763-
clean
1759+
shortened.to_string()
17641760
}
17651761
}
17661762
}
@@ -1775,21 +1771,17 @@ fn property_var_type_plain(
17751771
Some(th) if th.is_bare_array() => "array".to_string(),
17761772
Some(th) => {
17771773
let shortened = th.shorten();
1778-
let clean = shortened.to_string();
17791774
if th.is_callable() {
1780-
return format!("({}())", &clean);
1775+
return format!("({}())", shortened);
17811776
}
1782-
if !matches!(
1783-
th,
1784-
PhpType::Union(_) | PhpType::Intersection(_) | PhpType::Nullable(_)
1785-
) && !is_keyword_type(&clean)
1786-
&& let Some(cls) = class_loader(&clean)
1777+
if let Some(name) = shortened.base_name()
1778+
&& let Some(cls) = class_loader(name)
17871779
&& !cls.template_params.is_empty()
17881780
{
17891781
let parts: Vec<&str> = cls.template_params.iter().map(|s| s.as_str()).collect();
1790-
return format!("{}<{}>", &clean, parts.join(", "));
1782+
return format!("{}<{}>", name, parts.join(", "));
17911783
}
1792-
clean
1784+
shortened.to_string()
17931785
}
17941786
}
17951787
}

0 commit comments

Comments
 (0)