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
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -134,6 +134,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
134
134
- **`assertInstanceOf()` on a mock leaves an intersection, not a choice.** A mock really is both the interface it was built as and the class it stands in for, so `assertInstanceOf(MethodNode::class, $mock)` on a `MockObject` leaves a `MethodNode&MockObject`. It was recorded as `MockObject|MethodNode` instead, which satisfies neither half, so returning the value from a method declared to return the intersection was reported as a type error. A subject that is *already* an intersection now narrows within it as well, so `(FunctionNode|MethodNode)&MockObject` proven to be a `MethodNode` becomes `MethodNode&MockObject` rather than staying as it was.
135
135
- **An argument is not checked against a `@template` only it could have bound.** Comparing an argument to a parameter type that was substituted from that same argument is circular, and PHPantom already stood the check down where the template had a single binding site. Laravel's `travelTo` names `TDate` in both `$date` and its optional `$callback`, and a call passing only the date still binds `TDate` from that one argument — but the second site's existence was enough to re-enable the check, so `$this->travelTo(Carbon::create(2024))` was reported as expecting a `Carbon` and getting a `?Carbon`, contradicting the callee's own `@template TDate of …|null` bound. What counts is now the binding sites the caller actually filled.
136
136
- **A `@phpstan-assert-if-true` promise about the receiver's own members is kept.** PHPStan's `Scope::isInTrait()` is annotated `@phpstan-assert-if-true !null $this->getTraitReflection()`, and a tag whose subject is a member of the receiver rather than a parameter was ignored outright, so the paired getter still read as nullable inside the guard. Those now narrow the member as read through the variable the call was written on. A `!null` promise about a plain parameter was dropped for a related reason (the tag names no class, so the class-based narrowing had nothing to rule out) and is now applied as the matching `is_*()` guard would apply it. PHPStan leaves the identical `isInClass()` bare, so that pairing is supplied for it: extensions are written against it regardless.
137
+
- **A `Stringable` object passed to a `string` parameter is checked against the file's `strict_types` setting.** PHP only converts a `Stringable` object to a string automatically outside `declare(strict_types=1)`; under strict types the same call throws a `TypeError`. Every neighbouring type-juggling rule (int/float to string, numeric-string to int/float) already read the file's `strict_types` flag, but the `Stringable` rule was accepting the object either way, so a class relying on `__toString()` under strict types went unreported.
137
138
- **A fully-qualified call to an array builtin gets the same answer as an unqualified one.** Writing `\array_sum($counts)` instead of `array_sum($counts)` disabled every one of the rules that read a builtin's return type off the array it was handed, because the rules were looked up under the bare name while the call arrived carrying its leading separator. `\array_sum()` on a list of integers went back to `int|float`, `\array_pop()` on a list of objects lost the object, and the same for the whole family. The separator is now stripped before the lookup, so the fully-qualified spelling that is house style in a good deal of library code behaves like the unqualified one.
138
139
- **`array_chunk()` reports the chunks it makes, not the values it groups.** It was grouped with the builtins that rearrange an array's entries, all of which hand back the element type they were given, and it is the one that adds a level of nesting instead. `foreach (array_chunk($ids, 500) as $chunk)` gave `$chunk` a single ID rather than the batch of them, so passing it anywhere expecting an array was reported as a type error. Each chunk is now an array of the input's elements, renumbered from zero unless `$preserve_keys` asks for the original keys back.
139
140
- **`max()` and `min()` answer with the values they compare.** Both were `mixed` for every call, which accepts anything: `takesInt(max("a", "b"))` and `takesInt(max($strings))` both passed unchecked. A single iterable argument now reports one of its elements, and comparing several values reports one of those.
Copy file name to clipboardExpand all lines: docs/todo.md
-1Lines changed: 0 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -42,7 +42,6 @@ contributor even though it's short.
42
42
| B81 |[Foreach element extraction widens `false` to `bool`](todo/bugs.md#b81-foreach-element-extraction-widens-false-to-bool)| Low-Medium | Medium |
43
43
| B76 |[Blade variables typed from a component class are immune to condition narrowing](todo/bugs.md#b76-blade-variables-typed-from-a-component-class-are-immune-to-condition-narrowing)| Medium | Medium-High |
44
44
| B77 |[A foreach over a proven non-empty array still merges the zero-iteration path](todo/bugs.md#b77-a-foreach-over-a-proven-non-empty-array-still-merges-the-zero-iteration-path)| Medium | Medium-High |
45
-
| B85 |[A `Stringable` object is accepted for a `string` parameter under `strict_types=1`](todo/bugs.md#b85-a-stringable-object-is-accepted-for-a-string-parameter-under-strict_types1)| Low-Medium | Medium |
46
45
| B75 |[A dim-write to the foreach value variable leaks through the loop back-edge](todo/bugs.md#b75-a-dim-write-to-the-foreach-value-variable-leaks-through-the-loop-back-edge)| Medium | High |
0 commit comments