Skip to content

Commit eafea0b

Browse files
committed
Update bug list
1 parent 9888310 commit eafea0b

3 files changed

Lines changed: 186 additions & 9 deletions

File tree

docs/todo.md

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

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

24-
| # | Item | Impact | Effort |
25-
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | ----------- |
26-
27-
| H10 | [`return.unusedType` — remove unused type from return union](todo/phpstan-actions.md#h10-returnunusedtype--remove-unused-type-from-return-union) | Medium | Medium |
28-
| H6 | `return.type` — update return type to match actual returns | Medium | Medium |
29-
| | **Release 0.7.0** | | |
24+
| # | Item | Impact | Effort |
25+
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------- |
26+
| B10 | [`instanceof` ternary narrowing fails when target class is in a phar](todo/bugs.md#b10-instanceof-ternary-narrowing-fails-when-target-class-is-in-a-phar) | Low | Low-Medium |
27+
| B11 | [`static` return type not resolved through `class-string<T>` context](todo/bugs.md#b11-static-return-type-not-resolved-through-class-stringt-context) | Low | Medium |
28+
| B12 | [`Collection::reduce()` generic return type not inferred](todo/bugs.md#b12-collectionreduce-generic-return-type-not-inferred) | Low | Medium |
29+
| B13 | [Array shape tracking from keyed literal assignments in loops](todo/bugs.md#b13-array-shape-tracking-from-keyed-literal-assignments-in-loops) | Low | High |
30+
| B14 | [`DB::select()` returns bare `array`, element type unresolvable](todo/bugs.md#b14-dbselect-returns-bare-array-element-type-unresolvable) | Low | Low |
31+
| H10 | [`return.unusedType` — remove unused type from return union](todo/phpstan-actions.md#h10-returnunusedtype--remove-unused-type-from-return-union) | Medium | Medium |
32+
| H6 | `return.type` — update return type to match actual returns | Medium | Medium |
33+
| | **Release 0.7.0** | | |
3034

3135
## Sprint 5 — Polish for office adoption
3236

@@ -154,7 +158,7 @@ unlikely to move the needle for most users.
154158
| E2 | Project-level stubs as type resolution source | Medium | Medium |
155159
| E3 | IDE-provided and `.phpantom.toml` stub paths | Low-Medium | Low |
156160
| E6 | Stub install prompt for non-Composer projects | Low | Low |
157-
| E7 | [Stub-based framework patches](todo/external-stubs.md#e7-stub-based-framework-patches) | Medium | Medium |
161+
| E7 | [Stub-based framework patches](todo/external-stubs.md#e7-stub-based-framework-patches) | Medium | Medium |
158162
| | **[Performance](todo/performance.md)** | | |
159163
| P1.5 | [Layered class resolution (zero-copy inheritance)](todo/performance.md#p15-layered-class-resolution-zero-copy-inheritance) | High | Very High |
160164
| P13 | [Tiered storage: drop per-file maps for non-open files](todo/performance.md#p13-tiered-storage-drop-per-file-maps-for-non-open-files) | Medium-High | Medium-High |

docs/todo/bugs.md

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,176 @@
11
# PHPantom — Bug Fixes
22

3-
No outstanding items.
3+
## B10. `instanceof` ternary narrowing fails when target class is in a phar
4+
**Impact: Low · Effort: Low-Medium**
5+
6+
Pattern:
7+
```php
8+
$types = $argType instanceof UnionType ? $argType->getTypes() : [$argType];
9+
```
10+
11+
The type engine is unified: completion and diagnostics both use
12+
`resolve_variable_types``walk_statements_for_assignments`
13+
`try_apply_ternary_instanceof_narrowing`. A local-class test
14+
(assignment RHS context, interface parameter, `instanceof` subclass
15+
in ternary condition) passes with zero diagnostics.
16+
17+
The failure in the shared project is specific to PHPStan phar classes.
18+
`PHPStan\Type\UnionType` lives inside `phpstan.phar` and may not be
19+
loadable at resolution time, so `apply_instanceof_inclusion` silently
20+
fails (cannot resolve the class name to a `ClassInfo`), and the
21+
variable keeps its un-narrowed type (`Type`).
22+
23+
**Observed in:** `DecimalDivThrowTypeExtension:54``$argType` is
24+
typed as `PHPStan\Type\Type`, `getTypes()` exists on `UnionType` but
25+
not on the `Type` interface. The `instanceof UnionType` check in the
26+
ternary condition should narrow the type in the then-branch.
27+
28+
**Root cause:** The `instanceof` target class (`UnionType`) cannot be
29+
loaded from the phar, so `apply_instanceof_inclusion` has no
30+
`ClassInfo` to narrow to. The narrowing architecture itself is correct
31+
and unified across completion and diagnostics. The fix is either
32+
phar class indexing or suppressing diagnostics when the `instanceof`
33+
target class is unresolvable (since the developer clearly expects
34+
narrowing to occur).
35+
36+
---
37+
38+
## B11. `static` return type not resolved through `class-string<T>` context
39+
**Impact: Low · Effort: Medium**
40+
41+
Pattern:
42+
```php
43+
/** @param class-string<BackedEnum> $class */
44+
function enum(BackedEnum $value, string $class): void {
45+
foreach ($class::cases() as $item) {
46+
$name = $item->name; // unresolved
47+
$val = $item->value; // unresolved
48+
}
49+
}
50+
```
51+
52+
`UnitEnum::cases()` returns `static[]`. When called on a variable
53+
typed as `class-string<BackedEnum>`, the `static` return type should
54+
resolve to `BackedEnum[]`, making `$item` typed as `BackedEnum`.
55+
Currently `static` is not substituted through the `class-string<T>`
56+
context, so `$item` has no type and `->name` / `->value` are
57+
unresolvable.
58+
59+
**Observed in:** `OptionList:27,30``$class::cases()` where
60+
`$class` is `class-string<BackedEnum>`.
61+
62+
**Root cause:** The `class-string<T>` static dispatch fix (`caa6163`)
63+
resolves method signatures and return types for static calls on
64+
`class-string`-typed variables, but does not substitute `static` in
65+
the return type with the bound type `T`. The substitution
66+
`static → BackedEnum` needs to happen when the call target is
67+
resolved through a `class-string<BackedEnum>` context.
68+
69+
---
70+
71+
## B12. `Collection::reduce()` generic return type not inferred
72+
**Impact: Low · Effort: Medium**
73+
74+
Pattern:
75+
```php
76+
$result = $collection
77+
->reduce(fn(Decimal $carry, OrderProduct $p): Decimal => $carry->add($p->price), new Decimal('0'))
78+
->add($total); // unresolved
79+
```
80+
81+
The `reduce()` method on Laravel collections has this signature:
82+
```
83+
@template TReduceInitial
84+
@template TReduceReturnType
85+
@param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback
86+
@param TReduceInitial $initial
87+
@return TReduceReturnType
88+
```
89+
90+
PHPantom should infer:
91+
- `TReduceInitial = Decimal` (from the `$initial` argument)
92+
- `TReduceReturnType = Decimal` (from the closure return type hint)
93+
- Therefore `reduce()` returns `Decimal`
94+
95+
The bidirectional template inference (`4329efe`) partially addresses
96+
this, but `reduce()` still returns unresolved. The likely gap is the
97+
union `TReduceInitial|TReduceReturnType` in the callable's first
98+
parameter position: the inference engine may not decompose the union
99+
to extract individual template bindings when both templates appear
100+
in the same callable parameter type.
101+
102+
**Observed in:** `FlowService:517` — `->reduce(fn(Decimal $c, ...):
103+
Decimal => ..., new Decimal('0'))->add($total)`.
104+
105+
---
106+
107+
## B13. Array shape tracking from keyed literal assignments in loops
108+
**Impact: Low · Effort: High**
109+
110+
Pattern:
111+
```php
112+
$bundleProductCounts = [];
113+
foreach ($items as $item) {
114+
$bundleProductCounts[$item->id] = [
115+
'bundle' => $item->productBundle,
116+
'count' => 1,
117+
];
118+
}
119+
foreach ($bundleProductCounts as $entry) {
120+
$entry['bundle']->parentProduct(); // unresolved
121+
}
122+
```
123+
124+
PHPantom tracks array value types from variable-key assignments
125+
(`$arr[$key] = $value`), but when the value is an array literal with
126+
string keys (a shape), the element type is not preserved as a shape.
127+
Subsequent access like `$entry['bundle']->method()` requires knowing
128+
that `'bundle'` maps to a specific class type.
129+
130+
**Observed in:** `ProductSupplyAmountChangeListener:58` — array built
131+
with `['bundle' => $productBundle, 'count' => 1]` in a loop, then
132+
iterated; `$bundleProductCount['bundle']->parentProduct()` is
133+
unresolvable because the shape is lost.
134+
135+
**Depends on:** T19 (structured type representation) or at minimum
136+
a basic array shape inference that preserves `array{key: Type}` from
137+
literal array constructors and propagates it through foreach.
138+
139+
---
140+
141+
## B14. `DB::select()` returns bare `array`, element type unresolvable
142+
**Impact: Low · Effort: Low**
143+
144+
Pattern:
145+
```php
146+
$result = DB::select('select sum(...) as incoming_qty from ...');
147+
if ($result[0] instanceof stdClass && $result[0]->incoming_qty) {
148+
return Convert::toInt($result[0]->incoming_qty); // unresolved
149+
}
150+
```
151+
152+
The `DB` facade's `@method` annotation declares `select()` as
153+
returning bare `array` rather than `array<int, stdClass>`. Since the
154+
return type has no element type, `$result[0]` is untyped and
155+
`->incoming_qty` is unresolvable.
156+
157+
Two compounding issues:
158+
159+
1. **Stub/facade return type gap:** `DB::select()` should return
160+
`array<int, stdClass>` (which is what the underlying
161+
`Connection::select()` actually returns), but the facade
162+
annotation only says `array`.
163+
164+
2. **`instanceof` narrowing on array element access:** Even if the
165+
array element type were known, `$result[0] instanceof stdClass`
166+
should narrow `$result[0]` to `stdClass` within the condition.
167+
The narrowing system only matches bare variable names (`$var`),
168+
not subscript expressions (`$var[0]`). This is a T20 concern.
169+
170+
**Observed in:** `PurchaseFileService:1087,1089`.
171+
172+
**Partial workaround:** PHPantom could patch `DB::select()` to return
173+
`array<int, stdClass>` via framework-specific return type overrides
174+
(similar to how scope methods are handled). The `instanceof`
175+
narrowing gap (issue 2) would remain but would no longer trigger
176+
here because `stdClass` property access is already suppressed.

docs/todo/type-inference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ design.
518518
This fixed `Order:646,647` (`json_decode``mixed``is_object`
519519
guard → property access).
520520

521-
**Remaining:** `PurchaseFileService:1081,1083` — `$result[0]
521+
**Remaining:** `PurchaseFileService:1087,1089` — `$result[0]
522522
instanceof stdClass` where `$result` is bare `array` from
523523
`DB::select()`. This requires `instanceof` narrowing on array
524524
element access expressions, which is a T20 concern (the narrowing

0 commit comments

Comments
 (0)