diff --git a/resources/functionMap.php b/resources/functionMap.php index 9891593eb3..f8c4cf4d57 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -3909,18 +3909,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' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], +'hash' => ['non-falsy-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], 'hash_algos' => ['non-empty-list'], 'hash_copy' => ['HashContext', 'context'=>'HashContext'], 'hash_equals' => ['bool', 'known_string'=>'string', 'user_string'=>'string'], -'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_file' => ['non-falsy-string|false', 'algo'=>'string', 'filename'=>'string', 'raw_output='=>'bool'], +'hash_final' => ['non-falsy-string', 'context'=>'HashContext', 'raw_output='=>'bool'], +'hash_hkdf' => ['non-falsy-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], +'hash_hmac' => ['non-falsy-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], 'hash_hmac_algos' => ['non-empty-list'], -'hash_hmac_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'raw_output='=>'bool'], +'hash_hmac_file' => ['non-falsy-string|false', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'raw_output='=>'bool'], 'hash_init' => ['HashContext', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], -'hash_pbkdf2' => ['non-empty-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], +'hash_pbkdf2' => ['(non-falsy-string&lowercase-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 d242a5410a..c6a9dfe91a 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -53,8 +53,8 @@ 'gmmktime' => ['int|false', 'hour'=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'hash' => ['non-falsy-string', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], 'hash_hkdf' => ['non-falsy-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'], + 'hash_hmac' => ['non-falsy-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], + 'hash_pbkdf2' => ['non-falsy-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], 'imageaffine' => ['false|object', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], 'imagecreate' => ['__benevolent', 'width'=>'int', 'height'=>'int'], 'imagecreatefrombmp' => ['false|object', 'filename'=>'string'], @@ -192,10 +192,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' => ['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'], + 'hash' => ['non-falsy-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], + 'hash_hkdf' => ['non-falsy-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash_hmac' => ['non-falsy-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], + 'hash_pbkdf2' => ['non-falsy-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], 'hebrevc' => ['string', 'str'=>'string', 'max_chars_per_line='=>'int'], 'image2wbmp' => ['bool', 'im'=>'resource', 'filename='=>'?string', 'threshold='=>'int'], 'imageaffine' => ['resource|false', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], diff --git a/src/Type/Php/HashFunctionsReturnTypeExtension.php b/src/Type/Php/HashFunctionsReturnTypeExtension.php index 925873293d..85ba080957 100644 --- a/src/Type/Php/HashFunctionsReturnTypeExtension.php +++ b/src/Type/Php/HashFunctionsReturnTypeExtension.php @@ -6,21 +6,22 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; 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 array_map; +use function count; use function hash_algos; use function in_array; +use function is_bool; use function strtolower; final class HashFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -30,26 +31,32 @@ final class HashFunctionsReturnTypeExtension implements DynamicFunctionReturnTyp 'hash' => [ 'cryptographic' => false, 'possiblyFalse' => false, + 'binary' => 2, ], 'hash_file' => [ 'cryptographic' => false, 'possiblyFalse' => true, + 'binary' => 2, ], 'hash_hkdf' => [ 'cryptographic' => true, 'possiblyFalse' => false, + 'binary' => true, ], 'hash_hmac' => [ 'cryptographic' => true, 'possiblyFalse' => false, + 'binary' => 3, ], 'hash_hmac_file' => [ 'cryptographic' => true, 'possiblyFalse' => true, + 'binary' => 3, ], 'hash_pbkdf2' => [ 'cryptographic' => true, 'possiblyFalse' => false, + 'binary' => 5, ], ]; @@ -86,41 +93,46 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return isset(self::SUPPORTED_FUNCTIONS[$name]); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( - $scope, - $functionCall->getArgs(), - $functionReflection->getVariants(), - )->getReturnType(); - if (!isset($functionCall->getArgs()[0])) { - return $defaultReturnType; + return null; } - $algorithmType = $scope->getType($functionCall->getArgs()[0]->value); - if ($algorithmType instanceof MixedType) { - return TypeUtils::toBenevolentUnion($defaultReturnType); + $functionData = self::SUPPORTED_FUNCTIONS[strtolower($functionReflection->getName())]; + if (is_bool($functionData['binary'])) { + $binaryType = new ConstantBooleanType($functionData['binary']); + } elseif (isset($functionCall->getArgs()[$functionData['binary']])) { + $binaryType = $scope->getType($functionCall->getArgs()[$functionData['binary']]->value); + } else { + $binaryType = new ConstantBooleanType(false); + } + + $stringTypes = [ + new StringType(), + new AccessoryNonFalsyStringType(), + ]; + if ($binaryType->isFalse()->yes()) { + $stringTypes[] = new AccessoryLowercaseStringType(); } + $stringReturnType = new IntersectionType($stringTypes); + $algorithmType = $scope->getType($functionCall->getArgs()[0]->value); $constantAlgorithmTypes = $algorithmType->getConstantStrings(); + if (count($constantAlgorithmTypes) === 0) { + if ($functionData['possiblyFalse'] || !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + return TypeUtils::toBenevolentUnion(TypeCombinator::union($stringReturnType, new ConstantBooleanType(false))); + } - if ($constantAlgorithmTypes === []) { - return TypeUtils::toBenevolentUnion($defaultReturnType); + return $stringReturnType; } $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) { + function (ConstantStringType $type) use ($functionData, $stringReturnType, $invalidAlgorithmType) { $algorithm = strtolower($type->getValue()); if (!in_array($algorithm, $this->hashAlgorithms, true)) { return $invalidAlgorithmType; @@ -128,7 +140,7 @@ function (ConstantStringType $type) use ($functionData, $nonEmptyString, $invali if ($functionData['cryptographic'] && in_array($algorithm, self::NON_CRYPTOGRAPHIC_ALGORITHMS, true)) { return $invalidAlgorithmType; } - return $nonEmptyString; + return $stringReturnType; }, $constantAlgorithmTypes, ); diff --git a/tests/PHPStan/Analyser/nsrt/hash-functions-74.php b/tests/PHPStan/Analyser/nsrt/hash-functions-74.php index 0d068d3765..2ffcb920f8 100644 --- a/tests/PHPStan/Analyser/nsrt/hash-functions-74.php +++ b/tests/PHPStan/Analyser/nsrt/hash-functions-74.php @@ -11,7 +11,8 @@ public function hash_hmac(string $string): void { assertType('false', hash_hmac('crc32', 'data', 'key')); assertType('false', hash_hmac('invalid', 'data', 'key')); - assertType('(non-empty-string|false)', hash_hmac($string, 'data', 'key')); + assertType('((lowercase-string&non-falsy-string)|false)', hash_hmac($string, 'data', 'key')); + assertType('(non-falsy-string|false)', hash_hmac($string, 'data', 'key', true)); } public function hash_hmac_file(): void @@ -23,7 +24,8 @@ public function hash_hmac_file(): void public function hash(string $string): void { assertType('false', hash('invalid', 'data', false)); - assertType('(non-empty-string|false)', hash($string, 'data')); + assertType('((lowercase-string&non-falsy-string)|false)', hash($string, 'data')); + assertType('(non-falsy-string|false)', hash($string, 'data', true)); } public function hash_file(): void @@ -35,14 +37,15 @@ public function hash_hkdf(string $string): void { assertType('false', hash_hkdf('crc32', 'key')); assertType('false', hash_hkdf('invalid', 'key')); - assertType('(non-empty-string|false)', hash_hkdf($string, 'key')); + assertType('(non-falsy-string|false)', hash_hkdf($string, 'key')); } public function hash_pbkdf2(string $string): void { assertType('false', hash_pbkdf2('crc32', 'password', 'salt', 1000)); assertType('false', hash_pbkdf2('invalid', 'password', 'salt', 1000)); - assertType('(non-empty-string|false)', hash_pbkdf2($string, 'password', 'salt', 1000)); + assertType('((lowercase-string&non-falsy-string)|false)', hash_pbkdf2($string, 'password', 'salt', 1000)); + assertType('(non-falsy-string|false)', hash_pbkdf2($string, 'password', 'salt', 1000, 0, true)); } public function caseSensitive() diff --git a/tests/PHPStan/Analyser/nsrt/hash-functions-80.php b/tests/PHPStan/Analyser/nsrt/hash-functions-80.php index fe8cd726f0..1d11247d53 100644 --- a/tests/PHPStan/Analyser/nsrt/hash-functions-80.php +++ b/tests/PHPStan/Analyser/nsrt/hash-functions-80.php @@ -11,7 +11,8 @@ public function hash_hmac(string $string): void { assertType('*NEVER*', hash_hmac('crc32', 'data', 'key')); assertType('*NEVER*', hash_hmac('invalid', 'data', 'key')); - assertType('non-empty-string', hash_hmac($string, 'data', 'key')); + assertType('lowercase-string&non-falsy-string', hash_hmac($string, 'data', 'key')); + assertType('non-falsy-string', hash_hmac($string, 'data', 'key', true)); } public function hash_hmac_file(): void @@ -23,7 +24,7 @@ public function hash_hmac_file(): void public function hash(string $string): void { assertType('*NEVER*', hash('invalid', 'data', false)); - assertType('non-falsy-string', hash($string, 'data')); + assertType('lowercase-string&non-falsy-string', hash($string, 'data')); } public function hash_file(): void @@ -42,7 +43,7 @@ public function hash_pbkdf2(string $string): void { assertType('*NEVER*', hash_pbkdf2('crc32', 'password', 'salt', 1000)); assertType('*NEVER*', hash_pbkdf2('invalid', 'password', 'salt', 1000)); - assertType('non-empty-string', hash_pbkdf2($string, 'password', 'salt', 1000)); + assertType('lowercase-string&non-falsy-string', hash_pbkdf2($string, 'password', 'salt', 1000)); } public function caseSensitive() diff --git a/tests/PHPStan/Analyser/nsrt/hash-functions.php b/tests/PHPStan/Analyser/nsrt/hash-functions.php index 17b9eb4d8c..72f977baae 100644 --- a/tests/PHPStan/Analyser/nsrt/hash-functions.php +++ b/tests/PHPStan/Analyser/nsrt/hash-functions.php @@ -13,44 +13,52 @@ class HashFunctionTests public function hash_hmac(): void { - assertType('non-empty-string', hash_hmac('md5', 'data', 'key')); - assertType('non-empty-string', hash_hmac('sha256', 'data', 'key')); + assertType('lowercase-string&non-falsy-string', hash_hmac('md5', 'data', 'key')); + assertType('non-falsy-string', hash_hmac('md5', 'data', 'key', true)); + assertType('lowercase-string&non-falsy-string', hash_hmac('sha256', 'data', 'key')); + assertType('non-falsy-string', hash_hmac('sha256', 'data', 'key', true)); } public function hash_hmac_file(string $string): void { - assertType('non-empty-string|false', hash_hmac_file('md5', 'filename', 'key')); - assertType('non-empty-string|false', hash_hmac_file('sha256', 'filename', 'key')); - assertType('(non-empty-string|false)', hash_hmac_file($string, 'filename', 'key')); + assertType('(lowercase-string&non-falsy-string)|false', hash_hmac_file('md5', 'filename', 'key')); + assertType('non-falsy-string|false', hash_hmac_file('md5', 'filename', 'key', true)); + assertType('(lowercase-string&non-falsy-string)|false', hash_hmac_file('sha256', 'filename', 'key')); + assertType('non-falsy-string|false', hash_hmac_file('sha256', 'filename', 'key', true)); + assertType('((lowercase-string&non-falsy-string)|false)', hash_hmac_file($string, 'filename', 'key')); + assertType('(non-falsy-string|false)', hash_hmac_file($string, 'filename', 'key', true)); } public function hash($mixed): void { - assertType('non-empty-string', hash('sha256', 'data', false)); - assertType('non-empty-string', hash('sha256', 'data', true)); - assertType('non-empty-string', hash('md5', $mixed, false)); + assertType('lowercase-string&non-falsy-string', hash('sha256', 'data', false)); + assertType('non-falsy-string', hash('sha256', 'data', true)); + assertType('lowercase-string&non-falsy-string', hash('md5', $mixed, false)); } public function hash_file(): void { - assertType('non-empty-string|false', hash_file('sha256', 'filename', false)); - assertType('non-empty-string|false', hash_file('sha256', 'filename', true)); - assertType('non-empty-string|false', hash_file('crc32', 'filename')); + assertType('(lowercase-string&non-falsy-string)|false', hash_file('sha256', 'filename', false)); + assertType('non-falsy-string|false', hash_file('sha256', 'filename', true)); + assertType('(lowercase-string&non-falsy-string)|false', hash_file('crc32', 'filename')); + assertType('non-falsy-string|false', hash_file('crc32', 'filename', true)); } public function hash_hkdf(): void { - assertType('non-empty-string', hash_hkdf('sha256', 'key')); + assertType('non-falsy-string', hash_hkdf('sha256', 'key')); } public function hash_pbkdf2(): void { - assertType('non-empty-string', hash_pbkdf2('sha256', 'password', 'salt', 1000)); + assertType('lowercase-string&non-falsy-string', hash_pbkdf2('sha256', 'password', 'salt', 1000)); + assertType('non-falsy-string', hash_pbkdf2('sha256', 'password', 'salt', 1000, 0, true)); } public function caseSensitive() { - assertType('non-empty-string', hash('SHA256', 'data')); + assertType('lowercase-string&non-falsy-string', hash('SHA256', 'data')); + assertType('non-falsy-string', hash('SHA256', 'data', true)); } public function constantStrings(int $type) @@ -69,7 +77,8 @@ public function constantStrings(int $type) return; } - assertType('non-empty-string', hash_pbkdf2($algorithm, 'password', 'salt', 1000)); + assertType('lowercase-string&non-falsy-string', hash_pbkdf2($algorithm, 'password', 'salt', 1000)); + assertType('non-falsy-string', hash_pbkdf2($algorithm, 'password', 'salt', 1000, 0, true)); } }