From 57841756f681f68e7fa535152a047fe2f71f125c Mon Sep 17 00:00:00 2001 From: malsuke Date: Wed, 25 Dec 2024 12:48:49 +0900 Subject: [PATCH 01/36] feat improve preg_split type Extension --- .../PregSplitDynamicReturnTypeExtension.php | 73 ++++++++++++++++++- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index d51b5314b0..f2abef425a 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -10,9 +10,12 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; @@ -36,9 +39,28 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $flagsArg = $functionCall->getArgs()[3] ?? null; + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; + } + $patternArg = $args[0]; + $subjectArg = $args[1]; + $limitArg = $args[2] ?? null; + $flagArg = $args[3] ?? null; + $patternType = $scope->getType($patternArg->value); + $patternConstantTypes = $patternType->getConstantStrings(); + $subjectType = $scope->getType($subjectArg->value); + $subjectConstantTypes = $subjectType->getConstantStrings(); + + if ( + count($patternConstantTypes) > 0 && + @preg_match($patternConstantTypes[0]->getValue(), "") === false + ) { + + return new ErrorType(); + } - if ($flagsArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagsArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE')->yes()) { + if ($subjectArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($subjectArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE')->yes()) { $type = new ArrayType( new IntegerType(), new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), IntegerRangeType::fromInterval(0, null)], [2], [], TrinaryLogic::createYes()), @@ -46,7 +68,50 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeCombinator::union(TypeCombinator::intersect($type, new AccessoryArrayListType()), new ConstantBooleanType(false)); } - return null; - } + if ($limitArg === null) { + $limits = [-1]; + } else { + $limitType = $scope->getType($limitArg->value); + $limits = $limitType->getConstantScalarValues(); + } + if ($flagArg === null) { + $flags = [0]; + } else { + $flagType = $scope->getType($flagArg->value); + $flags = $flagType->getConstantScalarValues(); + } + + if (count($patternConstantTypes) === 0 || count($subjectConstantTypes) === 0 || count($flags) === 0) { + return null; + } + + $resultTypes = []; + foreach ($patternConstantTypes as $patternConstantType) { + foreach ($subjectConstantTypes as $subjectConstantType) { + foreach ($flags as $flag) { + foreach ($limits as $limit) { + $result = @preg_split($patternConstantType->getValue(), $subjectConstantType->getValue(), $limit, $flag); + if ($result !== false) { + $constantArray = ConstantArrayTypeBuilder::createEmpty(); + foreach ($result as $key => $value) { + assert(is_int($key)); + if (is_array($value)) { + $valueConstantArray = ConstantArrayTypeBuilder::createEmpty(); + $valueConstantArray->setOffsetValueType(new ConstantIntegerType(0), new ConstantStringType($value[0])); + $valueConstantArray->setOffsetValueType(new ConstantIntegerType(1), new ConstantIntegerType($value[1])); + $valueType = $valueConstantArray->getArray(); + } else { + $valueType = new ConstantStringType($value); + } + $constantArray->setOffsetValueType(new ConstantIntegerType($key), $valueType); + } + $resultTypes[] = $constantArray->getArray(); + } + } + } + } + } + return TypeCombinator::union(...$resultTypes); + } } From e595e1ede8b9255d1ea0af44f6d96afb8612462c Mon Sep 17 00:00:00 2001 From: malsuke Date: Wed, 25 Dec 2024 12:49:07 +0900 Subject: [PATCH 02/36] feat add test for varibles --- tests/PHPStan/Analyser/nsrt/preg_split.php | 29 ++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/preg_split.php b/tests/PHPStan/Analyser/nsrt/preg_split.php index 7122c16150..c153b55ec1 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_split.php +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -8,10 +8,25 @@ class HelloWorld { public function doFoo() { - assertType('list|false', preg_split('/-/', '1-2-3')); - assertType('list|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); - assertType('list}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); - assertType('list}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + $aaa = '/[0-9a]'; + assertType('*ERROR*', preg_split($aaa, '1-2-3')); + assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3')); + assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{'1', '3'}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'', 2}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + } + + public function doWithVariables(string $pattern, string $subject, int $offset, int $flags): void + { + assertType('(list|false)', preg_split($pattern, $subject, $offset, $flags)); + assertType('(list|false)', preg_split("//", $subject, $offset, $flags)); + assertType('(list|false)', preg_split($pattern, "1-2-3", $offset, $flags)); + assertType('(list|false)', preg_split($pattern, $subject, -1, $flags)); + assertType('(list|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_NO_EMPTY)); + assertType('list}>', preg_split($pattern, $subject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); } /** @@ -24,10 +39,10 @@ public function doFoo() */ public static function splitWithOffset($pattern, $subject, $limit = -1, $flags = 0) { - assertType('list}>|false', preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE)); - assertType('list}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags)); + assertType('list}>', preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('list}>', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags)); - assertType('list}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags | PREG_SPLIT_NO_EMPTY)); + assertType('list}>', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags | PREG_SPLIT_NO_EMPTY)); } /** From 0f52c6193e978299dfb983bf96384ca2e5d8223b Mon Sep 17 00:00:00 2001 From: malsuke Date: Wed, 25 Dec 2024 12:49:32 +0900 Subject: [PATCH 03/36] feat add benevolent type to preg_split --- resources/functionMap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index bf078bc8a0..067251cc00 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -9081,7 +9081,7 @@ 'preg_replace' => ['string|array|null', 'regex'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable(array):string', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback_array' => ['string|array|null', 'pattern'=>'array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], -'preg_split' => ['list|false', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'?int', 'flags='=>'int'], +'preg_split' => ['__benevolent|list}>|false>', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'?int', 'flags='=>'int'], 'prev' => ['mixed', '&rw_array_arg'=>'array|object'], 'print_r' => ['string|true', 'var'=>'mixed', 'return='=>'bool'], 'printf' => ['int', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], From 4c3681d614c59a11d756999b09af779552c3035e Mon Sep 17 00:00:00 2001 From: malsuke Date: Wed, 25 Dec 2024 16:36:30 +0900 Subject: [PATCH 04/36] feat new feat for flag --- .../PregSplitDynamicReturnTypeExtension.php | 61 ++++++++++++----- tests/PHPStan/Analyser/nsrt/preg_split.php | 65 ++++++++++--------- 2 files changed, 82 insertions(+), 44 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index f2abef425a..5d1cbe35c0 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -1,4 +1,4 @@ -getConstantStrings(); if ( - count($patternConstantTypes) > 0 && - @preg_match($patternConstantTypes[0]->getValue(), "") === false + count($patternConstantTypes) > 0 + && @preg_match($patternConstantTypes[0]->getValue(), "") === false ) { - return new ErrorType(); } - if ($subjectArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($subjectArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE')->yes()) { - $type = new ArrayType( - new IntegerType(), - new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), IntegerRangeType::fromInterval(0, null)], [2], [], TrinaryLogic::createYes()), - ); - return TypeCombinator::union(TypeCombinator::intersect($type, new AccessoryArrayListType()), new ConstantBooleanType(false)); - } - if ($limitArg === null) { $limits = [-1]; } else { @@ -82,15 +81,47 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $flags = $flagType->getConstantScalarValues(); } - if (count($patternConstantTypes) === 0 || count($subjectConstantTypes) === 0 || count($flags) === 0) { + if (count($patternConstantTypes) === 0 || count($subjectConstantTypes) === 0) { + $returnNonEmptyStrings = $flagArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_NO_EMPTY')->yes(); + if ($returnNonEmptyStrings) { + $stringType = TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType() + ); + } else { + $stringType = new StringType(); + } + + if ($flagArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE')->yes()) { + $type = new ArrayType( + new IntegerType(), + new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [$stringType, IntegerRangeType::fromInterval(0, null)], [2], [], TrinaryLogic::createYes()), + ); + return TypeUtils::toBenevolentUnion( + TypeCombinator::union( + TypeCombinator::intersect($type, new AccessoryArrayListType()), + new ConstantBooleanType(false) + ) + ); + } + + if ($flagArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_NO_EMPTY')->yes()) { + return TypeUtils::toBenevolentUnion( + TypeCombinator::union( + TypeCombinator::intersect(new ArrayType(new MixedType(), $stringType), new AccessoryArrayListType()), + new ConstantBooleanType(false) + ) + ); + } + return null; } $resultTypes = []; foreach ($patternConstantTypes as $patternConstantType) { foreach ($subjectConstantTypes as $subjectConstantType) { - foreach ($flags as $flag) { - foreach ($limits as $limit) { + foreach ($limits as $limit) { + foreach ($flags as $flag) { $result = @preg_split($patternConstantType->getValue(), $subjectConstantType->getValue(), $limit, $flag); if ($result !== false) { $constantArray = ConstantArrayTypeBuilder::createEmpty(); diff --git a/tests/PHPStan/Analyser/nsrt/preg_split.php b/tests/PHPStan/Analyser/nsrt/preg_split.php index c153b55ec1..e00ca6a693 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_split.php +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -6,29 +6,6 @@ class HelloWorld { - public function doFoo() - { - $aaa = '/[0-9a]'; - assertType('*ERROR*', preg_split($aaa, '1-2-3')); - assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3')); - assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); - assertType("array{'1', '3'}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY)); - assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); - assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); - assertType("array{array{'1', 0}, array{'', 2}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_OFFSET_CAPTURE)); - assertType("array{array{'1', 0}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); - } - - public function doWithVariables(string $pattern, string $subject, int $offset, int $flags): void - { - assertType('(list|false)', preg_split($pattern, $subject, $offset, $flags)); - assertType('(list|false)', preg_split("//", $subject, $offset, $flags)); - assertType('(list|false)', preg_split($pattern, "1-2-3", $offset, $flags)); - assertType('(list|false)', preg_split($pattern, $subject, -1, $flags)); - assertType('(list|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_NO_EMPTY)); - assertType('list}>', preg_split($pattern, $subject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); - } - /** * @param string $pattern * @param string $subject @@ -39,10 +16,9 @@ public function doWithVariables(string $pattern, string $subject, int $offset, i */ public static function splitWithOffset($pattern, $subject, $limit = -1, $flags = 0) { - assertType('list}>', preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE)); - assertType('list}>', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags)); - - assertType('list}>', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags | PREG_SPLIT_NO_EMPTY)); + assertType('(list}>|false)', preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('(list}>|false)', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags)); + assertType('(list}>|false)', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags | PREG_SPLIT_NO_EMPTY)); } /** @@ -50,13 +26,44 @@ public static function splitWithOffset($pattern, $subject, $limit = -1, $flags = * @param string $subject * @param int $limit */ - public static function dynamicFlags($pattern, $subject, $limit = -1) { + public static function dynamicFlags($pattern, $subject, $limit = -1) + { $flags = PREG_SPLIT_OFFSET_CAPTURE; if ($subject === '1-2-3') { $flags |= PREG_SPLIT_NO_EMPTY; } - assertType('list}>|false', preg_split($pattern, $subject, $limit, $flags)); + assertType('(list}>|false)', preg_split($pattern, $subject, $limit, $flags)); + } + + public function doFoo() + { + assertType('*ERROR*', preg_split('/[0-9a]', '1-2-3')); + assertType("array{''}", preg_split('/-/', '')); + assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3')); + assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{'1', '3'}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'', 2}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + } + + /** + * @param non-empty-string $nonEmptySubject + */ + public function doWithVariables(string $pattern, string $subject, string $nonEmptySubject, int $offset, int $flags): void + { + assertType('(list}|string>|false)', preg_split($pattern, $subject, $offset, $flags)); + assertType('(list}|string>|false)', preg_split("//", $subject, $offset, $flags)); + + assertType('(list}|string>|false)', preg_split($pattern, $nonEmptySubject, $offset, $flags)); + assertType('(list}|string>|false)', preg_split("//", $nonEmptySubject, $offset, $flags)); + + assertType('(list}|string>|false)', preg_split($pattern, "1-2-3", $offset, $flags)); + assertType('(list}|string>|false)', preg_split($pattern, $subject, -1, $flags)); + assertType('(list|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_NO_EMPTY)); + assertType('(list}>|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); } } From c4e76c901063978bdae497292941117641b20ca0 Mon Sep 17 00:00:00 2001 From: malsuke Date: Wed, 25 Dec 2024 17:46:22 +0900 Subject: [PATCH 05/36] feat improve for flag & non-empty-string --- .../PregSplitDynamicReturnTypeExtension.php | 57 +++++++++++----- tests/PHPStan/Analyser/nsrt/preg_split.php | 65 ++++++++++--------- 2 files changed, 74 insertions(+), 48 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 5d1cbe35c0..17ab405473 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -8,6 +8,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\Constant\ConstantArrayType; @@ -92,29 +93,49 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $stringType = new StringType(); } - if ($flagArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE')->yes()) { - $type = new ArrayType( - new IntegerType(), - new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [$stringType, IntegerRangeType::fromInterval(0, null)], [2], [], TrinaryLogic::createYes()), - ); - return TypeUtils::toBenevolentUnion( - TypeCombinator::union( - TypeCombinator::intersect($type, new AccessoryArrayListType()), - new ConstantBooleanType(false) - ) - ); + $capturedArrayType = new ConstantArrayType( + [ + new ConstantIntegerType(0), + new ConstantIntegerType(1)], [$stringType, IntegerRangeType::fromInterval(0, null) + ], + [2], + [], + TrinaryLogic::createYes() + ); + $valueType = $stringType; + if ($flagArg !== null) { + $flagState = $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE'); + if ($flagState->yes()) { + return TypeUtils::toBenevolentUnion( + TypeCombinator::union( + TypeCombinator::intersect( + new ArrayType(new IntegerType(), $capturedArrayType), + new AccessoryArrayListType() + ), + new ConstantBooleanType(false) + ) + ); + } + if ($flagState->maybe()) { + $valueType = TypeCombinator::union(new StringType(), $capturedArrayType); + } } - if ($flagArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_NO_EMPTY')->yes()) { - return TypeUtils::toBenevolentUnion( - TypeCombinator::union( - TypeCombinator::intersect(new ArrayType(new MixedType(), $stringType), new AccessoryArrayListType()), - new ConstantBooleanType(false) - ) + $arrayType = TypeCombinator::intersect(new ArrayType(new MixedType(), $valueType), new AccessoryArrayListType()); + if ($subjectType->isNonEmptyString()->yes()) { + $arrayType = TypeCombinator::intersect( + $arrayType, + new NonEmptyArrayType(), + new AccessoryArrayListType(), ); } - return null; + return TypeUtils::toBenevolentUnion( + TypeCombinator::union( + $arrayType, + new ConstantBooleanType(false) + ) + ); } $resultTypes = []; diff --git a/tests/PHPStan/Analyser/nsrt/preg_split.php b/tests/PHPStan/Analyser/nsrt/preg_split.php index e00ca6a693..ccbcf08e3d 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_split.php +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -6,6 +6,41 @@ class HelloWorld { + public function doFoo() + { + assertType('*ERROR*', preg_split('/[0-9a]', '1-2-3')); + assertType("array{''}", preg_split('/-/', '')); + assertType("array{}", preg_split('/-/', '', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{array{'', 0}}", preg_split('/-/', '', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{}", preg_split('/-/', '', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3')); + assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{'1', '3'}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'', 2}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + } + + /** + * @param non-empty-string $nonEmptySubject + */ + public function doWithVariables(string $pattern, string $subject, string $nonEmptySubject, int $offset, int $flags): void + { + assertType('(list}|string>|false)', preg_split($pattern, $subject, $offset, $flags)); + assertType('(list}|string>|false)', preg_split("//", $subject, $offset, $flags)); + + assertType('(non-empty-list}|string>|false)', preg_split($pattern, $nonEmptySubject, $offset, $flags)); + assertType('(non-empty-list}|string>|false)', preg_split("//", $nonEmptySubject, $offset, $flags)); + + assertType('(non-empty-list|false)', preg_split("//", $nonEmptySubject)); + + assertType('(non-empty-list}|string>|false)', preg_split($pattern, "1-2-3", $offset, $flags)); + assertType('(list}|string>|false)', preg_split($pattern, $subject, -1, $flags)); + assertType('(list|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_NO_EMPTY)); + assertType('(list}>|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); + } + /** * @param string $pattern * @param string $subject @@ -36,34 +71,4 @@ public static function dynamicFlags($pattern, $subject, $limit = -1) assertType('(list}>|false)', preg_split($pattern, $subject, $limit, $flags)); } - - public function doFoo() - { - assertType('*ERROR*', preg_split('/[0-9a]', '1-2-3')); - assertType("array{''}", preg_split('/-/', '')); - assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3')); - assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); - assertType("array{'1', '3'}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY)); - assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); - assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); - assertType("array{array{'1', 0}, array{'', 2}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_OFFSET_CAPTURE)); - assertType("array{array{'1', 0}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); - } - - /** - * @param non-empty-string $nonEmptySubject - */ - public function doWithVariables(string $pattern, string $subject, string $nonEmptySubject, int $offset, int $flags): void - { - assertType('(list}|string>|false)', preg_split($pattern, $subject, $offset, $flags)); - assertType('(list}|string>|false)', preg_split("//", $subject, $offset, $flags)); - - assertType('(list}|string>|false)', preg_split($pattern, $nonEmptySubject, $offset, $flags)); - assertType('(list}|string>|false)', preg_split("//", $nonEmptySubject, $offset, $flags)); - - assertType('(list}|string>|false)', preg_split($pattern, "1-2-3", $offset, $flags)); - assertType('(list}|string>|false)', preg_split($pattern, $subject, -1, $flags)); - assertType('(list|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_NO_EMPTY)); - assertType('(list}>|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); - } } From 68379ac505b3c98e89fd3400fffe24fd8e30ff4d Mon Sep 17 00:00:00 2001 From: malsuke Date: Wed, 25 Dec 2024 18:10:00 +0900 Subject: [PATCH 06/36] add test for PREG_SPLIT_DELIM_CAPTURE flag --- tests/PHPStan/Analyser/nsrt/preg_split.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/preg_split.php b/tests/PHPStan/Analyser/nsrt/preg_split.php index ccbcf08e3d..0a3ebada7e 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_split.php +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -11,6 +11,7 @@ public function doFoo() assertType('*ERROR*', preg_split('/[0-9a]', '1-2-3')); assertType("array{''}", preg_split('/-/', '')); assertType("array{}", preg_split('/-/', '', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{'1', '-', '2', '-', '3'}", preg_split('/ *(-) */', '1- 2-3', -1, PREG_SPLIT_DELIM_CAPTURE)); assertType("array{array{'', 0}}", preg_split('/-/', '', -1, PREG_SPLIT_OFFSET_CAPTURE)); assertType("array{}", preg_split('/-/', '', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3')); @@ -39,6 +40,8 @@ public function doWithVariables(string $pattern, string $subject, string $nonEmp assertType('(list}|string>|false)', preg_split($pattern, $subject, -1, $flags)); assertType('(list|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_NO_EMPTY)); assertType('(list}>|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("(list|false)", preg_split($pattern, $subject, $offset, PREG_SPLIT_DELIM_CAPTURE)); + assertType('(list}>|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE)); } /** From 2031d066cacb65f217c6b6808b32e3675bf91bae Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 26 Dec 2024 15:44:46 +0900 Subject: [PATCH 07/36] add test case for nonEmptySubject --- tests/PHPStan/Analyser/nsrt/preg_split.php | 28 +++++++++++++++------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/preg_split.php b/tests/PHPStan/Analyser/nsrt/preg_split.php index 0a3ebada7e..24c933156c 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_split.php +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -23,19 +23,11 @@ public function doFoo() assertType("array{array{'1', 0}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); } - /** - * @param non-empty-string $nonEmptySubject - */ - public function doWithVariables(string $pattern, string $subject, string $nonEmptySubject, int $offset, int $flags): void + public function doWithVariables(string $pattern, string $subject, int $offset, int $flags): void { assertType('(list}|string>|false)', preg_split($pattern, $subject, $offset, $flags)); assertType('(list}|string>|false)', preg_split("//", $subject, $offset, $flags)); - assertType('(non-empty-list}|string>|false)', preg_split($pattern, $nonEmptySubject, $offset, $flags)); - assertType('(non-empty-list}|string>|false)', preg_split("//", $nonEmptySubject, $offset, $flags)); - - assertType('(non-empty-list|false)', preg_split("//", $nonEmptySubject)); - assertType('(non-empty-list}|string>|false)', preg_split($pattern, "1-2-3", $offset, $flags)); assertType('(list}|string>|false)', preg_split($pattern, $subject, -1, $flags)); assertType('(list|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_NO_EMPTY)); @@ -44,6 +36,24 @@ public function doWithVariables(string $pattern, string $subject, string $nonEmp assertType('(list}>|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE)); } + /** + * @param non-empty-string $nonEmptySubject + */ + public function doWithNonEmptySubject(string $pattern, string $nonEmptySubject, int $offset, int $flags): void + { + assertType('(non-empty-list|false)', preg_split("//", $nonEmptySubject)); + + assertType('(non-empty-list}|string>|false)', preg_split($pattern, $nonEmptySubject, $offset, $flags)); + assertType('(non-empty-list}|string>|false)', preg_split("//", $nonEmptySubject, $offset, $flags)); + + assertType('(non-empty-list}>|false)', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); + assertType('(non-empty-list|false)', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_NO_EMPTY)); + assertType('(non-empty-list|false)', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_DELIM_CAPTURE)); + assertType('(non-empty-list}>|false)', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('(non-empty-list}>|false)', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('(non-empty-list|false)', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)); + } + /** * @param string $pattern * @param string $subject From 82e4ce69cc639154108ffc5e80a9f3a64eb883dc Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 26 Dec 2024 15:45:37 +0900 Subject: [PATCH 08/36] feat add if state for nonEmptySubject --- .../PregSplitDynamicReturnTypeExtension.php | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 17ab405473..6ec225ec5f 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -94,27 +94,38 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $capturedArrayType = new ConstantArrayType( - [ - new ConstantIntegerType(0), - new ConstantIntegerType(1)], [$stringType, IntegerRangeType::fromInterval(0, null) - ], + [new ConstantIntegerType(0), new ConstantIntegerType(1)], [$stringType, IntegerRangeType::fromInterval(0, null)], [2], [], TrinaryLogic::createYes() ); + $valueType = $stringType; if ($flagArg !== null) { $flagState = $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE'); if ($flagState->yes()) { - return TypeUtils::toBenevolentUnion( - TypeCombinator::union( - TypeCombinator::intersect( - new ArrayType(new IntegerType(), $capturedArrayType), - new AccessoryArrayListType() - ), - new ConstantBooleanType(false) - ) - ); + if ($subjectType->isNonEmptyString()->yes()) { + return TypeUtils::toBenevolentUnion( + TypeCombinator::union( + TypeCombinator::intersect( + new ArrayType(new IntegerType(), $capturedArrayType), + new NonEmptyArrayType(), + new AccessoryArrayListType(), + ), + new ConstantBooleanType(false) + ) + ); + } else { + return TypeUtils::toBenevolentUnion( + TypeCombinator::union( + TypeCombinator::intersect( + new ArrayType(new IntegerType(), $capturedArrayType), + new AccessoryArrayListType(), + ), + new ConstantBooleanType(false) + ) + ); + } } if ($flagState->maybe()) { $valueType = TypeCombinator::union(new StringType(), $capturedArrayType); From fd496c2e954b386303274e0741cb431c83a099bd Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 26 Dec 2024 16:18:35 +0900 Subject: [PATCH 09/36] feat cleanup --- .../PregSplitDynamicReturnTypeExtension.php | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 6ec225ec5f..4095b62772 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -104,28 +104,18 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($flagArg !== null) { $flagState = $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE'); if ($flagState->yes()) { + $arrayType = TypeCombinator::intersect( + new ArrayType(new IntegerType(), $capturedArrayType), + new AccessoryArrayListType(), + ); + if ($subjectType->isNonEmptyString()->yes()) { - return TypeUtils::toBenevolentUnion( - TypeCombinator::union( - TypeCombinator::intersect( - new ArrayType(new IntegerType(), $capturedArrayType), - new NonEmptyArrayType(), - new AccessoryArrayListType(), - ), - new ConstantBooleanType(false) - ) - ); - } else { - return TypeUtils::toBenevolentUnion( - TypeCombinator::union( - TypeCombinator::intersect( - new ArrayType(new IntegerType(), $capturedArrayType), - new AccessoryArrayListType(), - ), - new ConstantBooleanType(false) - ) - ); + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); } + + return TypeUtils::toBenevolentUnion( + TypeCombinator::union($arrayType, new ConstantBooleanType(false)) + ); } if ($flagState->maybe()) { $valueType = TypeCombinator::union(new StringType(), $capturedArrayType); From 96896caf3673d3329da13974af00c06417d9a466 Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 26 Dec 2024 16:23:17 +0900 Subject: [PATCH 10/36] feat cleanup --- src/Type/Php/PregSplitDynamicReturnTypeExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 4095b62772..6125e029c8 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -63,7 +63,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ( count($patternConstantTypes) > 0 - && @preg_match($patternConstantTypes[0]->getValue(), "") === false + && @preg_match($patternConstantTypes[0]->getValue(), '') === false ) { return new ErrorType(); } From 6ff7b8c89b4fce661830465632a8f58a803dd810 Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 26 Dec 2024 16:30:53 +0900 Subject: [PATCH 11/36] feat cleanup --- .../PregSplitDynamicReturnTypeExtension.php | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 6125e029c8..8a794fcfbc 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -85,55 +85,54 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if (count($patternConstantTypes) === 0 || count($subjectConstantTypes) === 0) { $returnNonEmptyStrings = $flagArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_NO_EMPTY')->yes(); if ($returnNonEmptyStrings) { - $stringType = TypeCombinator::intersect( + $returnStringType = TypeCombinator::intersect( new StringType(), new AccessoryNonEmptyStringType() ); } else { - $stringType = new StringType(); + $returnStringType = new StringType(); } $capturedArrayType = new ConstantArrayType( - [new ConstantIntegerType(0), new ConstantIntegerType(1)], [$stringType, IntegerRangeType::fromInterval(0, null)], + [new ConstantIntegerType(0), new ConstantIntegerType(1)], [$returnStringType, IntegerRangeType::fromInterval(0, null)], [2], [], TrinaryLogic::createYes() ); - $valueType = $stringType; + $returnInternalValueType = $returnStringType; if ($flagArg !== null) { $flagState = $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE'); if ($flagState->yes()) { - $arrayType = TypeCombinator::intersect( + $capturedArrayListType = TypeCombinator::intersect( new ArrayType(new IntegerType(), $capturedArrayType), new AccessoryArrayListType(), ); if ($subjectType->isNonEmptyString()->yes()) { - $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + $capturedArrayListType = TypeCombinator::intersect($capturedArrayListType, new NonEmptyArrayType()); } return TypeUtils::toBenevolentUnion( - TypeCombinator::union($arrayType, new ConstantBooleanType(false)) + TypeCombinator::union($capturedArrayListType, new ConstantBooleanType(false)) ); } if ($flagState->maybe()) { - $valueType = TypeCombinator::union(new StringType(), $capturedArrayType); + $returnInternalValueType = TypeCombinator::union(new StringType(), $capturedArrayType); } } - $arrayType = TypeCombinator::intersect(new ArrayType(new MixedType(), $valueType), new AccessoryArrayListType()); + $returnListType = TypeCombinator::intersect(new ArrayType(new MixedType(), $returnInternalValueType), new AccessoryArrayListType()); if ($subjectType->isNonEmptyString()->yes()) { - $arrayType = TypeCombinator::intersect( - $arrayType, + $returnListType = TypeCombinator::intersect( + $returnListType, new NonEmptyArrayType(), - new AccessoryArrayListType(), ); } return TypeUtils::toBenevolentUnion( TypeCombinator::union( - $arrayType, + $returnListType, new ConstantBooleanType(false) ) ); @@ -153,18 +152,20 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $valueConstantArray = ConstantArrayTypeBuilder::createEmpty(); $valueConstantArray->setOffsetValueType(new ConstantIntegerType(0), new ConstantStringType($value[0])); $valueConstantArray->setOffsetValueType(new ConstantIntegerType(1), new ConstantIntegerType($value[1])); - $valueType = $valueConstantArray->getArray(); + $returnInternalValueType = $valueConstantArray->getArray(); } else { - $valueType = new ConstantStringType($value); + $returnInternalValueType = new ConstantStringType($value); } - $constantArray->setOffsetValueType(new ConstantIntegerType($key), $valueType); + $constantArray->setOffsetValueType(new ConstantIntegerType($key), $returnInternalValueType); } + $resultTypes[] = $constantArray->getArray(); } } } } } + return TypeCombinator::union(...$resultTypes); } } From c9852d53d58c5bbb2387f4c6105c9ade0fdc3f05 Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 26 Dec 2024 16:51:51 +0900 Subject: [PATCH 12/36] feat add is_int assertion --- src/Type/Php/PregSplitDynamicReturnTypeExtension.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 8a794fcfbc..0bd7dab255 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -142,12 +142,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, foreach ($patternConstantTypes as $patternConstantType) { foreach ($subjectConstantTypes as $subjectConstantType) { foreach ($limits as $limit) { + if (!is_int($limit)) { + return null; + } foreach ($flags as $flag) { + if (!is_int($flag)) { + return null; + } $result = @preg_split($patternConstantType->getValue(), $subjectConstantType->getValue(), $limit, $flag); if ($result !== false) { $constantArray = ConstantArrayTypeBuilder::createEmpty(); foreach ($result as $key => $value) { - assert(is_int($key)); if (is_array($value)) { $valueConstantArray = ConstantArrayTypeBuilder::createEmpty(); $valueConstantArray->setOffsetValueType(new ConstantIntegerType(0), new ConstantStringType($value[0])); From 6d1d6e9dd7c6428f49f4925533f95e59436e8b70 Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 26 Dec 2024 16:52:23 +0900 Subject: [PATCH 13/36] feat fix test --- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index df2d5e5ada..6773734ee1 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -890,13 +890,7 @@ public function testBug7500(): void public function testBug7554(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7554.php'); - $this->assertCount(2, $errors); - - $this->assertSame(sprintf('Parameter #1 $%s of function count expects array|Countable, list|string>>|false given.', PHP_VERSION_ID < 80000 ? 'var' : 'value'), $errors[0]->getMessage()); - $this->assertSame(26, $errors[0]->getLine()); - - $this->assertSame('Cannot access offset int<1, max> on list}>|false.', $errors[1]->getMessage()); - $this->assertSame(27, $errors[1]->getLine()); + $this->assertCount(0, $errors); } public function testBug7637(): void From 65495067d2ff9a9f57e44e89700e8a6e9ebdf3bc Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 26 Dec 2024 16:55:43 +0900 Subject: [PATCH 14/36] feat fix test --- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 6773734ee1..735e56b72b 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -842,13 +842,11 @@ public function testOffsetAccess(): void public function testUnresolvableParameter(): void { $errors = $this->runAnalyse(__DIR__ . '/data/unresolvable-parameter.php'); - $this->assertCount(3, $errors); - $this->assertSame('Parameter #2 $array of function array_map expects array, list|false given.', $errors[0]->getMessage()); - $this->assertSame(18, $errors[0]->getLine()); - $this->assertSame('Method UnresolvableParameter\Collection::pipeInto() has parameter $class with no type specified.', $errors[1]->getMessage()); + $this->assertCount(2, $errors); + $this->assertSame('Method UnresolvableParameter\Collection::pipeInto() has parameter $class with no type specified.', $errors[0]->getMessage()); + $this->assertSame(30, $errors[0]->getLine()); + $this->assertSame('PHPDoc tag @param for parameter $class contains unresolvable type.', $errors[1]->getMessage()); $this->assertSame(30, $errors[1]->getLine()); - $this->assertSame('PHPDoc tag @param for parameter $class contains unresolvable type.', $errors[2]->getMessage()); - $this->assertSame(30, $errors[2]->getLine()); } public function testBug7248(): void From 3d51233d634850800ba453fcd305e54d116f318c Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 26 Dec 2024 17:03:32 +0900 Subject: [PATCH 15/36] fix cleanup --- .../PregSplitDynamicReturnTypeExtension.php | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 0bd7dab255..f8743506c2 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -1,4 +1,4 @@ -maybe()) { $returnInternalValueType = TypeCombinator::union(new StringType(), $capturedArrayType); @@ -130,12 +129,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ); } - return TypeUtils::toBenevolentUnion( - TypeCombinator::union( - $returnListType, - new ConstantBooleanType(false) - ) - ); + return TypeUtils::toBenevolentUnion(TypeCombinator::union($returnListType, new ConstantBooleanType(false))); } $resultTypes = []; @@ -150,22 +144,23 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return null; } $result = @preg_split($patternConstantType->getValue(), $subjectConstantType->getValue(), $limit, $flag); - if ($result !== false) { - $constantArray = ConstantArrayTypeBuilder::createEmpty(); - foreach ($result as $key => $value) { - if (is_array($value)) { - $valueConstantArray = ConstantArrayTypeBuilder::createEmpty(); - $valueConstantArray->setOffsetValueType(new ConstantIntegerType(0), new ConstantStringType($value[0])); - $valueConstantArray->setOffsetValueType(new ConstantIntegerType(1), new ConstantIntegerType($value[1])); - $returnInternalValueType = $valueConstantArray->getArray(); - } else { - $returnInternalValueType = new ConstantStringType($value); - } - $constantArray->setOffsetValueType(new ConstantIntegerType($key), $returnInternalValueType); + if ($result === false) { + continue; + } + $constantArray = ConstantArrayTypeBuilder::createEmpty(); + foreach ($result as $key => $value) { + if (is_array($value)) { + $valueConstantArray = ConstantArrayTypeBuilder::createEmpty(); + $valueConstantArray->setOffsetValueType(new ConstantIntegerType(0), new ConstantStringType($value[0])); + $valueConstantArray->setOffsetValueType(new ConstantIntegerType(1), new ConstantIntegerType($value[1])); + $returnInternalValueType = $valueConstantArray->getArray(); + } else { + $returnInternalValueType = new ConstantStringType($value); } - - $resultTypes[] = $constantArray->getArray(); + $constantArray->setOffsetValueType(new ConstantIntegerType($key), $returnInternalValueType); } + + $resultTypes[] = $constantArray->getArray(); } } } @@ -173,4 +168,5 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeCombinator::union(...$resultTypes); } + } From 540e1b2e3c84c239241a7c92b610b57a12894811 Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 26 Dec 2024 17:04:04 +0900 Subject: [PATCH 16/36] fix cleanup --- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 735e56b72b..2561a4d5bc 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -13,7 +13,6 @@ use PHPStan\Type\Constant\ConstantStringType; use function extension_loaded; use function restore_error_handler; -use function sprintf; use const PHP_VERSION_ID; class AnalyserIntegrationTest extends PHPStanTestCase From d48a51247ec68d30ec9159be12c7f4ea1318f343 Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 26 Dec 2024 17:08:26 +0900 Subject: [PATCH 17/36] fix cleanup --- src/Type/Php/PregSplitDynamicReturnTypeExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index f8743506c2..20aaa0142e 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -98,7 +98,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, [$returnStringType, IntegerRangeType::fromInterval(0, null)], [2], [], - TrinaryLogic::createYes() + TrinaryLogic::createYes(), ); $returnInternalValueType = $returnStringType; From d0209272fba30327449b14c693919d3fb452c709 Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 26 Dec 2024 18:55:38 +0900 Subject: [PATCH 18/36] fix cleanup loop --- .../PregSplitDynamicReturnTypeExtension.php | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 20aaa0142e..b1d8d90879 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -68,20 +68,33 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new ErrorType(); } + $limits = []; if ($limitArg === null) { $limits = [-1]; } else { $limitType = $scope->getType($limitArg->value); - $limits = $limitType->getConstantScalarValues(); + foreach ($limitType->getConstantScalarValues() as $limit) { + if (!is_int($limit)) { + return new ErrorType(); + } + $limits[] = $limit; + } } + $flags = []; if ($flagArg === null) { $flags = [0]; } else { $flagType = $scope->getType($flagArg->value); - $flags = $flagType->getConstantScalarValues(); + foreach ($flagType->getConstantScalarValues() as $flag) { + if (!is_int($flag)) { + return new ErrorType(); + } + $flags[] = $flag; + } } + if (count($patternConstantTypes) === 0 || count($subjectConstantTypes) === 0) { $returnNonEmptyStrings = $flagArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_NO_EMPTY')->yes(); if ($returnNonEmptyStrings) { @@ -136,13 +149,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, foreach ($patternConstantTypes as $patternConstantType) { foreach ($subjectConstantTypes as $subjectConstantType) { foreach ($limits as $limit) { - if (!is_int($limit)) { - return null; - } foreach ($flags as $flag) { - if (!is_int($flag)) { - return null; - } $result = @preg_split($patternConstantType->getValue(), $subjectConstantType->getValue(), $limit, $flag); if ($result === false) { continue; From 13937b8dda02d5f01e0553c09918b423b5a80ce8 Mon Sep 17 00:00:00 2001 From: malsuke Date: Tue, 14 Jan 2025 09:38:59 +0900 Subject: [PATCH 19/36] fix __benevolent usage --- resources/functionMap.php | 2 +- .../PregSplitDynamicReturnTypeExtension.php | 5 +-- tests/PHPStan/Analyser/nsrt/preg_split.php | 42 +++++++++---------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 067251cc00..75e397b72b 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -9081,7 +9081,7 @@ 'preg_replace' => ['string|array|null', 'regex'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable(array):string', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback_array' => ['string|array|null', 'pattern'=>'array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], -'preg_split' => ['__benevolent|list}>|false>', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'?int', 'flags='=>'int'], +'preg_split' => ['list|list}>|false', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'?int', 'flags='=>'int'], 'prev' => ['mixed', '&rw_array_arg'=>'array|object'], 'print_r' => ['string|true', 'var'=>'mixed', 'return='=>'bool'], 'printf' => ['int', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index b1d8d90879..ae85455f85 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -94,7 +94,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } - if (count($patternConstantTypes) === 0 || count($subjectConstantTypes) === 0) { $returnNonEmptyStrings = $flagArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_NO_EMPTY')->yes(); if ($returnNonEmptyStrings) { @@ -127,7 +126,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $capturedArrayListType = TypeCombinator::intersect($capturedArrayListType, new NonEmptyArrayType()); } - return TypeUtils::toBenevolentUnion(TypeCombinator::union($capturedArrayListType, new ConstantBooleanType(false))); + return TypeCombinator::union($capturedArrayListType, new ConstantBooleanType(false)); } if ($flagState->maybe()) { $returnInternalValueType = TypeCombinator::union(new StringType(), $capturedArrayType); @@ -142,7 +141,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ); } - return TypeUtils::toBenevolentUnion(TypeCombinator::union($returnListType, new ConstantBooleanType(false))); + return TypeCombinator::union($returnListType, new ConstantBooleanType(false)); } $resultTypes = []; diff --git a/tests/PHPStan/Analyser/nsrt/preg_split.php b/tests/PHPStan/Analyser/nsrt/preg_split.php index 24c933156c..3071ba1bd0 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_split.php +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -25,15 +25,15 @@ public function doFoo() public function doWithVariables(string $pattern, string $subject, int $offset, int $flags): void { - assertType('(list}|string>|false)', preg_split($pattern, $subject, $offset, $flags)); - assertType('(list}|string>|false)', preg_split("//", $subject, $offset, $flags)); + assertType('list}|string>|false', preg_split($pattern, $subject, $offset, $flags)); + assertType('list}|string>|false', preg_split("//", $subject, $offset, $flags)); - assertType('(non-empty-list}|string>|false)', preg_split($pattern, "1-2-3", $offset, $flags)); - assertType('(list}|string>|false)', preg_split($pattern, $subject, -1, $flags)); - assertType('(list|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_NO_EMPTY)); - assertType('(list}>|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); - assertType("(list|false)", preg_split($pattern, $subject, $offset, PREG_SPLIT_DELIM_CAPTURE)); - assertType('(list}>|false)', preg_split($pattern, $subject, $offset, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('non-empty-list}|string>|false', preg_split($pattern, "1-2-3", $offset, $flags)); + assertType('list}|string>|false', preg_split($pattern, $subject, -1, $flags)); + assertType('list|false', preg_split($pattern, $subject, $offset, PREG_SPLIT_NO_EMPTY)); + assertType('list}>|false', preg_split($pattern, $subject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("list|false", preg_split($pattern, $subject, $offset, PREG_SPLIT_DELIM_CAPTURE)); + assertType('list}>|false', preg_split($pattern, $subject, $offset, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE)); } /** @@ -41,17 +41,17 @@ public function doWithVariables(string $pattern, string $subject, int $offset, i */ public function doWithNonEmptySubject(string $pattern, string $nonEmptySubject, int $offset, int $flags): void { - assertType('(non-empty-list|false)', preg_split("//", $nonEmptySubject)); + assertType('non-empty-list|false', preg_split("//", $nonEmptySubject)); - assertType('(non-empty-list}|string>|false)', preg_split($pattern, $nonEmptySubject, $offset, $flags)); - assertType('(non-empty-list}|string>|false)', preg_split("//", $nonEmptySubject, $offset, $flags)); + assertType('non-empty-list}|string>|false', preg_split($pattern, $nonEmptySubject, $offset, $flags)); + assertType('non-empty-list}|string>|false', preg_split("//", $nonEmptySubject, $offset, $flags)); - assertType('(non-empty-list}>|false)', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); - assertType('(non-empty-list|false)', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_NO_EMPTY)); - assertType('(non-empty-list|false)', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_DELIM_CAPTURE)); - assertType('(non-empty-list}>|false)', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE)); - assertType('(non-empty-list}>|false)', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); - assertType('(non-empty-list|false)', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)); + assertType('non-empty-list}>|false', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_OFFSET_CAPTURE)); + assertType('non-empty-list|false', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_NO_EMPTY)); + assertType('non-empty-list|false', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_DELIM_CAPTURE)); + assertType('non-empty-list}>|false', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('non-empty-list}>|false', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('non-empty-list|false', preg_split("/-/", $nonEmptySubject, $offset, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)); } /** @@ -64,9 +64,9 @@ public function doWithNonEmptySubject(string $pattern, string $nonEmptySubject, */ public static function splitWithOffset($pattern, $subject, $limit = -1, $flags = 0) { - assertType('(list}>|false)', preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE)); - assertType('(list}>|false)', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags)); - assertType('(list}>|false)', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags | PREG_SPLIT_NO_EMPTY)); + assertType('list}>|false', preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('list}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags)); + assertType('list}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags | PREG_SPLIT_NO_EMPTY)); } /** @@ -82,6 +82,6 @@ public static function dynamicFlags($pattern, $subject, $limit = -1) $flags |= PREG_SPLIT_NO_EMPTY; } - assertType('(list}>|false)', preg_split($pattern, $subject, $limit, $flags)); + assertType('list}>|false', preg_split($pattern, $subject, $limit, $flags)); } } From 99b2d4decc8e366b6a1f3f245e7d2a17376d3559 Mon Sep 17 00:00:00 2001 From: malsuke Date: Tue, 14 Jan 2025 09:50:04 +0900 Subject: [PATCH 20/36] fix test --- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 2561a4d5bc..674c8c3932 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -841,11 +841,13 @@ public function testOffsetAccess(): void public function testUnresolvableParameter(): void { $errors = $this->runAnalyse(__DIR__ . '/data/unresolvable-parameter.php'); - $this->assertCount(2, $errors); - $this->assertSame('Method UnresolvableParameter\Collection::pipeInto() has parameter $class with no type specified.', $errors[0]->getMessage()); - $this->assertSame(30, $errors[0]->getLine()); - $this->assertSame('PHPDoc tag @param for parameter $class contains unresolvable type.', $errors[1]->getMessage()); + $this->assertCount(3, $errors); + $this->assertSame('Parameter #2 $array of function array_map expects array, list|false given.', $errors[0]->getMessage()); + $this->assertSame(18, $errors[0]->getLine()); + $this->assertSame('Method UnresolvableParameter\Collection::pipeInto() has parameter $class with no type specified.', $errors[1]->getMessage()); $this->assertSame(30, $errors[1]->getLine()); + $this->assertSame('PHPDoc tag @param for parameter $class contains unresolvable type.', $errors[2]->getMessage()); + $this->assertSame(30, $errors[2]->getLine()); } public function testBug7248(): void From bd4b7d2c78d31f16ddf07b9294a438c8074dc0e7 Mon Sep 17 00:00:00 2001 From: malsuke Date: Tue, 14 Jan 2025 16:37:12 +0900 Subject: [PATCH 21/36] fix test --- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 674c8c3932..136de5e6da 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -886,10 +886,21 @@ public function testBug7500(): void $this->assertNoErrors($errors); } + + /** + * + */ + public function testBug7554(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7554.php'); - $this->assertCount(0, $errors); + $this->assertCount(2, $errors); + + $this->assertSame(sprintf('Parameter #1 $%s of function count expects array|Countable, list|string>>|false given.', PHP_VERSION_ID < 80000 ? 'var' : 'value'), $errors[0]->getMessage()); + $this->assertSame(26, $errors[0]->getLine()); + + $this->assertSame('Cannot access offset int<1, max> on list}>|false.', $errors[1]->getMessage()); + $this->assertSame(27, $errors[1]->getLine()); } public function testBug7637(): void From eb3e9f41a16d84d7e3de4fbe8fe025a9aaacb6d9 Mon Sep 17 00:00:00 2001 From: malsuke Date: Tue, 14 Jan 2025 16:37:43 +0900 Subject: [PATCH 22/36] fix test --- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 136de5e6da..df2d5e5ada 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -13,6 +13,7 @@ use PHPStan\Type\Constant\ConstantStringType; use function extension_loaded; use function restore_error_handler; +use function sprintf; use const PHP_VERSION_ID; class AnalyserIntegrationTest extends PHPStanTestCase @@ -886,11 +887,6 @@ public function testBug7500(): void $this->assertNoErrors($errors); } - - /** - * - */ - public function testBug7554(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7554.php'); From daa3072619138c05a04127453072c8603786a431 Mon Sep 17 00:00:00 2001 From: malsuke Date: Tue, 14 Jan 2025 16:41:16 +0900 Subject: [PATCH 23/36] fix test --- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index df2d5e5ada..5ca00cbe79 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -895,7 +895,7 @@ public function testBug7554(): void $this->assertSame(sprintf('Parameter #1 $%s of function count expects array|Countable, list|string>>|false given.', PHP_VERSION_ID < 80000 ? 'var' : 'value'), $errors[0]->getMessage()); $this->assertSame(26, $errors[0]->getLine()); - $this->assertSame('Cannot access offset int<1, max> on list}>|false.', $errors[1]->getMessage()); + $this->assertSame('Cannot access offset int<1, max> on list}>|false.', $errors[1]->getMessage()); $this->assertSame(27, $errors[1]->getLine()); } From 4672297d48d80f192fcd64a626dba7fb8576dd4f Mon Sep 17 00:00:00 2001 From: malsuke Date: Tue, 14 Jan 2025 16:44:51 +0900 Subject: [PATCH 24/36] fix coding style --- src/Type/Php/PregSplitDynamicReturnTypeExtension.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index ae85455f85..01eaa4708c 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -24,7 +24,6 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; use function is_array; use function is_int; From b809edc1afb9d662ebab402d38a74060a2aaacfd Mon Sep 17 00:00:00 2001 From: malsuke Date: Fri, 7 Mar 2025 13:56:35 +0900 Subject: [PATCH 25/36] fix: use utils function, return point, allow numeric-string --- .../PregSplitDynamicReturnTypeExtension.php | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 01eaa4708c..6d8b666b1d 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -2,6 +2,8 @@ namespace PHPStan\Type\Php; +use Nette\Utils\RegexpException; +use Nette\Utils\Strings; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; @@ -27,7 +29,6 @@ use function count; use function is_array; use function is_int; -use function preg_match; use function preg_split; use function strtolower; @@ -57,23 +58,29 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $flagArg = $args[3] ?? null; $patternType = $scope->getType($patternArg->value); $patternConstantTypes = $patternType->getConstantStrings(); + if (count($patternConstantTypes) > 0) { + foreach ($patternConstantTypes as $patternConstantType) { + try { + Strings::match('', $patternConstantType->getValue()); + } catch (RegexpException $e) { + return new ErrorType(); + } + } + } + $subjectType = $scope->getType($subjectArg->value); $subjectConstantTypes = $subjectType->getConstantStrings(); - if ( - count($patternConstantTypes) > 0 - && @preg_match($patternConstantTypes[0]->getValue(), '') === false - ) { - return new ErrorType(); - } - $limits = []; if ($limitArg === null) { $limits = [-1]; } else { $limitType = $scope->getType($limitArg->value); + if (!$limitType->isInteger()->yes() && !$limitType->isString()->yes()) { + return new ErrorType(); + } foreach ($limitType->getConstantScalarValues() as $limit) { - if (!is_int($limit)) { + if (!is_int($limit) && !is_numeric($limit)) { return new ErrorType(); } $limits[] = $limit; @@ -85,15 +92,18 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $flags = [0]; } else { $flagType = $scope->getType($flagArg->value); + if (!$flagType->isInteger()->yes() && !$flagType->isString()->yes()) { + return new ErrorType(); + } foreach ($flagType->getConstantScalarValues() as $flag) { - if (!is_int($flag)) { + if (!is_int($flag) && !is_numeric($flag)) { return new ErrorType(); } $flags[] = $flag; } } - if (count($patternConstantTypes) === 0 || count($subjectConstantTypes) === 0) { + if ($this->isPatternOrSubjectEmpty($patternConstantTypes, $subjectConstantTypes)) { $returnNonEmptyStrings = $flagArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_NO_EMPTY')->yes(); if ($returnNonEmptyStrings) { $returnStringType = TypeCombinator::intersect( @@ -148,9 +158,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, foreach ($subjectConstantTypes as $subjectConstantType) { foreach ($limits as $limit) { foreach ($flags as $flag) { - $result = @preg_split($patternConstantType->getValue(), $subjectConstantType->getValue(), $limit, $flag); + $result = @preg_split($patternConstantType->getValue(), $subjectConstantType->getValue(), (int)$limit, (int)$flag); if ($result === false) { - continue; + return new ErrorType(); } $constantArray = ConstantArrayTypeBuilder::createEmpty(); foreach ($result as $key => $value) { @@ -174,4 +184,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeCombinator::union(...$resultTypes); } + /** + * @param ConstantStringType[] $patternConstantArray + * @param ConstantStringType[] $subjectConstantArray + * @return bool + */ + private function isPatternOrSubjectEmpty(array $patternConstantArray, array $subjectConstantArray): bool { + return count($patternConstantArray) === 0 || count($subjectConstantArray) === 0; + } } From c96736b2ee111d558dcec008602fc6a3d2c0a1fd Mon Sep 17 00:00:00 2001 From: malsuke Date: Fri, 7 Mar 2025 13:57:17 +0900 Subject: [PATCH 26/36] feat: add test for Error --- tests/PHPStan/Analyser/nsrt/preg_split.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/preg_split.php b/tests/PHPStan/Analyser/nsrt/preg_split.php index 3071ba1bd0..047c087803 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_split.php +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -6,9 +6,10 @@ class HelloWorld { + private const NUMERIC_STRING_1 = "1"; + private const NUMERIC_STRING_NEGATIVE_1 = "-1"; public function doFoo() { - assertType('*ERROR*', preg_split('/[0-9a]', '1-2-3')); assertType("array{''}", preg_split('/-/', '')); assertType("array{}", preg_split('/-/', '', -1, PREG_SPLIT_NO_EMPTY)); assertType("array{'1', '-', '2', '-', '3'}", preg_split('/ *(-) */', '1- 2-3', -1, PREG_SPLIT_DELIM_CAPTURE)); @@ -21,6 +22,19 @@ public function doFoo() assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); assertType("array{array{'1', 0}, array{'', 2}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_OFFSET_CAPTURE)); assertType("array{array{'1', 0}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + + assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3', self::NUMERIC_STRING_NEGATIVE_1)); + assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3', -1, self::NUMERIC_STRING_1)); + } + + public function doWithError() { + assertType('*ERROR*', preg_split('/[0-9a]', '1-2-3')); + assertType('*ERROR*', preg_split('/-/', '1-2-3', 'hogehoge')); + assertType('*ERROR*', preg_split('/-/', '1-2-3', -1, 'hogehoge')); + assertType('*ERROR*', preg_split('/-/', '1-2-3', [], self::NUMERIC_STRING_NEGATIVE_1)); + assertType('*ERROR*', preg_split('/-/', '1-2-3', null, self::NUMERIC_STRING_NEGATIVE_1)); + assertType('*ERROR*', preg_split('/-/', '1-2-3', -1, [])); + assertType('*ERROR*', preg_split('/-/', '1-2-3', -1, null)); } public function doWithVariables(string $pattern, string $subject, int $offset, int $flags): void From de4c4953c748a7e6c11ce7c23072da11b29f3b0c Mon Sep 17 00:00:00 2001 From: malsuke Date: Fri, 7 Mar 2025 14:02:07 +0900 Subject: [PATCH 27/36] feat: migrate validation to private method --- .../Php/PregSplitDynamicReturnTypeExtension.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 6d8b666b1d..13dca0acf7 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -60,9 +60,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $patternConstantTypes = $patternType->getConstantStrings(); if (count($patternConstantTypes) > 0) { foreach ($patternConstantTypes as $patternConstantType) { - try { - Strings::match('', $patternConstantType->getValue()); - } catch (RegexpException $e) { + if ($this->isValidPattern($patternConstantType->getValue()) === false) { return new ErrorType(); } } @@ -192,4 +190,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, private function isPatternOrSubjectEmpty(array $patternConstantArray, array $subjectConstantArray): bool { return count($patternConstantArray) === 0 || count($subjectConstantArray) === 0; } + + private function isValidPattern(string $pattern): bool + { + try { + Strings::match('', $pattern); + } catch (RegexpException $e) { + return false; + } + return true; + } } From a305dabaa64c0d0fc4f96adf67dc5294881a7066 Mon Sep 17 00:00:00 2001 From: malsuke Date: Fri, 7 Mar 2025 14:09:09 +0900 Subject: [PATCH 28/36] fix: coding style --- src/Type/Php/PregSplitDynamicReturnTypeExtension.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 13dca0acf7..77c98e64d4 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -29,12 +29,12 @@ use function count; use function is_array; use function is_int; +use function is_numeric; use function preg_split; use function strtolower; final class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct( private BitwiseFlagHelper $bitwiseFlagAnalyser, ) @@ -156,7 +156,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, foreach ($subjectConstantTypes as $subjectConstantType) { foreach ($limits as $limit) { foreach ($flags as $flag) { - $result = @preg_split($patternConstantType->getValue(), $subjectConstantType->getValue(), (int)$limit, (int)$flag); + $result = @preg_split($patternConstantType->getValue(), $subjectConstantType->getValue(), (int) $limit, (int) $flag); if ($result === false) { return new ErrorType(); } @@ -178,16 +178,16 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } } - return TypeCombinator::union(...$resultTypes); + } /** * @param ConstantStringType[] $patternConstantArray * @param ConstantStringType[] $subjectConstantArray - * @return bool */ - private function isPatternOrSubjectEmpty(array $patternConstantArray, array $subjectConstantArray): bool { + private function isPatternOrSubjectEmpty(array $patternConstantArray, array $subjectConstantArray): bool + { return count($patternConstantArray) === 0 || count($subjectConstantArray) === 0; } @@ -195,7 +195,7 @@ private function isValidPattern(string $pattern): bool { try { Strings::match('', $pattern); - } catch (RegexpException $e) { + } catch (RegexpException) { return false; } return true; From 3f7bdcc329c44e1422cec87279253f4becb1ca04 Mon Sep 17 00:00:00 2001 From: malsuke Date: Fri, 7 Mar 2025 14:15:31 +0900 Subject: [PATCH 29/36] fix: coding style --- src/Type/Php/PregSplitDynamicReturnTypeExtension.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 77c98e64d4..f786b0225a 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -35,6 +35,7 @@ final class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct( private BitwiseFlagHelper $bitwiseFlagAnalyser, ) @@ -179,7 +180,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } return TypeCombinator::union(...$resultTypes); - } /** @@ -200,4 +200,5 @@ private function isValidPattern(string $pattern): bool } return true; } + } From 658826ae3421c7ece93ed859631253d446293b78 Mon Sep 17 00:00:00 2001 From: malsuke Date: Tue, 11 Mar 2025 11:31:24 +0900 Subject: [PATCH 30/36] feat: change variable name, fix: check type of limit/flag --- .../PregSplitDynamicReturnTypeExtension.php | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index f786b0225a..ed8861bef3 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -37,7 +37,7 @@ final class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturn { public function __construct( - private BitwiseFlagHelper $bitwiseFlagAnalyser, + private readonly BitwiseFlagHelper $bitwiseFlagAnalyser, ) { } @@ -56,7 +56,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $patternArg = $args[0]; $subjectArg = $args[1]; $limitArg = $args[2] ?? null; - $flagArg = $args[3] ?? null; + $capturesOffset = $args[3] ?? null; $patternType = $scope->getType($patternArg->value); $patternConstantTypes = $patternType->getConstantStrings(); if (count($patternConstantTypes) > 0) { @@ -75,7 +75,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $limits = [-1]; } else { $limitType = $scope->getType($limitArg->value); - if (!$limitType->isInteger()->yes() && !$limitType->isString()->yes()) { + if (!$this->isIntOrStringValue($limitType)) { return new ErrorType(); } foreach ($limitType->getConstantScalarValues() as $limit) { @@ -87,11 +87,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $flags = []; - if ($flagArg === null) { + if ($capturesOffset === null) { $flags = [0]; } else { - $flagType = $scope->getType($flagArg->value); - if (!$flagType->isInteger()->yes() && !$flagType->isString()->yes()) { + $flagType = $scope->getType($capturesOffset->value); + if (!$this->isIntOrStringValue($flagType)) { return new ErrorType(); } foreach ($flagType->getConstantScalarValues() as $flag) { @@ -103,8 +103,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($this->isPatternOrSubjectEmpty($patternConstantTypes, $subjectConstantTypes)) { - $returnNonEmptyStrings = $flagArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_NO_EMPTY')->yes(); - if ($returnNonEmptyStrings) { + if ($capturesOffset !== null + && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($capturesOffset->value, $scope, 'PREG_SPLIT_NO_EMPTY')->yes()) { $returnStringType = TypeCombinator::intersect( new StringType(), new AccessoryNonEmptyStringType(), @@ -122,8 +122,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ); $returnInternalValueType = $returnStringType; - if ($flagArg !== null) { - $flagState = $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE'); + if ($capturesOffset !== null) { + $flagState = $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($capturesOffset->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE'); if ($flagState->yes()) { $capturedArrayListType = TypeCombinator::intersect( new ArrayType(new IntegerType(), $capturedArrayType), @@ -133,7 +133,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($subjectType->isNonEmptyString()->yes()) { $capturedArrayListType = TypeCombinator::intersect($capturedArrayListType, new NonEmptyArrayType()); } - return TypeCombinator::union($capturedArrayListType, new ConstantBooleanType(false)); } if ($flagState->maybe()) { @@ -179,6 +178,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } } + return TypeCombinator::union(...$resultTypes); } @@ -201,4 +201,8 @@ private function isValidPattern(string $pattern): bool return true; } + private function isIntOrStringValue(Type $type): bool + { + return $type->isInteger()->yes() || $type->isString()->yes() || $type->isConstantScalarValue()->yes(); + } } From ca784519d3bbe810d5e717f65ed5b753063e705f Mon Sep 17 00:00:00 2001 From: malsuke Date: Tue, 11 Mar 2025 11:31:59 +0900 Subject: [PATCH 31/36] add: add test for scaler value --- tests/PHPStan/Analyser/nsrt/preg_split.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/preg_split.php b/tests/PHPStan/Analyser/nsrt/preg_split.php index 047c087803..b15e1e2bb6 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_split.php +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -8,6 +8,7 @@ class HelloWorld { private const NUMERIC_STRING_1 = "1"; private const NUMERIC_STRING_NEGATIVE_1 = "-1"; + public function doFoo() { assertType("array{''}", preg_split('/-/', '')); @@ -39,6 +40,9 @@ public function doWithError() { public function doWithVariables(string $pattern, string $subject, int $offset, int $flags): void { + assertType("array{'1', '2', '3'}|array{'1-2-3'}", preg_split('/-/', '1-2-3', $this->generate())); + assertType("array{'1', '2', '3'}|array{'1-2-3'}", preg_split('/-/', '1-2-3', $this->generate(), $this->generate())); + assertType('list}|string>|false', preg_split($pattern, $subject, $offset, $flags)); assertType('list}|string>|false', preg_split("//", $subject, $offset, $flags)); @@ -50,6 +54,13 @@ public function doWithVariables(string $pattern, string $subject, int $offset, i assertType('list}>|false', preg_split($pattern, $subject, $offset, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE)); } + /** + * @return 1|'17' + */ + private function generate(): int|string { + return (rand() % 2 === 0) ? 1 : "17"; + } + /** * @param non-empty-string $nonEmptySubject */ From c896b1b1f4aaadc82a68b1daf60c986199782615 Mon Sep 17 00:00:00 2001 From: malsuke Date: Tue, 11 Mar 2025 11:53:04 +0900 Subject: [PATCH 32/36] fix: lint --- src/Type/Php/PregSplitDynamicReturnTypeExtension.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index ed8861bef3..7d59b59713 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -205,4 +205,5 @@ private function isIntOrStringValue(Type $type): bool { return $type->isInteger()->yes() || $type->isString()->yes() || $type->isConstantScalarValue()->yes(); } + } From 5949c79f658daf48a92fd26fe385f915bc3697d4 Mon Sep 17 00:00:00 2001 From: malsuke Date: Wed, 12 Mar 2025 21:45:41 +0900 Subject: [PATCH 33/36] feat: return union false --- .../PregSplitDynamicReturnTypeExtension.php | 5 +-- tests/PHPStan/Analyser/nsrt/preg_split.php | 34 +++++++++---------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 7d59b59713..fd408a3a05 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -157,9 +157,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, foreach ($limits as $limit) { foreach ($flags as $flag) { $result = @preg_split($patternConstantType->getValue(), $subjectConstantType->getValue(), (int) $limit, (int) $flag); - if ($result === false) { - return new ErrorType(); - } $constantArray = ConstantArrayTypeBuilder::createEmpty(); foreach ($result as $key => $value) { if (is_array($value)) { @@ -178,7 +175,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } } - + $resultTypes[] = new ConstantBooleanType(false); return TypeCombinator::union(...$resultTypes); } diff --git a/tests/PHPStan/Analyser/nsrt/preg_split.php b/tests/PHPStan/Analyser/nsrt/preg_split.php index b15e1e2bb6..210a467372 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_split.php +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -11,21 +11,21 @@ class HelloWorld public function doFoo() { - assertType("array{''}", preg_split('/-/', '')); - assertType("array{}", preg_split('/-/', '', -1, PREG_SPLIT_NO_EMPTY)); - assertType("array{'1', '-', '2', '-', '3'}", preg_split('/ *(-) */', '1- 2-3', -1, PREG_SPLIT_DELIM_CAPTURE)); - assertType("array{array{'', 0}}", preg_split('/-/', '', -1, PREG_SPLIT_OFFSET_CAPTURE)); - assertType("array{}", preg_split('/-/', '', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); - assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3')); - assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); - assertType("array{'1', '3'}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY)); - assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); - assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); - assertType("array{array{'1', 0}, array{'', 2}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_OFFSET_CAPTURE)); - assertType("array{array{'1', 0}, array{'3', 3}}", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); - - assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3', self::NUMERIC_STRING_NEGATIVE_1)); - assertType("array{'1', '2', '3'}", preg_split('/-/', '1-2-3', -1, self::NUMERIC_STRING_1)); + assertType("array{''}|false", preg_split('/-/', '')); + assertType("array{}|false", preg_split('/-/', '', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{'1', '-', '2', '-', '3'}|false", preg_split('/ *(-) */', '1- 2-3', -1, PREG_SPLIT_DELIM_CAPTURE)); + assertType("array{array{'', 0}}|false", preg_split('/-/', '', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{}|false", preg_split('/-/', '', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{'1', '2', '3'}|false", preg_split('/-/', '1-2-3')); + assertType("array{'1', '2', '3'}|false", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{'1', '3'}|false", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY)); + assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}|false", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'2', 2}, array{'3', 4}}|false", preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'', 2}, array{'3', 3}}|false", preg_split('/-/', '1--3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType("array{array{'1', 0}, array{'3', 3}}|false", preg_split('/-/', '1--3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + + assertType("array{'1', '2', '3'}|false", preg_split('/-/', '1-2-3', self::NUMERIC_STRING_NEGATIVE_1)); + assertType("array{'1', '2', '3'}|false", preg_split('/-/', '1-2-3', -1, self::NUMERIC_STRING_1)); } public function doWithError() { @@ -40,8 +40,8 @@ public function doWithError() { public function doWithVariables(string $pattern, string $subject, int $offset, int $flags): void { - assertType("array{'1', '2', '3'}|array{'1-2-3'}", preg_split('/-/', '1-2-3', $this->generate())); - assertType("array{'1', '2', '3'}|array{'1-2-3'}", preg_split('/-/', '1-2-3', $this->generate(), $this->generate())); + assertType("array{'1', '2', '3'}|array{'1-2-3'}|false", preg_split('/-/', '1-2-3', $this->generate())); + assertType("array{'1', '2', '3'}|array{'1-2-3'}|false", preg_split('/-/', '1-2-3', $this->generate(), $this->generate())); assertType('list}|string>|false', preg_split($pattern, $subject, $offset, $flags)); assertType('list}|string>|false', preg_split("//", $subject, $offset, $flags)); From a54bd80c1f515b54b2ef5a31a3a8f99fadedd739 Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 17 Apr 2025 13:43:31 +0900 Subject: [PATCH 34/36] feat: Handle case when preg_split returns false --- src/Type/Php/PregSplitDynamicReturnTypeExtension.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index fd408a3a05..4571d8613b 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -157,6 +157,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, foreach ($limits as $limit) { foreach ($flags as $flag) { $result = @preg_split($patternConstantType->getValue(), $subjectConstantType->getValue(), (int) $limit, (int) $flag); + if ($result === false) { + return new ErrorType(); + } $constantArray = ConstantArrayTypeBuilder::createEmpty(); foreach ($result as $key => $value) { if (is_array($value)) { From 1ee1a792c4d4d681d74e52724e9ccd907f02116b Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 15 May 2025 11:52:16 +0900 Subject: [PATCH 35/36] fix: for using ConstantArrayTypeBuilder and UnionType --- .../PregSplitDynamicReturnTypeExtension.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 4571d8613b..9647e82d64 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -26,6 +26,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; use function count; use function is_array; use function is_int; @@ -113,13 +114,16 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $returnStringType = new StringType(); } - $capturedArrayType = new ConstantArrayType( - [new ConstantIntegerType(0), new ConstantIntegerType(1)], - [$returnStringType, IntegerRangeType::fromInterval(0, null)], - [2], - [], - TrinaryLogic::createYes(), + $arrayTypeBuilder = ConstantArrayTypeBuilder::createEmpty(); + $arrayTypeBuilder->setOffsetValueType( + new ConstantIntegerType(0), + $returnStringType ); + $arrayTypeBuilder->setOffsetValueType( + new ConstantIntegerType(1), + IntegerRangeType::fromInterval(0, null) + ); + $capturedArrayType = $arrayTypeBuilder->getArray(); $returnInternalValueType = $returnStringType; if ($capturesOffset !== null) { @@ -203,7 +207,7 @@ private function isValidPattern(string $pattern): bool private function isIntOrStringValue(Type $type): bool { - return $type->isInteger()->yes() || $type->isString()->yes() || $type->isConstantScalarValue()->yes(); + return (new UnionType([new IntegerType(), new StringType()]))->isSuperTypeOf($type)->yes(); } } From 798fa2357003080a73517a20d30936144c8a1c2c Mon Sep 17 00:00:00 2001 From: malsuke Date: Thu, 15 May 2025 11:59:45 +0900 Subject: [PATCH 36/36] fix: cleanup --- src/Type/Php/PregSplitDynamicReturnTypeExtension.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 9647e82d64..b54b6676d0 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -7,13 +7,11 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BitwiseFlagHelper; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -117,11 +115,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayTypeBuilder = ConstantArrayTypeBuilder::createEmpty(); $arrayTypeBuilder->setOffsetValueType( new ConstantIntegerType(0), - $returnStringType + $returnStringType, ); $arrayTypeBuilder->setOffsetValueType( new ConstantIntegerType(1), - IntegerRangeType::fromInterval(0, null) + IntegerRangeType::fromInterval(0, null), ); $capturedArrayType = $arrayTypeBuilder->getArray();