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
@@ -125,6 +125,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
125
125
- **An array literal keeps the values it was written with.** `[1, 1.5, '123']` says exactly what it holds, but the values were widened to `int|float|string` the moment they were stored, so reading an entry back could not be proven to be anything the individual values were. Handing `$values[$key]` to a parameter or return type of `numeric` was reported, even though every entry of the array is numeric on its own, and the same held for a literal key: `$values[2]` read as `string` rather than as the numeric `'123'` written at that position. The values a literal names now survive into its type, so a read off it, a `foreach` over it, and an inferred `@return` all see them, and a key that is only known at runtime resolves to the set of entries the array actually has. Widening now happens where the array is changed instead: a push or a keyed write says the array is being built up rather than written out, so the value arriving there stands in for however many more follow. An alternative already covered by a broader sibling is folded away, so a list mixing a plain `string` with two string literals stays `list<string>`, and a literal naming more distinct values than a set of alternatives is worth reasoning about falls back to the base types.
126
126
- **A constant table constrains a plain signature too.** `@param key-of<ID_TABLE>` says a parameter takes one of the table's keys, and `@return value-of<ID_TABLE>` says the result is one of its values. PHPantom only looked behind the constant's name while working out a call's `@template` bindings, which a function that declares no `@template` never does, so both tags widened to whatever a key or a value could be in general: `acceptsKey('nope')` went unreported, and a return that can only ever be `int|string` was read as `mixed`. The constant is now read wherever a declared parameter or return type names one, so an untemplated function or method is held to the table's own keys and hands back the table's own values. The declaration reads the same way from inside the body: the parameter holds the keys the table has, so hover names them and passing one on is judged against them, and a `@return` naming the table is held to what the table holds, so returning a key the table does not have is reported where it is written. The `Class::TABLE` and `self::TABLE` spellings read the same way, and a constant that cannot be reached, or whose value is not an array literal, still widens rather than being guessed at.
127
127
-**A lookup into a constant table reads as the entry its key names.** A constant holding an array literal is the ordinary way to write a table of settings, and a function that reads one out of it can say so: `@template T of key-of<ID_TABLE>` with `@return ID_TABLE[T]` names the value under whichever key the caller passed. PHPantom read neither tag, because the docblock only ever sees the constant's *name* and nothing looked behind it, so the declaration's own `int|string` stood for every call and `takesInt(lookUp('immutable'))` was reported for passing a `string`. The constant's initializer is now read where a type operator asks for it, so each call resolves to its own entry: hover names it, argument checks judge against it, and a key the table does not hold is still rejected. The `Class::TABLE` spelling reads the same way, and a constant whose value is not an array literal is left alone rather than guessed at.
128
+
-**An omitted argument reads a constant table under the key its own default names.**`@template T of key-of<ID_TABLE>` with `@return ID_TABLE[T]` resolves to a single entry at every call site that writes the key out, but a parameter carrying its own default (`function lookUp(string $type = 'immutable')`) bound nothing when the caller left the argument off, so `lookUp()` fell back to the whole table's value union and `takesInt(lookUp())` was reported for passing a string the call can never return. A default value is as known at the declaration site as an argument is at the call site, so it now binds the template the same way: `lookUp()` resolves exactly as `lookUp('immutable')` does, for a method as much as for a function.
128
129
-**A `for` loop's update clause carries its type into the next iteration.**`for ($node = $head; $node !== null; $node = $node->next)` is how a linked list is walked by hand, and the reassignment in the update clause counted for nothing. The clause was read far enough to hover and navigate the variables in it, but the type it produced was never fed back into the loop, so the body saw whatever the initialiser bound on the first trip through on every trip, and the variable kept that type after the loop as well, even where the update clause was the only thing that could have changed it. The clause now runs where PHP runs it, after the body and before the condition is checked again: the body sees the type the update produces alongside the one the initialiser bound, and a walk that ends because it ran out of nodes leaves the cursor holding `null` rather than the node it started from. The initialisers stay the one-time seed they are, so a variable the update clause retypes is no longer reset to its starting type.
129
130
-**Arithmetic on a refined `int` no longer widens to `int|float`.**`int + int` is `int`, and that held for the bare spelling, but `strlen()`, `count()`, and most of the standard library's counting functions are declared with a refinement like `int<0,max>` rather than plain `int`, and accumulating one of those (`$length += strlen($text);`) read as an unrecognised operand and fell back to the conservative `int|float`, reported several lines away at the function's `return` rather than at the addition that caused it. Every `int` refinement (`positive-int`, `non-negative-int`, `int<min,max>`, and the rest) is now classified as `int` for arithmetic, and the same holds for `float`'s own refinements.
130
131
- **A generic type argument is no longer coerced away.** A file that does not `declare(strict_types=1)` lets PHP convert a `'7'` handed to an `int` parameter, and that leniency was applied to type arguments as well, so a `Box<string>` where a `Box<int>` was required came back compatible and the class-hierarchy check that had the last word compared the two by name and called it a `Box`. Nothing is converted inside a value that is passed on whole, so the arguments are now compared without the juggling rules whatever the file declares, on arguments, returns, and property assignments alike. A class with type arguments that cannot be reconciled with the declared ones is also settled on those arguments rather than on its name, so a future reader of the type engine's own subtype check gets the same answer the diagnostics do. What we cannot prove still passes, unchanged: an argument our inference had to widen, one a `@template-contravariant` declaration means to be wider, a class named without its type arguments, and a name that could be the same class written unqualified.
| B97 |[`CONSTANT[T]` reads as the whole table when `T` comes from a parameter's default value](todo/bugs.md#b97-constantt-reads-as-the-whole-table-when-t-comes-from-a-parameters-default-value)| Low-Medium | Medium |
106
105
| B96 |[A docblock `@param` type narrower than its native nullable type hint is not flagged](todo/bugs.md#b96-a-docblock-param-type-narrower-than-its-native-nullable-type-hint-is-not-flagged)| Low | Medium |
107
106
||**[Code Actions](todo/actions.md)**|||
108
107
| A40 |[Generate method from call](todo/actions.md#a40-generate-method-from-call)| Medium-High | Medium |
0 commit comments