From 97dca260f12cbd55efc4416af93d6c3f96e3c2de Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:33:52 +0000 Subject: [PATCH] Infer `non-decimal-int-string` from constant parts that cannot occur in a decimal-int-string - Add `PHPStan\Type\DecimalIntegerStringHelper` with `canStart()`/`canEnd()`/`canBeInside()`, which decide whether a known constant part can appear at a given position of a decimal-int-string (digits with an optional leading `-`, no redundant leading zeros). - `InitializerExprTypeResolver::resolveConcatType()` now adds `AccessoryDecimalIntegerStringType(inverse: true)` when every constant string of an operand is disqualified at its side. This covers `.`, `.=` and interpolated strings. Deliberately limited to constant operands: `non-decimal-int-string . int` is not sound because `'-' . 1` is `'-1'`. - `AccessoryDecimalIntegerStringType::toNumber()` returns `ErrorType` when inverted; a non-decimal-int-string can be an arbitrary non-numeric string such as `"foo"`, so `1 + $s` is a TypeError. Without this, the new inference made PHPStan stop reporting arithmetic on non-numeric strings. - Analogous cases fixed the same way: - `implode()`/`join()` - a constant separator between at least two elements, or constant element values of a non-empty array. - `sprintf()`/`vsprintf()` - the literal parts of a constant format, extracted by splitting on conversion specifications (bails out when a leftover `%` shows the format was not fully understood). - `strtolower()`/`strtoupper()`/`ucfirst()`/`ucwords()`/`mb_convert_case()` and friends preserve both `decimal-int-string` and `non-decimal-int-string`, since they never touch digits. `mb_convert_kana()` is excluded because it converts digits between half-width and full-width. - `number_format()` with at least one decimal and a decimal separator that cannot occur between digits. - These extensions now build their result with `TypeCombinator::intersect()` instead of `new IntersectionType()` so redundant accessory types are normalized away. - Probed and left alone: `str_pad()` (padding is not provably applied for a non-constant input), `trim()`/`substr()`/`str_replace()`/`strrev()` (can turn a non-decimal-int-string into a decimal one), `str_repeat()` (`"5"` repeated is `"55"`). --- .../InitializerExprTypeResolver.php | 47 ++++++ .../AccessoryDecimalIntegerStringType.php | 6 +- src/Type/DecimalIntegerStringHelper.php | 69 +++++++++ .../ImplodeFunctionReturnTypeExtension.php | 46 +++++- ...rmatFunctionDynamicReturnTypeExtension.php | 87 +++++++++-- ...intfFunctionDynamicReturnTypeExtension.php | 67 ++++++++- .../StrCaseFunctionsReturnTypeExtension.php | 15 +- tests/PHPStan/Analyser/nsrt/binary.php | 6 +- tests/PHPStan/Analyser/nsrt/bug-10863.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-11129.php | 32 ++-- tests/PHPStan/Analyser/nsrt/bug-15055.php | 142 ++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-3379.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-5168-php8.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-7387.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-8568.php | 4 +- .../Analyser/nsrt/constant-string-unions.php | 6 +- .../Analyser/nsrt/decimal-int-string.php | 2 +- .../PHPStan/Analyser/nsrt/dynamic-sprintf.php | 2 +- .../nsrt/foreach-partially-non-iterable.php | 2 +- tests/PHPStan/Analyser/nsrt/implode.php | 2 +- .../PHPStan/Analyser/nsrt/literal-string.php | 8 +- .../nsrt/lowercase-string-sprintf.php | 32 ++-- .../Analyser/nsrt/non-empty-string.php | 10 +- .../Analyser/nsrt/non-falsy-string.php | 10 +- tests/PHPStan/Analyser/nsrt/number_format.php | 18 +-- tests/PHPStan/Analyser/nsrt/str-casing.php | 4 +- .../PHPStan/Rules/DeadCode/data/bug-8620.php | 2 +- .../Rules/Methods/CallMethodsRuleTest.php | 2 +- tests/PHPStan/Rules/Methods/data/bug-5372.php | 2 +- 29 files changed, 527 insertions(+), 106 deletions(-) create mode 100644 src/Type/DecimalIntegerStringHelper.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-15055.php diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 7332aab8f36..5646bde08a2 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -43,6 +43,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -66,6 +67,7 @@ use PHPStan\Type\Constant\OversizedArrayBuilder; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ConstantTypeHelper; +use PHPStan\Type\DecimalIntegerStringHelper; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; @@ -595,6 +597,10 @@ public function resolveConcatType(Type $left, Type $right): Type $accessoryTypes[] = new AccessoryUppercaseStringType(); } + if ($this->isConcatNonDecimalIntegerString($leftStringType, $rightStringType)) { + $accessoryTypes[] = new AccessoryDecimalIntegerStringType(inverse: true); + } + $leftNumericStringNonEmpty = TypeCombinator::remove($leftStringType, new ConstantStringType('')); if ($leftNumericStringNonEmpty->isNumericString()->yes()) { $validationCallback = $left->isInteger()->yes() @@ -633,6 +639,47 @@ public function resolveConcatType(Type $left, Type $right): Type return new StringType(); } + /** + * A decimal-int-string is made of digits with an optional leading `-` (and without + * redundant leading zeros). So when one of the operands is a known constant string + * that cannot occur at its side of such a string, the concatenation can never be one. + * + * This is only sound for constant operands: `non-decimal-int-string . int` is not + * a non-decimal-int-string, because `'-' . 1` is `'-1'`. + */ + private function isConcatNonDecimalIntegerString(Type $leftStringType, Type $rightStringType): bool + { + $leftConstantStrings = $leftStringType->getConstantStrings(); + if (count($leftConstantStrings) > 0) { + $rightCanBeEmpty = !$rightStringType->isNonEmptyString()->yes(); + $allDisqualify = true; + foreach ($leftConstantStrings as $leftConstantString) { + if (DecimalIntegerStringHelper::canStart($leftConstantString->getValue(), $rightCanBeEmpty)) { + $allDisqualify = false; + break; + } + } + + if ($allDisqualify) { + return true; + } + } + + $rightConstantStrings = $rightStringType->getConstantStrings(); + if (count($rightConstantStrings) === 0) { + return false; + } + + $leftCanBeEmpty = !$leftStringType->isNonEmptyString()->yes(); + foreach ($rightConstantStrings as $rightConstantString) { + if (DecimalIntegerStringHelper::canEnd($rightConstantString->getValue(), $leftCanBeEmpty)) { + return false; + } + } + + return true; + } + /** * @param callable(Expr): Type $getTypeCallback */ diff --git a/src/Type/Accessory/AccessoryDecimalIntegerStringType.php b/src/Type/Accessory/AccessoryDecimalIntegerStringType.php index 4e94240e5ac..5549bb1b04e 100644 --- a/src/Type/Accessory/AccessoryDecimalIntegerStringType.php +++ b/src/Type/Accessory/AccessoryDecimalIntegerStringType.php @@ -218,10 +218,8 @@ public function tryRemove(Type $typeToRemove): ?Type public function toNumber(): Type { if ($this->inverse) { - return new UnionType([ - $this->toInteger(), - $this->toFloat(), - ]); + // a non-decimal-int-string can be an arbitrary non-numeric string like "foo" + return new ErrorType(); } return $this->toInteger(); diff --git a/src/Type/DecimalIntegerStringHelper.php b/src/Type/DecimalIntegerStringHelper.php new file mode 100644 index 00000000000..19cbc427971 --- /dev/null +++ b/src/Type/DecimalIntegerStringHelper.php @@ -0,0 +1,69 @@ +isNonDecimalIntegerString($arrayType, $valueTypeAsString, $separatorType)) { + $accessoryTypes[] = new AccessoryDecimalIntegerStringType(inverse: true); + } + if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); - return new IntersectionType($accessoryTypes); + return TypeCombinator::intersect(...$accessoryTypes); } return new StringType(); } + /** + * The separator ends up surrounded by values, and every value of a non-empty array ends up + * somewhere in the result. So a constant separator or constant value that cannot occur + * inside a decimal-int-string proves the whole result is not one. + */ + private function isNonDecimalIntegerString(Type $arrayType, Type $valueTypeAsString, Type $separatorType): bool + { + if (IntegerRangeType::fromInterval(2, null)->isSuperTypeOf($arrayType->getArraySize())->yes()) { + if ($this->allConstantStringsCannotBeInside($separatorType, !$valueTypeAsString->isNonEmptyString()->yes())) { + return true; + } + } + + if (!$arrayType->isIterableAtLeastOnce()->yes()) { + return false; + } + + return $this->allConstantStringsCannotBeInside($valueTypeAsString, true); + } + + private function allConstantStringsCannotBeInside(Type $type, bool $restBeforeCanBeEmpty): bool + { + $constantStrings = $type->getConstantStrings(); + if (count($constantStrings) === 0) { + return false; + } + + foreach ($constantStrings as $constantString) { + if (DecimalIntegerStringHelper::canBeInside($constantString->getValue(), $restBeforeCanBeEmpty)) { + return false; + } + } + + return true; + } + private function inferConstantType(ConstantArrayType $arrayType, ConstantStringType $separatorType, bool $isNonEmpty): ?Type { // Unsealed extras can append further segments the constant fold diff --git a/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php b/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php index b5f1e36320d..6c3b6fba05d 100644 --- a/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php @@ -2,15 +2,19 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\DecimalIntegerStringHelper; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntersectionType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function count; use function in_array; @@ -25,28 +29,81 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $stringType = new StringType(); - if (!isset($functionCall->getArgs()[3])) { - return $stringType; + $args = $functionCall->getArgs(); + + $accessoryTypes = []; + if ($this->isNumericString($args, $scope)) { + $accessoryTypes[] = new AccessoryNumericStringType(); + } + if ($this->isNonDecimalIntegerString($args, $scope)) { + $accessoryTypes[] = new AccessoryDecimalIntegerStringType(inverse: true); + } + + if (count($accessoryTypes) === 0) { + return new StringType(); } - $thousandsType = $scope->getType($functionCall->getArgs()[3]->value); - $decimalType = $scope->getType($functionCall->getArgs()[2]->value); + $accessoryTypes[] = new StringType(); + + return TypeCombinator::intersect(...$accessoryTypes); + } + + /** + * @param array $args + */ + private function isNumericString(array $args, Scope $scope): bool + { + if (!isset($args[3])) { + return false; + } - $constantThousandsTypes = $thousandsType->getConstantStrings(); + $constantThousandsTypes = $scope->getType($args[3]->value)->getConstantStrings(); if (count($constantThousandsTypes) !== 1 || $constantThousandsTypes[0]->getValue() !== '') { - return $stringType; + return false; + } + + $constantScalarValues = $scope->getType($args[2]->value)->getConstantScalarValues(); + + return count($constantScalarValues) === 1 && in_array($constantScalarValues[0], [null, '.', ''], true); + } + + /** + * With at least one decimal the decimal separator always ends up between digits, + * so a separator that cannot occur in a decimal-int-string rules the whole result out. + * + * @param array $args + */ + private function isNonDecimalIntegerString(array $args, Scope $scope): bool + { + if (!isset($args[1])) { + return false; + } + + if (!IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($scope->getType($args[1]->value))->yes()) { + return false; + } + + if (!isset($args[2])) { + return true; + } + + $decimalSeparatorType = $scope->getType($args[2]->value); + if ($decimalSeparatorType->isNull()->yes()) { + return true; + } + + $constantSeparators = $decimalSeparatorType->getConstantStrings(); + if (count($constantSeparators) === 0) { + return false; } - $constantScalarValues = $decimalType->getConstantScalarValues(); - if (count($constantScalarValues) !== 1 || !in_array($constantScalarValues[0], [null, '.', ''], true)) { - return $stringType; + foreach ($constantSeparators as $constantSeparator) { + if (DecimalIntegerStringHelper::canBeInside($constantSeparator->getValue(), false)) { + return false; + } } - return new IntersectionType([ - $stringType, - new AccessoryNumericStringType(), - ]); + return true; } } diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index 53a93c1dc21..02fb5547a4a 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -9,6 +9,7 @@ use PHPStan\Internal\CombinationsHelper; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; @@ -16,9 +17,9 @@ use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DecimalIntegerStringHelper; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -33,7 +34,9 @@ use function is_array; use function is_string; use function preg_match; +use function preg_split; use function sprintf; +use function str_contains; use function substr; use function vsprintf; @@ -41,6 +44,9 @@ final class SprintfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + /** The printf format is %[argnum$][flags][width][.precision]specifier, plus %% for a literal %. */ + private const CONVERSION_SPECIFICATION_REGEX = '/%(?:[0-9]+\\$)?(?:[-+ 0]|\'.)*(?:[0-9]+)?(?:\\.[0-9]*)?[bcdeEfFgGhHosuxX]|%%/'; + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array($functionReflection->getName(), ['sprintf', 'vsprintf'], true); @@ -72,6 +78,16 @@ public function getTypeFromFunctionCall( static fn (Type $type): bool => $type->toString()->isLowercaseString()->yes(), ); + $isNonDecimalIntString = count($formatStrings) !== 0; + foreach ($formatStrings as $constantString) { + if ($this->isNonDecimalIntegerStringFormat($constantString->getValue())) { + continue; + } + + $isNonDecimalIntString = false; + break; + } + $singlePlaceholderEarlyReturn = []; $allPatternsNonEmpty = count($formatStrings) !== 0; $allPatternsNonFalsy = count($formatStrings) !== 0; @@ -146,6 +162,7 @@ public function getTypeFromFunctionCall( $singlePlaceholderEarlyReturn[] = $this->getStringReturnType( new AccessoryNumericStringType(), $isLowercase, + false, ); } @@ -160,7 +177,7 @@ public function getTypeFromFunctionCall( } if ($allPatternsNonFalsy) { - return $this->getStringReturnType(new AccessoryNonFalsyStringType(), $isLowercase); + return $this->getStringReturnType(new AccessoryNonFalsyStringType(), $isLowercase, $isNonDecimalIntString); } $isNonEmpty = $allPatternsNonEmpty; @@ -174,10 +191,10 @@ public function getTypeFromFunctionCall( } if ($isNonEmpty) { - return $this->getStringReturnType(new AccessoryNonEmptyStringType(), $isLowercase); + return $this->getStringReturnType(new AccessoryNonEmptyStringType(), $isLowercase, $isNonDecimalIntString); } - return $this->getStringReturnType(null, $isLowercase); + return $this->getStringReturnType(null, $isLowercase, $isNonDecimalIntString); } /** @@ -353,7 +370,7 @@ private function getConstantType(array $args, FunctionReflection $functionReflec return TypeCombinator::union(...$returnTypes); } - private function getStringReturnType(?AccessoryType $accessoryType, bool $isLowercase): Type + private function getStringReturnType(?AccessoryType $accessoryType, bool $isLowercase, bool $isNonDecimalIntString): Type { $accessoryTypes = []; if ($accessoryType !== null) { @@ -362,6 +379,9 @@ private function getStringReturnType(?AccessoryType $accessoryType, bool $isLowe if ($isLowercase) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } + if ($isNonDecimalIntString) { + $accessoryTypes[] = new AccessoryDecimalIntegerStringType(inverse: true); + } if (count($accessoryTypes) === 0) { return new StringType(); @@ -369,7 +389,42 @@ private function getStringReturnType(?AccessoryType $accessoryType, bool $isLowe $accessoryTypes[] = new StringType(); - return new IntersectionType($accessoryTypes); + return TypeCombinator::intersect(...$accessoryTypes); + } + + /** + * Whether the literal parts of the format - the ones that end up in the result no matter + * what the values are - make the result a non-decimal-int-string. + */ + private function isNonDecimalIntegerStringFormat(string $format): bool + { + $literalParts = preg_split(self::CONVERSION_SPECIFICATION_REGEX, $format); + if ($literalParts === false) { + return false; + } + + foreach ($literalParts as $literalPart) { + // a conversion specification we failed to recognize would be mistaken for literal text + if (str_contains($literalPart, '%')) { + return false; + } + } + + foreach ($literalParts as $i => $literalPart) { + // every conversion specification can produce an empty string, so anything but the + // first literal part can still end up at the very beginning of the result + $canBePartOfDecimalIntegerString = $i === 0 + ? DecimalIntegerStringHelper::canStart($literalPart, true) + : DecimalIntegerStringHelper::canBeInside($literalPart, true); + + if ($canBePartOfDecimalIntegerString) { + continue; + } + + return true; + } + + return false; } } diff --git a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php index 5f3b7f9ecbe..35f07f0575c 100644 --- a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -7,6 +7,7 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; @@ -14,7 +15,6 @@ use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -154,6 +154,17 @@ public function getTypeFromFunctionCall( $accessoryTypes[] = new AccessoryUppercaseStringType(); } + // mb_convert_kana is the only one of these functions that can change digits, + // converting them between their half-width and full-width forms + if ($fnName !== 'mb_convert_kana') { + $isDecimalIntegerString = $argStringType->isDecimalIntegerString(); + if ($isDecimalIntegerString->yes()) { + $accessoryTypes[] = new AccessoryDecimalIntegerStringType(); + } elseif ($isDecimalIntegerString->no()) { + $accessoryTypes[] = new AccessoryDecimalIntegerStringType(inverse: true); + } + } + if ($argStringType->isNumericString()->yes()) { $accessoryTypes[] = new AccessoryNumericStringType(); } elseif ($argStringType->isNonFalsyString()->yes()) { @@ -165,7 +176,7 @@ public function getTypeFromFunctionCall( if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); - return new IntersectionType($accessoryTypes); + return TypeCombinator::intersect(...$accessoryTypes); } return new StringType(); diff --git a/tests/PHPStan/Analyser/nsrt/binary.php b/tests/PHPStan/Analyser/nsrt/binary.php index ce3a13e6cc6..434ade5ccf4 100644 --- a/tests/PHPStan/Analyser/nsrt/binary.php +++ b/tests/PHPStan/Analyser/nsrt/binary.php @@ -395,8 +395,8 @@ public function doFoo(array $generalArray) assertType('\'a\'', min('a', 'b')); assertType('DateTimeImmutable', max(new \DateTimeImmutable("today"), new \DateTimeImmutable("tomorrow"))); assertType('1', min(1, 2.2, 3.3)); - assertType('non-falsy-string', "Hello $world"); - assertType('non-falsy-string', $string .= "str"); + assertType('non-decimal-int-string&non-falsy-string', "Hello $world"); + assertType('non-decimal-int-string&non-falsy-string', $string .= "str"); assertType('int', $integer5 <<= 2.2); assertType('int', $float7 >>= 2.2); assertType('3', count($arrayOfIntegers)); @@ -515,7 +515,7 @@ public function doFoo(array $generalArray) assertType('\'\'|\'f\'|\'o\'', $fooString[$integer]); assertType('\'foo bar\'', $foobarString); assertType('\'foo bar\'', "$fooString bar"); - assertType('non-falsy-string', "$std bar"); + assertType('non-decimal-int-string&non-falsy-string', "$std bar"); assertType('non-empty-array<\'foo\'|int|stdClass>', $arrToPush); assertType('non-empty-array<\'foo\'|int|stdClass>', $arrToPush2); assertType('array{0: \'lorem\', 1: 5, foo: stdClass, 2: \'test\'}', $arrToUnshift); diff --git a/tests/PHPStan/Analyser/nsrt/bug-10863.php b/tests/PHPStan/Analyser/nsrt/bug-10863.php index c88d3543824..665848d6875 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10863.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10863.php @@ -12,7 +12,7 @@ class Foo */ public function doFoo($b): void { - assertType('lowercase-string&non-falsy-string&uppercase-string', '@' . $b); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', '@' . $b); } /** @@ -20,7 +20,7 @@ public function doFoo($b): void */ public function doFoo2($b): void { - assertType('lowercase-string&non-falsy-string&uppercase-string', '@' . $b); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', '@' . $b); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11129.php b/tests/PHPStan/Analyser/nsrt/bug-11129.php index 33007584be5..ac2e201ceeb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11129.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11129.php @@ -21,13 +21,13 @@ public function foo( $maybeNegativeConstStrings, $maybeNonNumericConstStrings, $maybeFloatConstStrings, bool $bool, float $float ): void { - assertType('lowercase-string&non-falsy-string&uppercase-string', '0'.$i); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', '0'.$i); assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $i.'0'); - assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', '0'.$positiveInt); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&numeric-string&uppercase-string', '0'.$positiveInt); assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $positiveInt.'0'); - assertType('lowercase-string&non-falsy-string&uppercase-string', '0'.$negativeInt); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', '0'.$negativeInt); assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $negativeInt.'0'); assertType("'00'|'01'|'02'", '0'.$positiveConstStrings); @@ -52,15 +52,15 @@ public function foo( assertType('lowercase-string&non-falsy-string&uppercase-string', $maybeFloatConstStrings.$i); assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $i.'1'); - assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $i.'1.0'); - assertType('lowercase-string&non-falsy-string&uppercase-string', $i.'1.1.1'); - assertType('lowercase-string&non-falsy-string&uppercase-string', $i.'-1'); - assertType('lowercase-string&non-falsy-string&uppercase-string', $i.'-1.0'); - assertType('lowercase-string&non-falsy-string&numeric-string', $i.'10e-3'); - assertType('lowercase-string&non-falsy-string', $i.'-10e-3'); - assertType('non-falsy-string&numeric-string&uppercase-string', $i.'10E3'); - assertType('non-falsy-string&uppercase-string', $i.'-10E3'); - assertType('non-falsy-string', $i.'10eE3'); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&numeric-string&uppercase-string', $i.'1.0'); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', $i.'1.1.1'); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', $i.'-1'); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', $i.'-1.0'); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&numeric-string', $i.'10e-3'); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', $i.'-10e-3'); + assertType('non-decimal-int-string&non-falsy-string&numeric-string&uppercase-string', $i.'10E3'); + assertType('non-decimal-int-string&non-falsy-string&uppercase-string', $i.'-10E3'); + assertType('non-decimal-int-string&non-falsy-string', $i.'10eE3'); assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $i.$bool); assertType('lowercase-string&non-empty-string&uppercase-string', $bool.$i); @@ -85,10 +85,10 @@ public function foo( // https://3v4l.org/Ia4r0 $scientificFloatAsString = '3e4'; - assertType('non-falsy-string', $numericString.$scientificFloatAsString); - assertType('lowercase-string&non-falsy-string&numeric-string', $i.$scientificFloatAsString); - assertType('non-falsy-string', $scientificFloatAsString.$numericString); - assertType('lowercase-string&non-falsy-string', $scientificFloatAsString.$i); + assertType('non-decimal-int-string&non-falsy-string', $numericString.$scientificFloatAsString); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&numeric-string', $i.$scientificFloatAsString); + assertType('non-decimal-int-string&non-falsy-string', $scientificFloatAsString.$numericString); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', $scientificFloatAsString.$i); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-15055.php b/tests/PHPStan/Analyser/nsrt/bug-15055.php new file mode 100644 index 00000000000..738d96ca702 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15055.php @@ -0,0 +1,142 @@ += 8.1 +declare(strict_types = 1); + +namespace Bug15055; + +use function PHPStan\Testing\assertType; + +enum StepType: string +{ + + case Action = 'action'; + case Event = 'event'; + +} + +function literalPrefix(int $id): void +{ + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', 'step_' . $id); +} + +function enumPrefix(StepType $type, int $id): void +{ + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', $type->value . '_' . $id); +} + +function literalSuffix(int $id): void +{ + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', $id . '_step'); +} + +function interpolated(int $id): void +{ + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', "step_$id"); +} + +function assignOp(int $id): void +{ + $key = 'step'; + $key .= $id; + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', $key); +} + +/** @param non-decimal-int-string&non-empty-string $prefix */ +function mustStayString(string $prefix, int $id): void +{ + // unsound to infer non-decimal-int-string here: '-' is a non-decimal-int-string + // while '-' . 1 is the decimal-int-string '-1' + assertType('non-falsy-string', $prefix . $id); +} + +function leadingMinusStaysString(int $id): void +{ + assertType('lowercase-string&non-falsy-string&uppercase-string', '-' . $id); + assertType('lowercase-string&non-falsy-string&uppercase-string', '1' . $id); +} + +function leadingZeroIsNeverCanonical(int $id): void +{ + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', '0' . $id); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', '00' . $id); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', '-0' . $id); +} + +function minusInTheMiddle(int $id, string $s): void +{ + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', $id . '-1'); + // $s can be '', so the result can be the decimal-int-string '-1' + assertType('non-falsy-string', $s . '-1'); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $id . '007'); + // $s can be '', so the result can be the decimal-int-string '0' + assertType('non-empty-string', '0' . $s); +} + +/** @param ''|'foo' $x */ +function possiblyEmptyConstantUnion(string $x, int $id): void +{ + assertType('lowercase-string&non-empty-string', $x . $id); +} + +/** + * @param non-empty-list $ints + * @param array{int, int} $pair + * @param non-empty-list<'foo'|'bar'> $words + */ +function implodeSeparator(array $ints, array $pair, array $words): void +{ + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', implode('_', $pair)); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string&uppercase-string', implode('-', $pair)); + // a single element makes the separator disappear + assertType('lowercase-string&non-falsy-string&uppercase-string', implode('_', $ints)); + assertType('lowercase-string&non-empty-string&uppercase-string', implode('', $ints)); + assertType('literal-string&lowercase-string&non-decimal-int-string&non-falsy-string', implode('', $words)); +} + +function sprintfLiteralParts(int $id, string $s): void +{ + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', sprintf('step_%d', $id)); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', sprintf('%d_step', $id)); + assertType('non-decimal-int-string&non-falsy-string', sprintf('%s.%s', $s, $s)); + // '-' . '1' is the decimal-int-string '-1' + assertType('lowercase-string&numeric-string', sprintf('%d', $id)); + assertType('lowercase-string&non-falsy-string', sprintf('-%d', $id)); + assertType('non-falsy-string', sprintf('%s-%s', $s, $s)); + // the padding of '%05d' is not part of the literal text + assertType('lowercase-string&numeric-string', sprintf('%05d', $id)); +} + +function numberFormatDecimalSeparator(float $x, int $decimals): void +{ + assertType('non-decimal-int-string', number_format($x, 2)); + assertType('non-decimal-int-string&numeric-string', number_format($x, 2, '.', '')); + assertType('non-decimal-int-string&numeric-string', number_format($x, 2, null, '')); + // without a decimal separator the digits end up next to each other + assertType('numeric-string', number_format($x, 2, '', '')); + assertType('string', number_format($x)); + assertType('string', number_format($x, $decimals, '.')); +} + +/** + * @param non-decimal-int-string $nds + * @param decimal-int-string $ds + */ +function caseFunctionsKeepDigits(string $nds, string $ds): void +{ + assertType('lowercase-string&non-decimal-int-string', strtolower($nds)); + assertType('non-decimal-int-string&uppercase-string', strtoupper($nds)); + assertType('non-decimal-int-string', ucfirst($nds)); + assertType('lowercase-string&non-decimal-int-string', mb_strtolower($nds)); + assertType('non-decimal-int-string', ucwords($nds)); + assertType('decimal-int-string', strtolower($ds)); + // mb_convert_kana can convert digits to their full-width form + assertType('string', mb_convert_kana($nds)); +} + +/** @param non-decimal-int-string $nds */ +function arithmeticOnNonDecimalIntString(string $nds): void +{ + // a non-decimal-int-string can be an arbitrary non-numeric string + assertType('*ERROR*', 1 + $nds); + assertType('*ERROR*', -$nds); + assertType('*ERROR*', $nds & 3); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3379.php b/tests/PHPStan/Analyser/nsrt/bug-3379.php index 4500775f5ae..1b912a56c83 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3379.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3379.php @@ -13,5 +13,5 @@ class Foo function () { echo Foo::URL; - assertType('non-falsy-string', Foo::URL); + assertType('non-decimal-int-string&non-falsy-string', Foo::URL); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-5168-php8.php b/tests/PHPStan/Analyser/nsrt/bug-5168-php8.php index 52623ac4357..add07c3e99e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5168-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5168-php8.php @@ -8,5 +8,5 @@ function (float $f): void { define('LARAVEL_START', microtime(true)); $comment = 'Calculated in ' . microtime(true) - $f; - assertType('non-falsy-string', $comment); + assertType('non-decimal-int-string&non-falsy-string', $comment); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7387.php b/tests/PHPStan/Analyser/nsrt/bug-7387.php index c0f518ff43b..8e6ce4c1a23 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7387.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7387.php @@ -102,7 +102,7 @@ public function invalidPositionalArgFormat($mixed, string $s) { public function escapedPercent(int $i) { // https://3v4l.org/2m50L - assertType('lowercase-string&non-falsy-string', sprintf("%%d", $i)); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', sprintf("%%d", $i)); } public function vsprintf(array $array) diff --git a/tests/PHPStan/Analyser/nsrt/bug-8568.php b/tests/PHPStan/Analyser/nsrt/bug-8568.php index 9236447acf6..853118285da 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-8568.php +++ b/tests/PHPStan/Analyser/nsrt/bug-8568.php @@ -8,7 +8,7 @@ class HelloWorld { public function sayHello(): void { - assertType('lowercase-string&non-falsy-string', 'a' . $this->get()); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', 'a' . $this->get()); } public function get(): ?int @@ -20,7 +20,7 @@ public function get(): ?int * @param numeric-string $numericS */ public function intersections($numericS): void { - assertType('non-falsy-string', 'a'. $numericS); + assertType('non-decimal-int-string&non-falsy-string', 'a'. $numericS); assertType('numeric-string', (string) $numericS); } } diff --git a/tests/PHPStan/Analyser/nsrt/constant-string-unions.php b/tests/PHPStan/Analyser/nsrt/constant-string-unions.php index ee236b553e6..9ba70ccfec3 100644 --- a/tests/PHPStan/Analyser/nsrt/constant-string-unions.php +++ b/tests/PHPStan/Analyser/nsrt/constant-string-unions.php @@ -74,7 +74,7 @@ public function testLimit(string $s15, string $s16, string $s17, string $suffix) // union should contain 128 elements assertType("'1'|'10'|'10a'|'10b'|'10c'|'10d'|'10e'|'10f'|'10g'|'10h'|'11'|'11a'|'11b'|'11c'|'11d'|'11e'|'11f'|'11g'|'11h'|'12'|'12a'|'12b'|'12c'|'12d'|'12e'|'12f'|'12g'|'12h'|'13'|'13a'|'13b'|'13c'|'13d'|'13e'|'13f'|'13g'|'13h'|'14'|'14a'|'14b'|'14c'|'14d'|'14e'|'14f'|'14g'|'14h'|'15'|'15a'|'15b'|'15c'|'15d'|'15e'|'15f'|'15g'|'15h'|'16'|'16a'|'16b'|'16c'|'16d'|'16e'|'16f'|'16g'|'16h'|'1a'|'1b'|'1c'|'1d'|'1e'|'1f'|'1g'|'1h'|'2'|'2a'|'2b'|'2c'|'2d'|'2e'|'2f'|'2g'|'2h'|'3'|'3a'|'3b'|'3c'|'3d'|'3e'|'3f'|'3g'|'3h'|'4'|'4a'|'4b'|'4c'|'4d'|'4e'|'4f'|'4g'|'4h'|'5'|'5a'|'5b'|'5c'|'5d'|'5e'|'5f'|'5g'|'5h'|'6'|'6a'|'6b'|'6c'|'6d'|'6e'|'6f'|'6g'|'6h'|'7'|'7a'|'7b'|'7c'|'7d'|'7e'|'7f'|'7g'|'7h'|'8'|'8a'|'8b'|'8c'|'8d'|'8e'|'8f'|'8g'|'8h'|'9'|'9a'|'9b'|'9c'|'9d'|'9e'|'9f'|'9g'|'9h'", $s16); // fallback to the more general form - assertType("literal-string&lowercase-string&non-falsy-string", $s17); + assertType("'1'|'10'|'11'|'12'|'13'|'14'|'15'|'16'|'17'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|(literal-string&lowercase-string&non-decimal-int-string&non-falsy-string)", $s17); $left = rand() ? 'a' : 'b'; $right = rand() ? 'x' : 'y'; @@ -86,7 +86,7 @@ public function testLimit(string $s15, string $s16, string $s17, string $suffix) $left .= $right; assertType("'axxxxxx'|'axxxxxy'|'axxxxyx'|'axxxxyy'|'axxxyxx'|'axxxyxy'|'axxxyyx'|'axxxyyy'|'axxyxxx'|'axxyxxy'|'axxyxyx'|'axxyxyy'|'axxyyxx'|'axxyyxy'|'axxyyyx'|'axxyyyy'|'axyxxxx'|'axyxxxy'|'axyxxyx'|'axyxxyy'|'axyxyxx'|'axyxyxy'|'axyxyyx'|'axyxyyy'|'axyyxxx'|'axyyxxy'|'axyyxyx'|'axyyxyy'|'axyyyxx'|'axyyyxy'|'axyyyyx'|'axyyyyy'|'ayxxxxx'|'ayxxxxy'|'ayxxxyx'|'ayxxxyy'|'ayxxyxx'|'ayxxyxy'|'ayxxyyx'|'ayxxyyy'|'ayxyxxx'|'ayxyxxy'|'ayxyxyx'|'ayxyxyy'|'ayxyyxx'|'ayxyyxy'|'ayxyyyx'|'ayxyyyy'|'ayyxxxx'|'ayyxxxy'|'ayyxxyx'|'ayyxxyy'|'ayyxyxx'|'ayyxyxy'|'ayyxyyx'|'ayyxyyy'|'ayyyxxx'|'ayyyxxy'|'ayyyxyx'|'ayyyxyy'|'ayyyyxx'|'ayyyyxy'|'ayyyyyx'|'ayyyyyy'|'bxxxxxx'|'bxxxxxy'|'bxxxxyx'|'bxxxxyy'|'bxxxyxx'|'bxxxyxy'|'bxxxyyx'|'bxxxyyy'|'bxxyxxx'|'bxxyxxy'|'bxxyxyx'|'bxxyxyy'|'bxxyyxx'|'bxxyyxy'|'bxxyyyx'|'bxxyyyy'|'bxyxxxx'|'bxyxxxy'|'bxyxxyx'|'bxyxxyy'|'bxyxyxx'|'bxyxyxy'|'bxyxyyx'|'bxyxyyy'|'bxyyxxx'|'bxyyxxy'|'bxyyxyx'|'bxyyxyy'|'bxyyyxx'|'bxyyyxy'|'bxyyyyx'|'bxyyyyy'|'byxxxxx'|'byxxxxy'|'byxxxyx'|'byxxxyy'|'byxxyxx'|'byxxyxy'|'byxxyyx'|'byxxyyy'|'byxyxxx'|'byxyxxy'|'byxyxyx'|'byxyxyy'|'byxyyxx'|'byxyyxy'|'byxyyyx'|'byxyyyy'|'byyxxxx'|'byyxxxy'|'byyxxyx'|'byyxxyy'|'byyxyxx'|'byyxyxy'|'byyxyyx'|'byyxyyy'|'byyyxxx'|'byyyxxy'|'byyyxyx'|'byyyxyy'|'byyyyxx'|'byyyyxy'|'byyyyyx'|'byyyyyy'", $left); $left .= $right; - assertType("literal-string&lowercase-string&non-falsy-string", $left); + assertType("literal-string&lowercase-string&non-decimal-int-string&non-falsy-string", $left); $left = rand() ? 'a' : 'b'; $right = rand() ? 'x' : 'y'; @@ -98,7 +98,7 @@ public function testLimit(string $s15, string $s16, string $s17, string $suffix) $left = "{$left}{$right}"; assertType("'axxxxxx'|'axxxxxy'|'axxxxyx'|'axxxxyy'|'axxxyxx'|'axxxyxy'|'axxxyyx'|'axxxyyy'|'axxyxxx'|'axxyxxy'|'axxyxyx'|'axxyxyy'|'axxyyxx'|'axxyyxy'|'axxyyyx'|'axxyyyy'|'axyxxxx'|'axyxxxy'|'axyxxyx'|'axyxxyy'|'axyxyxx'|'axyxyxy'|'axyxyyx'|'axyxyyy'|'axyyxxx'|'axyyxxy'|'axyyxyx'|'axyyxyy'|'axyyyxx'|'axyyyxy'|'axyyyyx'|'axyyyyy'|'ayxxxxx'|'ayxxxxy'|'ayxxxyx'|'ayxxxyy'|'ayxxyxx'|'ayxxyxy'|'ayxxyyx'|'ayxxyyy'|'ayxyxxx'|'ayxyxxy'|'ayxyxyx'|'ayxyxyy'|'ayxyyxx'|'ayxyyxy'|'ayxyyyx'|'ayxyyyy'|'ayyxxxx'|'ayyxxxy'|'ayyxxyx'|'ayyxxyy'|'ayyxyxx'|'ayyxyxy'|'ayyxyyx'|'ayyxyyy'|'ayyyxxx'|'ayyyxxy'|'ayyyxyx'|'ayyyxyy'|'ayyyyxx'|'ayyyyxy'|'ayyyyyx'|'ayyyyyy'|'bxxxxxx'|'bxxxxxy'|'bxxxxyx'|'bxxxxyy'|'bxxxyxx'|'bxxxyxy'|'bxxxyyx'|'bxxxyyy'|'bxxyxxx'|'bxxyxxy'|'bxxyxyx'|'bxxyxyy'|'bxxyyxx'|'bxxyyxy'|'bxxyyyx'|'bxxyyyy'|'bxyxxxx'|'bxyxxxy'|'bxyxxyx'|'bxyxxyy'|'bxyxyxx'|'bxyxyxy'|'bxyxyyx'|'bxyxyyy'|'bxyyxxx'|'bxyyxxy'|'bxyyxyx'|'bxyyxyy'|'bxyyyxx'|'bxyyyxy'|'bxyyyyx'|'bxyyyyy'|'byxxxxx'|'byxxxxy'|'byxxxyx'|'byxxxyy'|'byxxyxx'|'byxxyxy'|'byxxyyx'|'byxxyyy'|'byxyxxx'|'byxyxxy'|'byxyxyx'|'byxyxyy'|'byxyyxx'|'byxyyxy'|'byxyyyx'|'byxyyyy'|'byyxxxx'|'byyxxxy'|'byyxxyx'|'byyxxyy'|'byyxyxx'|'byyxyxy'|'byyxyyx'|'byyxyyy'|'byyyxxx'|'byyyxxy'|'byyyxyx'|'byyyxyy'|'byyyyxx'|'byyyyxy'|'byyyyyx'|'byyyyyy'", $left); $left = "{$left}{$right}"; - assertType("literal-string&lowercase-string&non-falsy-string", $left); + assertType("literal-string&lowercase-string&non-decimal-int-string&non-falsy-string", $left); } /** diff --git a/tests/PHPStan/Analyser/nsrt/decimal-int-string.php b/tests/PHPStan/Analyser/nsrt/decimal-int-string.php index 171e8972e92..05e10711103 100644 --- a/tests/PHPStan/Analyser/nsrt/decimal-int-string.php +++ b/tests/PHPStan/Analyser/nsrt/decimal-int-string.php @@ -32,7 +32,7 @@ public function doBar(string $s): void assertType('bool', (bool) $s); - assertType('float|int', $s + $s); + assertType('*ERROR*', $s + $s); } public function doBaz(string $s): void diff --git a/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php b/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php index 3555613fe04..e4e84f86206 100644 --- a/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php +++ b/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php @@ -33,7 +33,7 @@ public function integerRange(int $a, string $b): void */ public function tooBigRange(int $a, string $b): void { - assertType("lowercase-string&non-falsy-string", sprintf('%d %s', $a, $b)); + assertType("lowercase-string&non-decimal-int-string&non-falsy-string", sprintf('%d %s', $a, $b)); } } diff --git a/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php b/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php index a1d72792523..6c302c65a97 100644 --- a/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php +++ b/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php @@ -29,7 +29,7 @@ public function sayHello(\stdClass $s): void foreach ($s as $k => $v) { $a .= 'test'; } - assertType('(literal-string&lowercase-string&non-falsy-string)|null', $a); + assertType('(literal-string&lowercase-string&non-decimal-int-string&non-falsy-string)|null', $a); } } diff --git a/tests/PHPStan/Analyser/nsrt/implode.php b/tests/PHPStan/Analyser/nsrt/implode.php index ecd36a0570f..29f50ffea21 100644 --- a/tests/PHPStan/Analyser/nsrt/implode.php +++ b/tests/PHPStan/Analyser/nsrt/implode.php @@ -75,7 +75,7 @@ public function unsealedConstArr($unsealed) { // the exact constant fold `'a,b'` is unsound. The result keeps only // what's guaranteed: with a non-falsy separator and at least two // explicit elements, the output always contains a comma. - assertType('non-falsy-string', implode(',', $unsealed)); + assertType('non-decimal-int-string&non-falsy-string', implode(',', $unsealed)); } /** @param array{'a', 'b', ...} $unsealed */ diff --git a/tests/PHPStan/Analyser/nsrt/literal-string.php b/tests/PHPStan/Analyser/nsrt/literal-string.php index eb52d7ba5c7..a654218b0c0 100644 --- a/tests/PHPStan/Analyser/nsrt/literal-string.php +++ b/tests/PHPStan/Analyser/nsrt/literal-string.php @@ -18,10 +18,10 @@ public function doFoo($literalString, string $string, $numericString) assertType('literal-string', '' . $literalString); assertType('literal-string&non-empty-string', $literalString . '0'); assertType('literal-string&non-empty-string', '0' . $literalString); - assertType('literal-string&non-falsy-string', $literalString . 'foo'); - assertType('literal-string&non-falsy-string', 'foo' . $literalString); - assertType('literal-string&non-falsy-string', "foo ${literalString}"); - assertType('literal-string&non-falsy-string', "${literalString} foo"); + assertType('literal-string&non-decimal-int-string&non-falsy-string', $literalString . 'foo'); + assertType('literal-string&non-decimal-int-string&non-falsy-string', 'foo' . $literalString); + assertType('literal-string&non-decimal-int-string&non-falsy-string', "foo ${literalString}"); + assertType('literal-string&non-decimal-int-string&non-falsy-string', "${literalString} foo"); assertType('string', $string . ''); assertType('string', '' . $string); assertType('string', $literalString . $string); diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-sprintf.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-sprintf.php index db7127a15c6..25143bb0259 100644 --- a/tests/PHPStan/Analyser/nsrt/lowercase-string-sprintf.php +++ b/tests/PHPStan/Analyser/nsrt/lowercase-string-sprintf.php @@ -40,22 +40,22 @@ public function doSprintf( assertType('lowercase-string', sprintf('%s', $lowercase)); assertType('lowercase-string&numeric-string', sprintf('%d', $lowercase)); - assertType('non-falsy-string', sprintf($format, $lowercase)); - assertType('lowercase-string&non-falsy-string', sprintf($formatLower, $lowercase)); + assertType('non-decimal-int-string&non-falsy-string', sprintf($format, $lowercase)); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', sprintf($formatLower, $lowercase)); assertType('lowercase-string', sprintf($lowercase, $lowercase)); assertType('string', sprintf($string, $lowercase)); assertType('lowercase-string&non-empty-string', sprintf('%s', $nonEmptyLowercase)); assertType('lowercase-string&numeric-string', sprintf('%d', $nonEmptyLowercase)); - assertType('non-falsy-string', sprintf($format, $nonEmptyLowercase)); - assertType('lowercase-string&non-falsy-string', sprintf($formatLower, $nonEmptyLowercase)); + assertType('non-decimal-int-string&non-falsy-string', sprintf($format, $nonEmptyLowercase)); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', sprintf($formatLower, $nonEmptyLowercase)); assertType('lowercase-string&non-empty-string', sprintf($nonEmptyLowercase, $nonEmptyLowercase)); assertType('string', sprintf($string, $nonEmptyLowercase)); assertType('lowercase-string&non-falsy-string', sprintf('%s', $nonFalsyLowercase)); assertType('lowercase-string&numeric-string', sprintf('%d', $nonFalsyLowercase)); - assertType('non-falsy-string', sprintf($format, $nonFalsyLowercase)); - assertType('lowercase-string&non-falsy-string', sprintf($formatLower, $nonFalsyLowercase)); + assertType('non-decimal-int-string&non-falsy-string', sprintf($format, $nonFalsyLowercase)); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', sprintf($formatLower, $nonFalsyLowercase)); assertType('lowercase-string&non-empty-string', sprintf($nonFalsyLowercase, $nonFalsyLowercase)); assertType('string', sprintf($string, $nonFalsyLowercase)); } @@ -79,36 +79,36 @@ public function doVSprintf( assertType("'A'|'B'", vsprintf('%s', [$constant])); assertType('numeric-string', vsprintf('%d', [$constant])); - assertType('non-falsy-string', vsprintf($format, [$constant])); - assertType('non-falsy-string', vsprintf($formatLower, [$constant])); + assertType('non-decimal-int-string&non-falsy-string', vsprintf($format, [$constant])); + assertType('non-decimal-int-string&non-falsy-string', vsprintf($formatLower, [$constant])); assertType('string', vsprintf($lowercase, [$constant])); assertType('string', vsprintf($string, [$constant])); assertType("'a'|'b'", vsprintf('%s', [$constantLower])); assertType('lowercase-string&numeric-string', vsprintf('%d', [$constantLower])); - assertType('non-falsy-string', vsprintf($format, [$constantLower])); - assertType('lowercase-string&non-falsy-string', vsprintf($formatLower, [$constantLower])); + assertType('non-decimal-int-string&non-falsy-string', vsprintf($format, [$constantLower])); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', vsprintf($formatLower, [$constantLower])); assertType('lowercase-string', vsprintf($lowercase, [$constantLower])); assertType('string', vsprintf($string, [$constantLower])); assertType('lowercase-string', vsprintf('%s', [$lowercase])); assertType('lowercase-string&numeric-string', vsprintf('%d', [$lowercase])); - assertType('non-falsy-string', vsprintf($format, [$lowercase])); - assertType('lowercase-string&non-falsy-string', vsprintf($formatLower, [$lowercase])); + assertType('non-decimal-int-string&non-falsy-string', vsprintf($format, [$lowercase])); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', vsprintf($formatLower, [$lowercase])); assertType('lowercase-string', vsprintf($lowercase, [$lowercase])); assertType('string', vsprintf($string, [$lowercase])); assertType('lowercase-string&non-empty-string', vsprintf('%s', [$nonEmptyLowercase])); assertType('lowercase-string&numeric-string', vsprintf('%d', [$nonEmptyLowercase])); - assertType('non-falsy-string', vsprintf($format, [$nonEmptyLowercase])); - assertType('lowercase-string&non-falsy-string', vsprintf($formatLower, [$nonEmptyLowercase])); + assertType('non-decimal-int-string&non-falsy-string', vsprintf($format, [$nonEmptyLowercase])); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', vsprintf($formatLower, [$nonEmptyLowercase])); assertType('lowercase-string&non-empty-string', vsprintf($nonEmptyLowercase, [$nonEmptyLowercase])); assertType('string', vsprintf($string, [$nonEmptyLowercase])); assertType('lowercase-string&non-falsy-string', vsprintf('%s', [$nonFalsyLowercase])); assertType('lowercase-string&numeric-string', vsprintf('%d', [$nonFalsyLowercase])); - assertType('non-falsy-string', vsprintf($format, [$nonFalsyLowercase])); - assertType('lowercase-string&non-falsy-string', vsprintf($formatLower, [$nonFalsyLowercase])); + assertType('non-decimal-int-string&non-falsy-string', vsprintf($format, [$nonFalsyLowercase])); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', vsprintf($formatLower, [$nonFalsyLowercase])); assertType('lowercase-string&non-empty-string', vsprintf($nonFalsyLowercase, [$nonFalsyLowercase])); assertType('string', vsprintf($string, [$nonFalsyLowercase])); } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index c8031310aee..ba594dea052 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -213,8 +213,8 @@ public function sayHello(int $i): void // coming from issue #5291 $s = array(1, $i); - assertType('lowercase-string&non-falsy-string', implode("a", $s)); - assertType('non-falsy-string&uppercase-string', implode("A", $s)); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', implode("a", $s)); + assertType('non-decimal-int-string&non-falsy-string&uppercase-string', implode("A", $s)); } /** @@ -235,7 +235,7 @@ public function sayHello2(int $i): void // coming from issue #5291 $s = array(1, $i); - assertType('lowercase-string&non-falsy-string', join("a", $s)); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', join("a", $s)); } /** @@ -258,7 +258,7 @@ class LiteralString function x(string $tableName, string $original): void { - assertType('non-falsy-string', "from `$tableName`"); + assertType('non-decimal-int-string&non-falsy-string', "from `$tableName`"); } /** @@ -423,7 +423,7 @@ function multiplesPrintfFormats(string $s) { assertType('string', sprintf($maybeNonEmpty, $s)); assertType('string', sprintf($maybeNonFalsy, $s)); assertType('non-empty-string', sprintf($nonEmpty, $s)); - assertType('non-falsy-string', sprintf($nonFalsy, $s)); + assertType('non-decimal-int-string&non-falsy-string', sprintf($nonFalsy, $s)); } function subtract($m) { diff --git a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php index 12ca4c3fac4..a1a902bf089 100644 --- a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -118,17 +118,17 @@ function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArra // empty array only works as long as no placeholder in the pattern assertType('string', vsprintf($nonFalsey, [])); assertType('string', vsprintf($nonFalsey, [])); - assertType("string", vsprintf('foo', [])); + assertType("non-decimal-int-string", vsprintf('foo', [])); assertType("string", vsprintf('%s', ...$arr)); assertType("string", vsprintf(...$arr)); - assertType('non-falsy-string', vsprintf('%sAA%s', [$s, $s])); + assertType('non-decimal-int-string&non-falsy-string', vsprintf('%sAA%s', [$s, $s])); assertType('non-falsy-string', vsprintf('%d%d', [$s, $s])); // could be non-falsy-string&numeric-string - assertType('non-falsy-string', sprintf("%sAA%s", $s, $s)); + assertType('non-decimal-int-string&non-falsy-string', sprintf("%sAA%s", $s, $s)); assertType('non-falsy-string', sprintf("%d%d", $s, $s)); // could be non-falsy-string&numeric-string - assertType('non-falsy-string', sprintf("%sAA%s%s%s%s", $s, $s, $s, $s, $s)); - assertType('non-falsy-string', sprintf("%sAA%s%s%s%s%s", $s, $s, $s, $s, $s, $s)); + assertType('non-decimal-int-string&non-falsy-string', sprintf("%sAA%s%s%s%s", $s, $s, $s, $s, $s)); + assertType('non-decimal-int-string&non-falsy-string', sprintf("%sAA%s%s%s%s%s", $s, $s, $s, $s, $s, $s)); assertType('int<1, max>', strlen($nonFalsey)); diff --git a/tests/PHPStan/Analyser/nsrt/number_format.php b/tests/PHPStan/Analyser/nsrt/number_format.php index eb4d2a81cae..e8d4e6b9e24 100644 --- a/tests/PHPStan/Analyser/nsrt/number_format.php +++ b/tests/PHPStan/Analyser/nsrt/number_format.php @@ -3,16 +3,16 @@ use function PHPStan\Testing\assertType; assertType('string', number_format(1002.7)); -assertType('string', number_format(1002.7, 3)); -assertType('string', number_format(1002.7, 3, null)); -assertType('string', number_format(1002.7, 3, '.')); -assertType('string', number_format(1002.7, 3, '.', ',')); -assertType('string', number_format(1002.7, 3, '.', null)); +assertType('non-decimal-int-string', number_format(1002.7, 3)); +assertType('non-decimal-int-string', number_format(1002.7, 3, null)); +assertType('non-decimal-int-string', number_format(1002.7, 3, '.')); +assertType('non-decimal-int-string', number_format(1002.7, 3, '.', ',')); +assertType('non-decimal-int-string', number_format(1002.7, 3, '.', null)); assertType('string', number_format(1002.7, 3, '', null)); -assertType('string', number_format(1002.7, 3, 'b', null)); -assertType('string', number_format(1002.7, 3, 'b', '')); +assertType('non-decimal-int-string', number_format(1002.7, 3, 'b', null)); +assertType('non-decimal-int-string', number_format(1002.7, 3, 'b', '')); -assertType('numeric-string', number_format(1002.7, 3, '.', '')); -assertType('numeric-string', number_format(1002.7, 3, null, '')); +assertType('non-decimal-int-string&numeric-string', number_format(1002.7, 3, '.', '')); +assertType('non-decimal-int-string&numeric-string', number_format(1002.7, 3, null, '')); assertType('numeric-string', number_format(1002.7, 3, '', '')); diff --git a/tests/PHPStan/Analyser/nsrt/str-casing.php b/tests/PHPStan/Analyser/nsrt/str-casing.php index ebdbd8054d3..a806fb47bf7 100644 --- a/tests/PHPStan/Analyser/nsrt/str-casing.php +++ b/tests/PHPStan/Analyser/nsrt/str-casing.php @@ -30,7 +30,7 @@ public function bar($numericS, $nonE, $lowercaseS, $literal, $edgeUnion, $caseMo assertType("'Hello|World'", ucwords('hello|world', "|")); assertType("'ČESKÁ REPUBLIKA'", mb_convert_case('Česká republika', MB_CASE_UPPER)); assertType("'česká republika'", mb_convert_case('Česká republika', MB_CASE_LOWER)); - assertType("non-falsy-string", mb_convert_case('Česká republika', $mixed)); + assertType("non-decimal-int-string&non-falsy-string", mb_convert_case('Česká republika', $mixed)); assertType("'ČESKÁ REPUBLIKA'|'Česká Republika'|'česká republika'", mb_convert_case('Česká republika', $caseMode)); assertType("'Abc123アイウガギグばびぶ漢字'", mb_convert_kana('Abc123アイウガギグばびぶ漢字')); assertType("'Abc123アイウガギグばびぶ漢字'", mb_convert_kana('Abc123アイウガギグばびぶ漢字', 'aKV')); @@ -96,7 +96,7 @@ public function bar($numericS, $nonE, $lowercaseS, $literal, $edgeUnion, $caseMo public function foo() { // invalid char conversions still lead to non-falsy-string - assertType("lowercase-string&non-falsy-string", mb_strtolower("\xfe\xff\x65\xe5\x67\x2c\x8a\x9e", 'CP1252')); + assertType("lowercase-string&non-decimal-int-string&non-falsy-string", mb_strtolower("\xfe\xff\x65\xe5\x67\x2c\x8a\x9e", 'CP1252')); // valid char sequence, but not support non ASCII / UTF-8 encodings assertType("non-falsy-string", mb_convert_kana("\x95\x5c\x8c\xbb", 'SJIS-win')); // invalid UTF-8 sequence diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-8620.php b/tests/PHPStan/Rules/DeadCode/data/bug-8620.php index 44bc78cd456..5b4e6de815c 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-8620.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-8620.php @@ -9,7 +9,7 @@ class HelloWorld public function nullCoalesceAndConcatenation (?int $a = null): int { $key = ($a ?? "x") . "-"; - assertType('lowercase-string&non-falsy-string', $key); + assertType('lowercase-string&non-decimal-int-string&non-falsy-string', $key); if ($key === "x-") { return 0; } return 1; diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 7bb3906083d..715873cf3db 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2320,7 +2320,7 @@ public function testBug5372(): void $this->checkUnionTypes = true; $this->analyse([__DIR__ . '/data/bug-5372.php'], [ [ - 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', + 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', 64, 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], diff --git a/tests/PHPStan/Rules/Methods/data/bug-5372.php b/tests/PHPStan/Rules/Methods/data/bug-5372.php index 712516416c1..a8fabac1058 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-5372.php +++ b/tests/PHPStan/Rules/Methods/data/bug-5372.php @@ -60,7 +60,7 @@ public function doFoo(string $classString) assertType('Bug5372\Collection', $col); $newCol = $col->map(static fn(string $var): string => $var . 'bar'); - assertType('Bug5372\Collection', $newCol); + assertType('Bug5372\Collection', $newCol); $this->takesStrings($newCol); $newCol = $col->map(static fn(string $var): string => $classString);