From fa48721ac6d22a8bd15f928b99b9028cad2082ab Mon Sep 17 00:00:00 2001 From: Jean-Luc Herren Date: Wed, 1 Dec 2021 22:28:58 +0100 Subject: [PATCH] Update return types for hash functions --- conf/config.neon | 5 - resources/functionMap.php | 20 ++-- .../Php/HashFunctionsReturnTypeExtension.php | 94 +++++++++++++++++- .../HashHmacFunctionsReturnTypeExtension.php | 97 ------------------- .../Analyser/LegacyNodeScopeResolverTest.php | 56 ----------- .../Analyser/NodeScopeResolverTest.php | 2 + tests/PHPStan/Analyser/data/functions.php | 18 ---- .../PHPStan/Analyser/data/hash-functions.php | 84 ++++++++++++++++ 8 files changed, 185 insertions(+), 191 deletions(-) delete mode 100644 src/Type/Php/HashHmacFunctionsReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/data/hash-functions.php diff --git a/conf/config.neon b/conf/config.neon index 29f63bdbdc6..caab7bdc58e 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1107,11 +1107,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 c082585eccb..b85d526a7b7 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -3902,19 +3902,19 @@ '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_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_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_file' => ['non-empty-string|false', 'algo'=>'non-empty-string', 'filename'=>'non-empty-string', 'raw_output='=>'bool'], +'hash_final' => ['non-empty-string', 'context'=>'HashContext', 'raw_output='=>'bool'], +'hash_hkdf' => ['non-empty-string|false', 'algo'=>'non-empty-string', 'key'=>'non-empty-string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], +'hash_hmac' => ['non-empty-string|false', 'algo'=>'non-empty-string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], +'hash_hmac_algos' => ['array'], +'hash_hmac_file' => ['non-empty-string|false', 'algo'=>'non-empty-string', 'filename'=>'non-empty-string', 'key'=>'string', 'raw_output='=>'bool'], +'hash_init' => ['HashContext', 'algo'=>'non-empty-string', 'options='=>'int', 'key='=>'string'], +'hash_pbkdf2' => ['non-empty-string|false', 'algo'=>'non-empty-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_file' => ['bool', 'context'=>'HashContext', 'filename'=>'non-empty-string', 'scontext='=>'?HashContext'], 'hash_update_stream' => ['int', 'context'=>'HashContext', 'handle'=>'resource', 'length='=>'int'], 'hashTableObj::clear' => ['void'], 'hashTableObj::get' => ['string', 'key'=>'string'], diff --git a/src/Type/Php/HashFunctionsReturnTypeExtension.php b/src/Type/Php/HashFunctionsReturnTypeExtension.php index f977e240ad1..c4f30aebb84 100644 --- a/src/Type/Php/HashFunctionsReturnTypeExtension.php +++ b/src/Type/Php/HashFunctionsReturnTypeExtension.php @@ -6,19 +6,78 @@ use PHPStan\Analyser\Scope; 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\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; 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() + { + $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 @@ -34,13 +93,38 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeUtils::toBenevolentUnion($defaultReturnType); } - $values = TypeUtils::getConstantStrings($argType); - if (count($values) !== 1) { + $constantStringTypes = TypeUtils::getConstantStrings($argType); + + if ($constantStringTypes === []) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - $string = $values[0]; - return in_array($string->getValue(), hash_algos(), true) ? new StringType() : new ConstantBooleanType(false); + $functionData = self::SUPPORTED_FUNCTIONS[strtolower($functionReflection->getName())]; + $false = new ConstantBooleanType(false); + $nonEmptyString = new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + + $returnTypes = array_map( + function (ConstantStringType $type) use ($functionData, $nonEmptyString, $false) { + $algorithm = strtolower($type->getValue()); + if (!in_array($algorithm, $this->hashAlgorithms, true)) { + return $false; + } + if ($functionData['cryptographic'] && in_array($algorithm, self::NON_CRYPTOGRAPHIC_ALGORITHMS, true)) { + return $false; + } + return $nonEmptyString; + }, + $constantStringTypes, + ); + + if ($functionData['possiblyFalse']) { + $returnTypes[] = $false; + } + + return TypeCombinator::union(...$returnTypes); } } diff --git a/src/Type/Php/HashHmacFunctionsReturnTypeExtension.php b/src/Type/Php/HashHmacFunctionsReturnTypeExtension.php deleted file mode 100644 index afc607f6e19..00000000000 --- a/src/Type/Php/HashHmacFunctionsReturnTypeExtension.php +++ /dev/null @@ -1,97 +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 89dbb2ae320..e8a4e3bb9ed 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -5606,62 +5606,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 f52034b8cac..e6fcf1c612f 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -569,6 +569,8 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/filesystem-functions.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/classPhpDocs-phpstanPropertyPrefix.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/hash-functions.php'); } /** diff --git a/tests/PHPStan/Analyser/data/functions.php b/tests/PHPStan/Analyser/data/functions.php index 173a4cd5f58..7c36d6e8545 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.php b/tests/PHPStan/Analyser/data/hash-functions.php new file mode 100644 index 00000000000..bb441f82d55 --- /dev/null +++ b/tests/PHPStan/Analyser/data/hash-functions.php @@ -0,0 +1,84 @@ +