Skip to content

Commit e0165b4

Browse files
ondrejmirtesclaude
andcommitted
Range min() / max() over unsealed extras of a constant array
`processArrayType` fed only the explicit `getValueTypes()` into the min/max comparison, so `min(array{1, 2, ...<int, int>})` inferred `1` and `max(...)` inferred `2` — unsound, since the unsealed extras can be any int above or below the explicit entries. Append the unsealed value type to the comparison arguments when the constant array `isUnsealed()->yes()`, so the result widens to cover the extras (`int` here). Sealed constant arrays keep their exact min/max. `vsprintf` was on the same verify list but is already sound: it reads only the positions the format references (extras beyond them are ignored) and bails to a general string when a referenced position falls outside the explicit keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9bbf9b8 commit e0165b4

2 files changed

Lines changed: 20 additions & 0 deletions

File tree

src/Type/Php/MinMaxFunctionReturnTypeExtension.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ private function processArrayType(string $functionName, Type $argType): Type
128128
$argumentTypes[] = $innerType;
129129
}
130130

131+
$unsealedTypes = $constArrayType->getUnsealedTypes();
132+
if ($unsealedTypes !== null && $constArrayType->isUnsealed()->yes()) {
133+
// Unsealed extras can hold further values, so the min/max
134+
// must also range over the unsealed value type — otherwise
135+
// the explicit entries would be reported as the answer.
136+
$argumentTypes[] = $unsealedTypes[1];
137+
}
138+
131139
$resultTypes[] = $this->processType($functionName, $argumentTypes);
132140
}
133141

tests/PHPStan/Analyser/nsrt/minmax.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ function dummy5(int $i, int $j): void
1818
assertType('array{1: true}', array_filter([false, true]));
1919
}
2020

21+
/**
22+
* @param array{1, 2, ...<int, int>} $unsealed
23+
*/
24+
function unsealedMinMax(array $unsealed): void
25+
{
26+
// The unsealed `<int, int>` extras can be any int, so min/max of
27+
// `{1, 2} ∪ extras` is unbounded — the explicit `1`/`2` must not be
28+
// reported as the result.
29+
assertType('int', min($unsealed));
30+
assertType('int', max($unsealed));
31+
}
32+
2133
function dummy6(string $s, string $t): void {
2234
assertType('array{0?: non-falsy-string, 1?: non-falsy-string}', array_filter([$s, $t]));
2335
}

0 commit comments

Comments
 (0)