From 20d2b1c1fe0f29152c366703d10924d25157284f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 23 Sep 2024 20:16:08 +0200 Subject: [PATCH] More precise `IntegerRangeType::toString()` --- src/Type/IntegerRangeType.php | 5 +++ ...intfFunctionDynamicReturnTypeExtension.php | 22 +++++++++++- tests/PHPStan/Analyser/nsrt/bug-7387.php | 35 +++++++++++++------ tests/PHPStan/Analyser/nsrt/filter-var.php | 2 +- .../PHPStan/Analyser/nsrt/range-to-string.php | 22 ++++++++++++ .../nsrt/unset-conditional-expressions.php | 2 +- 6 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/range-to-string.php diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index 534be0ebbb..8a9414fbee 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -465,6 +465,11 @@ public function toAbsoluteNumber(): Type public function toString(): Type { + $finiteTypes = $this->getFiniteTypes(); + if ($finiteTypes !== []) { + return TypeCombinator::union(...$finiteTypes)->toString(); + } + $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this); if ($isZero->no()) { return new IntersectionType([ diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index 26cc34d860..4e00f1d8ec 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -105,9 +105,29 @@ public function getTypeFromFunctionCall( $checkArgType = $scope->getType($args[$checkArg]->value); if ( $matches['specifier'] === 's' - && ($checkArgType->isConstantValue()->no() || $matches['width'] === '') && ($checkArgType->isString()->yes() || $checkArgType->isInteger()->yes()) ) { + if ($checkArgType instanceof IntegerRangeType) { + $constArgTypes = $checkArgType->getFiniteTypes(); + } else { + $constArgTypes = $checkArgType->getConstantScalarTypes(); + } + if ($constArgTypes !== []) { + $result = []; + $printfArgs = array_fill(0, count($args) - 1, ''); + foreach ($constArgTypes as $constArgType) { + $printfArgs[$checkArg - 1] = $constArgType->getValue(); + try { + $result[] = new ConstantStringType(@sprintf($constantString->getValue(), ...$printfArgs)); + } catch (Throwable) { + continue 2; + } + } + $singlePlaceholderEarlyReturn = TypeCombinator::union(...$result); + + continue; + } + $singlePlaceholderEarlyReturn = $checkArgType->toString(); } elseif ($matches['specifier'] !== 's') { $singlePlaceholderEarlyReturn = new IntersectionType([ diff --git a/tests/PHPStan/Analyser/nsrt/bug-7387.php b/tests/PHPStan/Analyser/nsrt/bug-7387.php index 0081826133..ad53206b25 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7387.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7387.php @@ -6,7 +6,10 @@ class HelloWorld { - public function inputTypes(int $i, float $f, string $s) { + /** + * @param int<-1, 5> $intRange + */ + public function inputTypes(int $i, float $f, string $s, int $intRange) { // https://3v4l.org/iXaDX assertType('numeric-string', sprintf('%.14F', $i)); assertType('numeric-string', sprintf('%.14F', $f)); @@ -19,6 +22,9 @@ public function inputTypes(int $i, float $f, string $s) { assertType('numeric-string', sprintf('%14F', $i)); assertType('numeric-string', sprintf('%14F', $f)); assertType('numeric-string', sprintf('%14F', $s)); + + assertType("'-1'|'0'|'1'|'2'|'3'|'4'|'5'", sprintf('%s', $intRange)); + assertType("' 0'|' 1'|' 2'|' 3'|' 4'|' 5'|'-1'", sprintf('%2s', $intRange)); } public function specifiers(int $i) { @@ -53,18 +59,27 @@ public function specifiers(int $i) { */ public function positionalArgs($mixed, int $i, float $f, string $s, int $posInt, int $negInt, int $nonZeroIntRange, int $intRange) { // https://3v4l.org/vVL0c - assertType('numeric-string', sprintf('%2$14s', $mixed, $i)); - assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $posInt)); - assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $negInt)); - assertType('numeric-string', sprintf('%2$14s', $mixed, $intRange)); - assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $nonZeroIntRange)); - - assertType("non-falsy-string", sprintf('%2$14s', $mixed, 1)); - assertType("non-falsy-string", sprintf('%2$14s', $mixed, '1')); - assertType("non-falsy-string", sprintf('%2$14s', $mixed, 'abc')); + assertType('numeric-string', sprintf('%2$6s', $mixed, $i)); + assertType('non-falsy-string&numeric-string', sprintf('%2$6s', $mixed, $posInt)); + assertType('non-falsy-string&numeric-string', sprintf('%2$6s', $mixed, $negInt)); + assertType("' 1'|' 2'|' 3'|' 4'|' 5'", sprintf('%2$6s', $mixed, $nonZeroIntRange)); + + // https://3v4l.org/1ECIq + assertType('non-falsy-string', sprintf("%2$'#6s", $mixed, 1)); + assertType('non-falsy-string', sprintf("%2$'#6s", $mixed, $i)); + assertType('non-falsy-string', sprintf("%2$'#6s", $mixed, $posInt)); + assertType('non-falsy-string', sprintf("%2$'#6s", $mixed, $negInt)); + assertType("' 0'|' 1'|' 2'|' 3'|' 4'|' 5'|' -1'", sprintf('%2$6s', $mixed, $intRange)); + assertType('non-falsy-string', sprintf("%2$'#6s", $mixed, $nonZeroIntRange)); + + assertType("' 1'", sprintf('%2$6s', $mixed, 1)); + assertType("' 1'", sprintf('%2$6s', $mixed, '1')); + assertType("' abc'", sprintf('%2$6s', $mixed, 'abc')); + assertType("' 0'|' 1'|' 2'|' 3'|' 4'|' 5'|' -1'", sprintf('%2$6s', $mixed, $intRange)); assertType("'1'", sprintf('%2$s', $mixed, 1)); assertType("'1'", sprintf('%2$s', $mixed, '1')); assertType("'abc'", sprintf('%2$s', $mixed, 'abc')); + assertType("'-1'|'0'|'1'|'2'|'3'|'4'|'5'", sprintf('%2$s', $mixed, $intRange)); assertType('numeric-string', sprintf('%2$.14F', $mixed, $i)); assertType('numeric-string', sprintf('%2$.14F', $mixed, $f)); diff --git a/tests/PHPStan/Analyser/nsrt/filter-var.php b/tests/PHPStan/Analyser/nsrt/filter-var.php index 0d43930de3..e726c77d75 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var.php @@ -159,7 +159,7 @@ public function scalars(bool $bool, float $float, int $int, string $string, int assertType("'17.1'", filter_var(17.1)); assertType("'1.0E-50'", filter_var(1e-50)); assertType('numeric-string', filter_var($int)); - assertType('numeric-string', filter_var($intRange)); + assertType("'0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'", filter_var($intRange)); assertType("'17'", filter_var(17)); assertType('string', filter_var($string)); assertType('non-empty-string', filter_var($nonEmptyString)); diff --git a/tests/PHPStan/Analyser/nsrt/range-to-string.php b/tests/PHPStan/Analyser/nsrt/range-to-string.php new file mode 100644 index 0000000000..494c135d95 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/range-to-string.php @@ -0,0 +1,22 @@ + $i + * @param int<-10, 10> $ii + * @param int<0, 128> $maxlong + * @param int<0, 129> $toolong + */ + public function sayHello($i, $ii, $maxlong, $toolong): void + { + assertType("'10'|'5'|'6'|'7'|'8'|'9'", (string) $i); + assertType("'-1'|'-10'|'-2'|'-3'|'-4'|'-5'|'-6'|'-7'|'-8'|'-9'|'0'|'1'|'10'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'", (string) $ii); + assertType("'0'|'1'|'10'|'100'|'101'|'102'|'103'|'104'|'105'|'106'|'107'|'108'|'109'|'11'|'110'|'111'|'112'|'113'|'114'|'115'|'116'|'117'|'118'|'119'|'12'|'120'|'121'|'122'|'123'|'124'|'125'|'126'|'127'|'128'|'13'|'14'|'15'|'16'|'17'|'18'|'19'|'2'|'20'|'21'|'22'|'23'|'24'|'25'|'26'|'27'|'28'|'29'|'3'|'30'|'31'|'32'|'33'|'34'|'35'|'36'|'37'|'38'|'39'|'4'|'40'|'41'|'42'|'43'|'44'|'45'|'46'|'47'|'48'|'49'|'5'|'50'|'51'|'52'|'53'|'54'|'55'|'56'|'57'|'58'|'59'|'6'|'60'|'61'|'62'|'63'|'64'|'65'|'66'|'67'|'68'|'69'|'7'|'70'|'71'|'72'|'73'|'74'|'75'|'76'|'77'|'78'|'79'|'8'|'80'|'81'|'82'|'83'|'84'|'85'|'86'|'87'|'88'|'89'|'9'|'90'|'91'|'92'|'93'|'94'|'95'|'96'|'97'|'98'|'99'", (string) $maxlong); + assertType("numeric-string", (string) $toolong); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php b/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php index b310ffe1a5..afda5d2229 100644 --- a/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php +++ b/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php @@ -42,7 +42,7 @@ public function doBaz(): void } } - assertType('array{a?: bool, b?: numeric-string, c?: int<-1, 1>, d?: int<0, 1>}', $breakdowns); + assertType("array{a?: bool, b?: '0'|'1', c?: int<-1, 1>, d?: int<0, 1>}", $breakdowns); } }