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(); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy