diff --git a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php index 2a3d43c9c0..d5c7450d19 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -6,12 +6,14 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; 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\IntegerRangeType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -37,8 +39,16 @@ final class ParseUrlFunctionDynamicReturnTypeExtension implements DynamicFunctio /** @var array|null */ private ?array $componentTypesPairedStrings = null; + /** @var array|null */ + private ?array $componentTypesPairedConstantsForLowercaseString = null; + + /** @var array|null */ + private ?array $componentTypesPairedStringsForLowercaseString = null; + private ?Type $allComponentsTogetherType = null; + private ?Type $allComponentsTogetherTypeForLowercaseString = null; + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'parse_url'; @@ -52,23 +62,22 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $this->cacheReturnTypes(); + $urlType = $scope->getType($functionCall->getArgs()[0]->value); if (count($functionCall->getArgs()) > 1) { $componentType = $scope->getType($functionCall->getArgs()[1]->value); if (!$componentType->isConstantValue()->yes()) { - return $this->createAllComponentsReturnType(); + return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); } $componentType = $componentType->toInteger(); - if (!$componentType instanceof ConstantIntegerType) { - return $this->createAllComponentsReturnType(); + return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); } } else { $componentType = new ConstantIntegerType(-1); } - $urlType = $scope->getType($functionCall->getArgs()[0]->value); if (count($urlType->getConstantStrings()) > 0) { $types = []; foreach ($urlType->getConstantStrings() as $constantString) { @@ -86,21 +95,44 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($componentType->getValue() === -1) { - return TypeCombinator::union($this->createComponentsArray(), new ConstantBooleanType(false)); + return TypeCombinator::union( + $this->createComponentsArray($urlType->isLowercaseString()->yes()), + new ConstantBooleanType(false) + ); + } + + if ($urlType->isLowercaseString()->yes()) { + return $this->componentTypesPairedConstantsForLowercaseString[$componentType->getValue()] ?? new ConstantBooleanType(false); } return $this->componentTypesPairedConstants[$componentType->getValue()] ?? new ConstantBooleanType(false); } - private function createAllComponentsReturnType(): Type + private function createAllComponentsReturnType(bool $urlIsLowercase): Type { + if ($urlIsLowercase) { + if ($this->allComponentsTogetherTypeForLowercaseString === null) { + $returnTypes = [ + new ConstantBooleanType(false), + new NullType(), + IntegerRangeType::fromInterval(0, 65535), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + $this->createComponentsArray(true), + ]; + + $this->allComponentsTogetherTypeForLowercaseString = TypeCombinator::union(...$returnTypes); + } + + return $this->allComponentsTogetherTypeForLowercaseString; + } + if ($this->allComponentsTogetherType === null) { $returnTypes = [ new ConstantBooleanType(false), new NullType(), IntegerRangeType::fromInterval(0, 65535), new StringType(), - $this->createComponentsArray(), + $this->createComponentsArray(false), ]; $this->allComponentsTogetherType = TypeCombinator::union(...$returnTypes); @@ -109,19 +141,29 @@ private function createAllComponentsReturnType(): Type return $this->allComponentsTogetherType; } - private function createComponentsArray(): Type + private function createComponentsArray(bool $urlIsLowercase): Type { - $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder = ConstantArrayTypeBuilder::createEmpty(); - if ($this->componentTypesPairedStrings === null) { - throw new ShouldNotHappenException(); - } + if ($urlIsLowercase) { + if ($this->componentTypesPairedStringsForLowercaseString === null) { + throw new ShouldNotHappenException(); + } + + foreach ($this->componentTypesPairedStringsForLowercaseString as $componentName => $componentValueType) { + $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); + } + } else { + if ($this->componentTypesPairedStrings === null) { + throw new ShouldNotHappenException(); + } - foreach ($this->componentTypesPairedStrings as $componentName => $componentValueType) { - $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); + foreach ($this->componentTypesPairedStrings as $componentName => $componentValueType) { + $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); + } } - return $builder->getArray(); + return $builder->getArray(); } private function cacheReturnTypes(): void @@ -131,11 +173,13 @@ private function cacheReturnTypes(): void } $string = new StringType(); + $lowercaseString = new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); $port = IntegerRangeType::fromInterval(0, 65535); $false = new ConstantBooleanType(false); $null = new NullType(); $stringOrFalseOrNull = TypeCombinator::union($string, $false, $null); + $lowercaseStringOrFalseOrNull = TypeCombinator::union($lowercaseString, $false, $null); $portOrFalseOrNull = TypeCombinator::union($port, $false, $null); $this->componentTypesPairedConstants = [ @@ -148,6 +192,16 @@ private function cacheReturnTypes(): void PHP_URL_QUERY => $stringOrFalseOrNull, PHP_URL_FRAGMENT => $stringOrFalseOrNull, ]; + $this->componentTypesPairedConstantsForLowercaseString = [ + PHP_URL_SCHEME => $lowercaseStringOrFalseOrNull, + PHP_URL_HOST => $lowercaseStringOrFalseOrNull, + PHP_URL_PORT => $portOrFalseOrNull, + PHP_URL_USER => $lowercaseStringOrFalseOrNull, + PHP_URL_PASS => $lowercaseStringOrFalseOrNull, + PHP_URL_PATH => $lowercaseStringOrFalseOrNull, + PHP_URL_QUERY => $lowercaseStringOrFalseOrNull, + PHP_URL_FRAGMENT => $lowercaseStringOrFalseOrNull, + ]; $this->componentTypesPairedStrings = [ 'scheme' => $string, @@ -159,6 +213,16 @@ private function cacheReturnTypes(): void 'query' => $string, 'fragment' => $string, ]; + $this->componentTypesPairedStringsForLowercaseString = [ + 'scheme' => $lowercaseString, + 'host' => $lowercaseString, + 'port' => $port, + 'user' => $lowercaseString, + 'pass' => $lowercaseString, + 'path' => $lowercaseString, + 'query' => $lowercaseString, + 'fragment' => $lowercaseString, + ]; } } diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 9860a71f59..430b0e02f8 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -5492,7 +5492,7 @@ public function dataFunctions(): array '$parseUrlConstantUrlWithoutComponent2', ], [ - 'array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|int<0, 65535>|string|false|null', + 'array{scheme?: lowercase-string, host?: lowercase-string, port?: int<0, 65535>, user?: lowercase-string, pass?: lowercase-string, path?: lowercase-string, query?: lowercase-string, fragment?: lowercase-string}|int<0, 65535>|lowercase-string|false|null', '$parseUrlConstantUrlUnknownComponent', ], [ diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-parse-url.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-parse-url.php new file mode 100644 index 0000000000..1a6f7e683e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/lowercase-string-parse-url.php @@ -0,0 +1,26 @@ +, user?: lowercase-string, pass?: lowercase-string, path?: lowercase-string, query?: lowercase-string, fragment?: lowercase-string}|false', parse_url($lowercase)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_SCHEME)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_HOST)); + assertType('int<0, 65535>|false|null', parse_url($lowercase, PHP_URL_PORT)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_USER)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_PASS)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_PATH)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_QUERY)); + assertType('lowercase-string|false|null', parse_url($lowercase, PHP_URL_FRAGMENT)); + } + +}