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
+7Lines changed: 7 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -119,6 +119,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
119
119
120
120
### Fixed
121
121
122
+
-**A variable seeded with `false` can be checked for.**`$time = false; … $time = strtotime($raw); … if ($time) { date($f, $time); }` is how PHP code has always written "not parsed yet", and PHPantom widened that first `false` to `bool` the moment it was assigned. `bool` includes `true`, so the truthiness check had nothing to subtract and the guarded body still saw `bool|int`, which was then reported against `date()`'s `?int` parameter. A written `true` or `false` now keeps its own type the way every other literal does, so the join is `int|false` and the check clears the `false` half. A value that genuinely is `bool` narrows to `true` inside a truthy branch as well, so the common `$found = false; foreach … { $found = true; break; } if ($found)` pattern reports what the branch proved. A type written back into source still widens to `bool`: an inferred return type suggests `: bool` rather than the `true` that would need PHP 8.2 and would reject a subclass returning the other half.
123
+
-**An override's own return type has the last word over the docblock it inherits.** An implementation without a docblock of its own inherits the interface's `@return`, which is usually what you want, but the implementation's native declaration is a promise the wider union does not get to override. An interface declaring `@return array<string, mixed>|list<mixed>|string` with an implementation declaring `: array` reported the `string` half as a possible result, and that half then failed every `array` parameter the value was passed to. The inherited union is now restricted to what the override's own declaration allows. Only alternatives whose kind is unmistakable take part, so a class name, `object`, `callable`, or `iterable` on either side rules nothing out.
124
+
-**An arrow function keeps its parameters in the type it produces.**`fn (BrandView $view) => $this->attrs($view)` was inferred as `Closure(): array<…>`, with the arity and the parameter type dropped, so passing it where a `Closure(BrandView): array<…>` was declared was reported as a mismatch. Closure and arrow function literals now carry their declared parameters, with an untyped parameter contributing `mixed`, which any expected parameter type still satisfies.
125
+
-**`ctype_digit()` and `define()` accept what PHP accepts.** The bundled stubs type `ctype_digit()` and the rest of the `ctype_*` family as taking a `string`, and `define()`'s `$value` with the scalar-or-array union it had before PHP 7. php-src says `mixed` for both, so `ctype_digit($count)` and `define('STDERR', fopen('php://stderr', 'wb'))` were reported as type errors on code that runs fine. Both are widened back. Passing an int to a `ctype_*` function is a deprecation rather than a type error, and belongs in a deprecation diagnostic if it is worth surfacing at all.
126
+
-**A parenthesised union inside an intersection is spelled back with its parentheses.**`&` binds tighter than `|`, so printing `(FunctionNode|MethodNode)&MockObject` without them produced `FunctionNode|MethodNode&MockObject`, which reads as `FunctionNode|(MethodNode&MockObject)` and drops the intersection from the first branch. Hover and diagnostic messages now keep the parentheses, so the type reads back as the one that was written.
127
+
-**A union no longer names the same type twice.** Diagnostics printed types such as `string|App\Entity\User|string` and `null|string|array|null`, which is cosmetic but appears in most multi-branch messages and makes the real disagreement harder to spot. Repeated alternatives are dropped wherever a union is built, and a branch join that produced both halves of a boolean settles on `bool` rather than listing `true|false`.
128
+
122
129
-**A guard that exits by calling a `never` method ends the branch whatever the call is written on.** A guard body whose only statement is a call to a method declared `never` cannot fall through, so the code after the `if` sees the narrowed type, and PHPantom read it that way only when the call was written on a variable. `$app->abort(422)` ended the branch; the same call written `app()->abort(422)`, `(new Application())->abort(422)`, or `$this->responder->abort(422)` did not, and the guarded value kept the union it was supposed to have shed. The receiver is now typed through the same resolution every other expression goes through, so all of those forms end the branch, and the value that survives a Laravel `if (!$file instanceof UploadedFile) { app()->abort(422); }` guard is the `UploadedFile` the guard proved it to be.
123
130
-**An `instanceof` check rules out the array half of a union, not just the other class.** A method whose return type mixes a class with an array of it, such as Laravel's `Request::file()` returning `UploadedFile|array<UploadedFile>|null`, resolves to a single value that names the class and carries the whole union alongside it. Narrowing only ever reached the class half, so the array stayed put: after a correct `if (!$file instanceof UploadedFile) { throw … }` guard, or inside a plain `if ($file instanceof UploadedFile) { … }`, passing the value to a parameter typed `UploadedFile` was still reported as passing `UploadedFile|array<UploadedFile>|null`. A check that concludes what the value is now drops the alternatives it has ruled out, including a `null` the guard had already proven impossible. A check that only rules something out is unaffected, so the array half still survives a negated `instanceof` inside the branch it guards.
124
131
- **An argument written as an array element, a global constant, or a simple operator expression is no longer read as nothing.** `str_replace()`, `preg_replace()`, and any `@template` binding all decide part of their answer from the argument's own source text, and several ordinary ways of writing that argument left them with nothing to go on: `str_replace('a', 'b', $data['message'])` where `$data` is `array{message: string}`, `preg_replace('/-.*/', '', PHP_VERSION)`, and `preg_replace('/\s+/', ' ', $body ?: '')` all kept the full undecided union of both replace branches, even though assigning the same expression to a variable first resolved it correctly. An array-shape or generic element now reads its own value type instead of only the class-backed results the general resolver reported; a bare identifier that names a global constant is read through the same constant lookup hover already uses; and concatenation (always `string`) and the elvis operator (the union of both sides) are read directly rather than being mistaken for a bare variable name.
| B145 |[A non-literal string or int write key widens to `int\|string`](todo/bugs.md#b145-a-non-literal-string-or-int-write-key-widens-to-intstring)| High | Low-Medium |
29
+
| 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 |
30
+
| 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 |
31
+
| 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 |
32
+
| B153 |[Short-circuit operators do not narrow their right operand](todo/bugs.md#b153-short-circuit-operators-do-not-narrow-their-right-operand)| Medium | Low-Medium |
| 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 |
35
+
| B142 |[Builtins with argument-dependent return types, round two](todo/bugs.md#b142-builtins-with-argument-dependent-return-types-round-two)| High | Medium |
36
+
| B144 |[`preg_match``$matches` is nullable and shapeless](todo/bugs.md#b144-preg_match-matches-is-nullable-and-shapeless)| High | Medium |
37
+
| B146 |[Array builtins lose key and element generics](todo/bugs.md#b146-array-builtins-lose-key-and-element-generics)| High | Medium |
38
+
| B149 |[`instanceof` narrowing extends the union instead of filtering it](todo/bugs.md#b149-instanceof-narrowing-extends-the-union-instead-of-filtering-it)| High | Medium |
39
+
| B152 |[A ternary's arms do not receive the condition's narrowing](todo/bugs.md#b152-a-ternarys-arms-do-not-receive-the-conditions-narrowing)| High | Medium |
40
+
| B165 |[`__()` / `trans()` report their raw `string\|array\|null` signature](todo/bugs.md#b165-__--trans-report-their-raw-stringarraynull-signature)| High | Medium |
41
+
| B166 |[Console `argument()` / `option()` ignore the command's `$signature`](todo/bugs.md#b166-console-argument--option-ignore-the-commands-signature)| High | Medium |
42
+
| B141 |[A `never` conditional branch does not assert the condition](todo/bugs.md#b141-a-never-conditional-branch-does-not-assert-the-condition) (depends on B139) | Medium-High | Medium |
43
+
| B147 |[Array literals are not tuples: slot reads return the union of all elements](todo/bugs.md#b147-array-literals-are-not-tuples-slot-reads-return-the-union-of-all-elements)| Medium-High | Medium |
44
+
| B150 |[Branch-local reassignment and narrowing are wrong at the join point](todo/bugs.md#b150-branch-local-reassignment-and-narrowing-are-wrong-at-the-join-point)| Medium-High | Medium |
45
+
| B151 |[Negated compound guards with an early exit narrow nothing](todo/bugs.md#b151-negated-compound-guards-with-an-early-exit-narrow-nothing)| Medium-High | Medium |
46
+
| B154 |[A check on a nullsafe chain does not narrow the receiver](todo/bugs.md#b154-a-check-on-a-nullsafe-chain-does-not-narrow-the-receiver)| Medium-High | Medium |
47
+
| B157 |[`@phpstan-assert` tags on called methods are ignored](todo/bugs.md#b157-phpstan-assert-tags-on-called-methods-are-ignored)| Medium-High | Medium |
| B169 |[Blade `@if` narrowing does not reach `@include`d variables](todo/bugs.md#b169-blade-if-narrowing-does-not-reach-included-variables)| Medium-High | Medium |
50
+
| B143 |[Conditional-return arguments written as expressions still read as nothing](todo/bugs.md#b143-conditional-return-arguments-written-as-expressions-still-read-as-nothing) (depends on B139) | Medium | Medium |
51
+
| B156 |[`assert()` still misses some provable conditions](todo/bugs.md#b156-assert-still-misses-some-provable-conditions)| Medium | Medium |
52
+
| B159 |[An inline `@var` re-pins the variable on every read](todo/bugs.md#b159-an-inline-var-re-pins-the-variable-on-every-read)| Medium | Medium |
53
+
| B174 |[A `break` that leaves a loop early is missing from the post-loop join](todo/bugs.md#b174-a-break-that-leaves-a-loop-early-is-missing-from-the-post-loop-join)| Medium | Medium |
54
+
| B173 |[Classes shipped inside a dependency's phar are invisible](todo/bugs.md#b173-classes-shipped-inside-a-dependencys-phar-are-invisible)| Low-Medium | Medium |
55
+
| B155 |[A checked call expression is forgotten by the next identical call](todo/bugs.md#b155-a-checked-call-expression-is-forgotten-by-the-next-identical-call)| Medium-High | Medium-High |
56
+
| B158 |[Strict `in_array` against a constant list does not narrow the needle](todo/bugs.md#b158-strict-in_array-against-a-constant-list-does-not-narrow-the-needle) (depends on B155) | Low-Medium | Medium |
57
+
| B148 |[Element writes do not refine tracked array state](todo/bugs.md#b148-element-writes-do-not-refine-tracked-array-state)| Medium | Medium-High |
0 commit comments