You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/CHANGELOG.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -119,6 +119,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
119
119
120
120
### Fixed
121
121
122
+
-**A `@property` tag beats an inherited property nobody can reach.** PHP only calls `__get()` when no *accessible* property of that name exists, so a `protected` declaration up the chain is never what the read yields. PHPantom reported its type anyway: an Eloquent model documenting `@property string $connection` still resolved `$model->connection` through `Model`'s own `\UnitEnum|string|null`, and the same happened for `$table` and `$keyType`, which models shadow routinely. The tag now describes the read it was written for. A property the class declares *itself* is a different matter and keeps its own type, since it is in scope everywhere the tag is, and so does an accessible inherited one.
123
+
-**An accumulator that starts as `[]` counts in whole numbers.**`$totals[$k] = ($totals[$k] ?? 0) + $n` is the standard way to tally by key, and the first pass read the empty array as an unknown value rather than a miss, so the sum came out `int|float` and failed every `array<string, int>` it was declared as. An offset read on `[]` now yields `null`, which is what PHP produces and what the `??` was written to catch. The empty array also stops trailing along beside the array a later write produced: a variable seeded with `[]` and appended to in a loop, or captured by reference and filled in by a closure, reports the array it ends up holding instead of that alternative plus the empty one it started from, so reading an element out of it no longer carries a `null` from the empty half.
122
124
-**A ternary's arms see what its condition proved.**`is_string($req) ? $req : 'today'` handed both arms the raw `string|array|null`, so a value the condition had just established was still reported against every `string` the ternary fed. Each arm is now resolved under its own polarity of the condition, using the same narrowing an `if`/`else` body gets, and it happens wherever the ternary is written rather than only in some positions: assignment, argument, and return all behave the same. That covers the whole family of conditions rather than a list of recognised shapes, so a type guard, a null or falsy check, `instanceof`, a member-existence proof, and anything added to narrowing later all reach the arms. A nested ternary's else arm carries the outer conditions' inverse narrowing as well as its own, and `?:` still yields the truthy half of its subject.
123
125
-**A negated compound guard narrows by every conjunct.**`if (!is_string($payload) || $payload === '') { return; }` is the standard way to reject everything a function cannot handle, and the code after it was left with the un-narrowed union: the guard proved nothing. Falling through an `||` means every operand was false, so each operand's own inverse now applies, whatever kind of check it is. Previously only `instanceof` and member-existence checks were read one operand at a time, and the rest were matched against the whole `||` expression, which never matched. Every exit form works (`return`, `throw`, `continue`, `abort()`), so does the `else` branch, and so do chains of more than two conjuncts. `is_resource()` joins the `is_*` family it was missing from, and `!== ''` / `!== []` now refine to `non-empty-string` / `non-empty-array` rather than only removing a literal that was never in the union.
124
126
-**An array written under a `string` key stays keyed by `string`.** Every non-literal string key widened to `int|string`, on the grounds that a numeric string becomes an int key at runtime. Only a *literal* decimal-integer string does, so a function building `array<string, string>` reported `array<int|string, string>` and failed its own declared return type, including after an explicit `(string)` cast, a backed enum's `->value`, and `ReflectionProperty::getName()`. A key expression now keeps its own domain: `string` stays `string`, `int` stays `int`, and the int conversion applies to literal decimal keys alone. `++$i` and `$i++` resolve as well, so a counter used as a write key no longer falls back to `array-key`.
| B140 |[Interface phpDoc is not inherited by an implementation without its own docblock](todo/bugs.md#b140-interface-phpdoc-is-not-inherited-by-an-implementation-without-its-own-docblock)| High | Low-Medium |
29
28
| B167 |[Factory `create()`/`make()` keep the collection half on single-model chains](todo/bugs.md#b167-factory-createmake-keep-the-collection-half-on-single-model-chains)| Medium-High | Low-Medium |
30
-
| B171 |[A subclass `@property` tag loses to an inherited real property](todo/bugs.md#b171-a-subclass-property-tag-loses-to-an-inherited-real-property)| Medium | Low-Medium |
| B163 |[An `int` assigned to a `float` property is reported](todo/bugs.md#b163-an-int-assigned-to-a-float-property-is-reported)| Low | Low-Medium |
32
30
| B139 |[Conditional return types are not evaluated against argument types](todo/bugs.md#b139-conditional-return-types-are-not-evaluated-against-argument-types)| High | Medium |
33
31
| B142 |[Builtins with argument-dependent return types, round two](todo/bugs.md#b142-builtins-with-argument-dependent-return-types-round-two)| High | Medium |
34
32
| B144 |[`preg_match``$matches` is nullable and shapeless](todo/bugs.md#b144-preg_match-matches-is-nullable-and-shapeless)| High | Medium |
0 commit comments