Skip to content

Commit cefa772

Browse files
committed
Update roadmap
1 parent 2d7f517 commit cefa772

2 files changed

Lines changed: 105 additions & 6 deletions

File tree

docs/todo.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ within the same impact tier.
2121

2222
## Sprint 4 — Refactoring toolkit & type inference
2323

24-
| # | Item | Impact | Effort |
25-
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | ------ |
26-
| H10 | [`return.unusedType` — remove unused type from return union](todo/phpstan-actions.md#h10-returnunusedtype--remove-unused-type-from-return-union) | Medium | Medium |
27-
| H6 | `return.type` — update return type to match actual returns | Medium | Medium |
28-
| | **Release 0.7.0** | | |
24+
| # | Item | Impact | Effort |
25+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | ----------- |
26+
| T24 | [`stdClass` dynamic property access](todo/type-inference.md#t24-stdclass-dynamic-property-access) (suppress diagnostics on `stdClass`/`object`) | Low-Medium | Low |
27+
| T22 | [Array value type tracking from loop assignments](todo/type-inference.md#t22-array-value-type-tracking-from-loop-assignments) | Medium | Medium |
28+
| T23 | [`class-string<T>` static method dispatch](todo/type-inference.md#t23-class-stringt-static-method-dispatch) | Medium | Medium |
29+
| T21 | [Bidirectional template inference](todo/type-inference.md#t21-bidirectional-template-inference-upperlower-bounds) (closure return type → generic) | Medium | Medium-High |
30+
| H10 | [`return.unusedType` — remove unused type from return union](todo/phpstan-actions.md#h10-returnunusedtype--remove-unused-type-from-return-union) | Medium | Medium |
31+
| H6 | `return.type` — update return type to match actual returns | Medium | Medium |
32+
| | **Release 0.7.0** | | |
2933

3034
## Sprint 5 — Polish for office adoption
3135

@@ -91,7 +95,6 @@ unlikely to move the needle for most users.
9195
| | **[Type Inference](todo/type-inference.md)** | | |
9296
| T19 | [Structured type representation](todo/type-inference.md#t19-structured-type-representation) (replace string-based types with `PhpType` enum) | High | Very High |
9397
| T20 | [Type narrowing reconciliation engine](todo/type-inference.md#t20-type-narrowing-reconciliation-engine) (sure/sureNot tracking, AND/OR algebra) | Medium-High | High |
94-
| T21 | [Bidirectional template inference](todo/type-inference.md#t21-bidirectional-template-inference-upperlower-bounds) (upper/lower bounds, variance) | Medium | Medium-High |
9598
| T6 | `Closure::bind()` / `Closure::fromCallable()` return type preservation | Low-Medium | Low-Medium |
9699
| T12 | [Intersection types flattened to unions by `type_strings_joined`](todo/type-inference.md#t12-intersection-types-flattened-to-unions-by-type_strings_joined) | Low-Medium | Low (after M4) |
97100
| T13 | [Closure variables lose callable signature detail](todo/type-inference.md#t13-closure-variables-lose-callable-signature-detail) | Low-Medium | Medium |

docs/todo/type-inference.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,64 @@ simpler, but basic reconciliation can work with strings too).
488488

489489
---
490490

491+
## T22. Array value type tracking from loop assignments
492+
**Impact: Medium · Effort: Medium**
493+
494+
When a variable is initialized as `$arr = []` and then populated
495+
inside a loop with `$arr[$key] = $value`, PHPantom loses the element
496+
type entirely — it resolves `$arr` as bare `array`. Iterating the
497+
array in a subsequent `foreach` then produces `unresolved_member_access`
498+
on every element access.
499+
500+
Examples from triage:
501+
502+
- `$bundleProductCounts[$id] = ['bundle' => $productBundle, 'count' => 1]`
503+
in a loop, then `$bundleProductCount['bundle']->parentProduct()` in
504+
a second loop. (`ProductSupplyAmountChangeListener:56,58`.)
505+
- `$warehouseOrderLines[$key] = $orderLine` (where `$orderLine` is
506+
`PCNPurchaseOrderLine`) in a loop, then
507+
`$warehouseOrderLines[$key] ?? null` resolves as just `null` instead
508+
of `PCNPurchaseOrderLine|null`. (`PCNService:1073,1077`.)
509+
510+
The second example also requires the null-coalescing operator (`??`)
511+
to produce a union of both sides rather than only the fallback.
512+
513+
**Implementation:** when processing an assignment like
514+
`$arr[$expr] = $rhs`, record the RHS type as the array's value type.
515+
When the same variable is accessed in a `foreach` or via bracket
516+
access, use the recorded value type for the element. For `$a ?? $b`,
517+
the result type should be `type($a) | type($b)` with `null` removed
518+
from the left side.
519+
520+
---
521+
522+
## T23. `class-string<T>` static method dispatch
523+
**Impact: Medium · Effort: Medium**
524+
525+
When a variable is typed as `class-string<Foo>` (via `@param` or
526+
native type), calling static methods on it (e.g. `$class::cases()`,
527+
`$class::create()`) should resolve through `Foo` as the base class.
528+
PHPantom currently does not resolve static method calls on
529+
`class-string`-typed variables, so the return type is unknown and
530+
any chaining breaks.
531+
532+
A specific sub-problem is the `static` return type: `UnitEnum::cases()`
533+
returns `static[]`. When called via `$class::cases()` where `$class`
534+
is `class-string<BackedEnum>`, the return type should resolve as
535+
`BackedEnum[]`, making `$item->name` and `$item->value` accessible
536+
in a `foreach`.
537+
538+
Example from triage: `OptionList:27,30``$class::cases()` where
539+
`$class` is typed as `class-string<BackedEnum>`. Properties `name`
540+
and `value` on the iterated elements are unresolved.
541+
542+
**Implementation:** in the static call resolution path, detect when
543+
the subject is a variable with a `class-string<T>` type. Extract `T`
544+
and resolve the method on that class. When the return type contains
545+
`static`, substitute it with `T`.
546+
547+
---
548+
491549
## T21. Bidirectional template inference (upper/lower bounds)
492550
**Impact: Medium · Effort: Medium-High**
493551

@@ -508,9 +566,47 @@ Key gaps:
508566
3. No variance tracking. `@template-covariant T` vs `@template T`
509567
affects whether `Container<Cat>` is assignable to
510568
`Container<Animal>`.
569+
4. Template parameters not instantiated from closure return type at
570+
call sites. Example: `Collection::reduce(fn(Decimal $c, ...):
571+
Decimal => ..., new Decimal('0'))``TReduceReturnType` should
572+
be inferred as `Decimal` from the callback return type, and
573+
`TReduceInitial` as `Decimal` from the `$initial` argument.
574+
Currently the return type of `reduce()` is unresolved.
575+
(Triage: `FlowService:517`.)
511576

512577
**Implementation:** add a `TemplateResolution` struct that accumulates
513578
lower and upper bounds during call-site analysis, with a `resolve()`
514579
method that picks the most specific bound.
515580

516581
---
582+
583+
## T24. `stdClass` dynamic property access
584+
**Impact: Low-Medium · Effort: Low**
585+
586+
`stdClass` is PHP's generic dynamic-property container. Accessing any
587+
property on a value known to be `stdClass` (or narrowed to `object`
588+
via `is_object()`) should not produce `unresolved_member_access`
589+
diagnostics, because `stdClass` permits arbitrary properties by
590+
design.
591+
592+
This affects two common patterns:
593+
594+
1. `json_decode($json, false)` returns `mixed`. After an
595+
`is_object($data)` guard, `$data` should narrow to `object`, and
596+
property access like `$data->error_link` should be permitted
597+
without a diagnostic. (`Order:646,647`.)
598+
2. `DB::select()` returns `array`. After `$result[0] instanceof
599+
stdClass`, the element should narrow to `stdClass` and property
600+
access should be permitted. (`PurchaseFileService:1081,1083`.)
601+
602+
Both patterns also depend on narrowing improvements tracked in T20
603+
(`is_object()` narrowing, `instanceof` on array element access).
604+
This item covers the additional step: once the type is narrowed to
605+
`stdClass` or `object`, suppress member-not-found diagnostics.
606+
607+
**Implementation:** in the diagnostic emission path for
608+
`unknown_member` and `unresolved_member_access`, skip the diagnostic
609+
when the resolved type is `stdClass` or bare `object`. Optionally,
610+
resolve any property access on `stdClass` as `mixed`.
611+
612+
---

0 commit comments

Comments
 (0)