diff --git a/conf/config.neon b/conf/config.neon index a9699fee47..a7bdfe9aea 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1119,11 +1119,6 @@ services: class: PHPStan\Type\Php\GettimeofdayDynamicFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - - class: PHPStan\Type\Php\HashHmacFunctionsReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\HashFunctionsReturnTypeExtension tags: diff --git a/resources/functionMap.php b/resources/functionMap.php index 9a29b02e0a..1ffbddbf12 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -3902,18 +3902,18 @@ 'HaruPage::stroke' => ['bool', 'close_path='=>'bool'], 'HaruPage::textOut' => ['bool', 'x'=>'float', 'y'=>'float', 'text'=>'string'], 'HaruPage::textRect' => ['bool', 'left'=>'float', 'top'=>'float', 'right'=>'float', 'bottom'=>'float', 'text'=>'string', 'align='=>'int'], -'hash' => ['string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], -'hash_algos' => ['array'], +'hash' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], +'hash_algos' => ['array'], 'hash_copy' => ['HashContext', 'context'=>'HashContext'], 'hash_equals' => ['bool', 'known_string'=>'string', 'user_string'=>'string'], -'hash_file' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'raw_output='=>'bool'], -'hash_final' => ['string', 'context'=>'HashContext', 'raw_output='=>'bool'], -'hash_hkdf' => ['string', 'algo'=>'string', 'ikm'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], -'hash_hmac' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], -'hash_hmac_algos' => ['array'], -'hash_hmac_file' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'raw_output='=>'bool'], +'hash_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'raw_output='=>'bool'], +'hash_final' => ['non-empty-string', 'context'=>'HashContext', 'raw_output='=>'bool'], +'hash_hkdf' => ['non-empty-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], +'hash_hmac' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], +'hash_hmac_algos' => ['array'], +'hash_hmac_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'raw_output='=>'bool'], 'hash_init' => ['HashContext', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], -'hash_pbkdf2' => ['string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], +'hash_pbkdf2' => ['non-empty-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], 'hash_update' => ['bool', 'context'=>'HashContext', 'data'=>'string'], 'hash_update_file' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'scontext='=>'?HashContext'], 'hash_update_stream' => ['int', 'context'=>'HashContext', 'handle'=>'resource', 'length='=>'int'], diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index 420b00c3b2..d260510eca 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -45,7 +45,10 @@ 'get_resource_id' => ['int', 'res'=>'resource'], 'gmdate' => ['string', 'format'=>'string', 'timestamp='=>'int'], 'gmmktime' => ['int|false', 'hour'=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], - 'hash_hkdf' => ['string', 'algo'=>'string', 'ikm'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], + 'hash_hkdf' => ['non-empty-string', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash_hmac' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], + 'hash_pbkdf2' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], 'imageaffine' => ['false|object', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], 'imagecreate' => ['false|object', 'x_size'=>'int', 'y_size'=>'int'], 'imagecreatefrombmp' => ['false|object', 'filename'=>'string'], @@ -169,7 +172,10 @@ 'gmmktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'gmp_random' => ['GMP', 'limiter='=>'int'], 'gzgetss' => ['string|false', 'zp'=>'resource', 'length'=>'int', 'allowable_tags='=>'string'], - 'hash_hkdf' => ['string|false', 'algo'=>'string', 'ikm'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], + 'hash_hkdf' => ['non-empty-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash_hmac' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], + 'hash_pbkdf2' => ['non-empty-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], 'image2wbmp' => ['bool', 'im'=>'resource', 'filename='=>'?string', 'threshold='=>'int'], 'imageaffine' => ['resource|false', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], 'imagecreate' => ['resource|false', 'x_size'=>'int', 'y_size'=>'int'], diff --git a/src/Type/Php/HashFunctionsReturnTypeExtension.php b/src/Type/Php/HashFunctionsReturnTypeExtension.php index 2dce0394e6..a1c6d2e06e 100644 --- a/src/Type/Php/HashFunctionsReturnTypeExtension.php +++ b/src/Type/Php/HashFunctionsReturnTypeExtension.php @@ -4,24 +4,86 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; -use function count; +use function array_map; use function hash_algos; use function in_array; +use function strtolower; final class HashFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + private const SUPPORTED_FUNCTIONS = [ + 'hash' => [ + 'cryptographic' => false, + 'possiblyFalse' => false, + ], + 'hash_file' => [ + 'cryptographic' => false, + 'possiblyFalse' => true, + ], + 'hash_hkdf' => [ + 'cryptographic' => true, + 'possiblyFalse' => false, + ], + 'hash_hmac' => [ + 'cryptographic' => true, + 'possiblyFalse' => false, + ], + 'hash_hmac_file' => [ + 'cryptographic' => true, + 'possiblyFalse' => true, + ], + 'hash_pbkdf2' => [ + 'cryptographic' => true, + 'possiblyFalse' => false, + ], + ]; + + private const NON_CRYPTOGRAPHIC_ALGORITHMS = [ + 'adler32', + 'crc32', + 'crc32b', + 'crc32c', + 'fnv132', + 'fnv1a32', + 'fnv164', + 'fnv1a64', + 'joaat', + 'murmur3a', + 'murmur3c', + 'murmur3f', + 'xxh32', + 'xxh64', + 'xxh3', + 'xxh128', + ]; + + /** @var array */ + private array $hashAlgorithms; + + public function __construct(private PhpVersion $phpVersion) + { + $this->hashAlgorithms = hash_algos(); + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'hash'; + $name = strtolower($functionReflection->getName()); + return isset(self::SUPPORTED_FUNCTIONS[$name]); } public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type @@ -32,18 +94,48 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $defaultReturnType; } - $argType = $scope->getType($functionCall->getArgs()[0]->value); - if ($argType instanceof MixedType) { + $algorithmType = $scope->getType($functionCall->getArgs()[0]->value); + if ($algorithmType instanceof MixedType) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - $values = TypeUtils::getConstantStrings($argType); - if (count($values) !== 1) { + $constantAlgorithmTypes = TypeUtils::getConstantStrings($algorithmType); + + if ($constantAlgorithmTypes === []) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - $string = $values[0]; - return in_array($string->getValue(), hash_algos(), true) ? new StringType() : new ConstantBooleanType(false); + $neverType = new NeverType(); + $falseType = new ConstantBooleanType(false); + $nonEmptyString = new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + + $invalidAlgorithmType = $this->phpVersion->throwsValueErrorForInternalFunctions() ? $neverType : $falseType; + $functionData = self::SUPPORTED_FUNCTIONS[strtolower($functionReflection->getName())]; + + $returnTypes = array_map( + function (ConstantStringType $type) use ($functionData, $nonEmptyString, $invalidAlgorithmType) { + $algorithm = strtolower($type->getValue()); + if (!in_array($algorithm, $this->hashAlgorithms, true)) { + return $invalidAlgorithmType; + } + if ($functionData['cryptographic'] && in_array($algorithm, self::NON_CRYPTOGRAPHIC_ALGORITHMS, true)) { + return $invalidAlgorithmType; + } + return $nonEmptyString; + }, + $constantAlgorithmTypes, + ); + + $returnType = TypeCombinator::union(...$returnTypes); + + if ($functionData['possiblyFalse'] && !$neverType->isSuperTypeOf($returnType)->yes()) { + $returnType = TypeCombinator::union($returnType, $falseType); + } + + return $returnType; } } diff --git a/src/Type/Php/HashHmacFunctionsReturnTypeExtension.php b/src/Type/Php/HashHmacFunctionsReturnTypeExtension.php deleted file mode 100644 index 5139a7ae80..0000000000 --- a/src/Type/Php/HashHmacFunctionsReturnTypeExtension.php +++ /dev/null @@ -1,99 +0,0 @@ -getName(), ['hash_hmac', 'hash_hmac_file'], true); - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type - { - if ($functionReflection->getName() === 'hash_hmac') { - $defaultReturnType = new StringType(); - } else { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } - - if (!isset($functionCall->getArgs()[0])) { - return $defaultReturnType; - } - - $argType = $scope->getType($functionCall->getArgs()[0]->value); - if ($argType instanceof MixedType) { - return TypeUtils::toBenevolentUnion($defaultReturnType); - } - - $values = TypeUtils::getConstantStrings($argType); - if (count($values) !== 1) { - return TypeUtils::toBenevolentUnion($defaultReturnType); - } - $string = $values[0]; - - return in_array($string->getValue(), self::HMAC_ALGORITHMS, true) ? $defaultReturnType : new ConstantBooleanType(false); - } - -} diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index f49e371de0..283c86b488 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -5618,62 +5618,6 @@ public function dataFunctions(): array 'array|int|false', '$strWordCountStrTypeIndeterminant', ], - [ - 'string', - '$hashHmacMd5', - ], - [ - 'string', - '$hashHmacSha256', - ], - [ - 'false', - '$hashHmacNonCryptographic', - ], - [ - 'false', - '$hashHmacRandom', - ], - [ - 'string', - '$hashHmacVariable', - ], - [ - 'string|false', - '$hashHmacFileMd5', - ], - [ - 'string|false', - '$hashHmacFileSha256', - ], - [ - 'false', - '$hashHmacFileNonCryptographic', - ], - [ - 'false', - '$hashHmacFileRandom', - ], - [ - '(string|false)', - '$hashHmacFileVariable', - ], - [ - 'string', - '$hash', - ], - [ - 'string', - '$hashRaw', - ], - [ - 'false', - '$hashRandom', - ], - [ - 'string', - '$hashMixed', - ], ]; } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 70aac8b22f..33ece56045 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -619,6 +619,13 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6404.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6399.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4357.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/hash-functions.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/hash-functions-80.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/hash-functions-74.php'); + } } /** diff --git a/tests/PHPStan/Analyser/data/functions.php b/tests/PHPStan/Analyser/data/functions.php index 173a4cd5f5..7c36d6e854 100644 --- a/tests/PHPStan/Analyser/data/functions.php +++ b/tests/PHPStan/Analyser/data/functions.php @@ -123,22 +123,4 @@ $integer = doFoo(); $strWordCountStrTypeIndeterminant = str_word_count('string', $integer); -$hashHmacMd5 = hash_hmac('md5', 'data', 'key'); -$hashHmacSha256 = hash_hmac('sha256', 'data', 'key'); -$hashHmacNonCryptographic = hash_hmac('crc32', 'data', 'key'); -$hashHmacRandom = hash_hmac('random', 'data', 'key'); -$hashHmacVariable = hash_hmac($string, 'data', 'key'); - -$hashHmacFileMd5 = hash_hmac_file('md5', 'data', 'key'); -$hashHmacFileSha256 = hash_hmac_file('sha256', 'data', 'key'); -$hashHmacFileNonCryptographic = hash_hmac_file('crc32', 'data', 'key'); -$hashHmacFileRandom = hash_hmac_file('random', 'data', 'key'); -$hashHmacFileVariable = hash_hmac_file($string, 'data', 'key'); - -$hash = hash('sha256', 'data', false); -$hashRaw = hash('sha256', 'data', true); -$hashRandom = hash('random', 'data', false); -/** @var mixed $mixed */ -$mixed = doFoo(); -$hashMixed = hash('md5', $mixed, false); die; diff --git a/tests/PHPStan/Analyser/data/hash-functions-74.php b/tests/PHPStan/Analyser/data/hash-functions-74.php new file mode 100644 index 0000000000..85915248d7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/hash-functions-74.php @@ -0,0 +1,53 @@ +