From ab267509c784586e7888d7157d2bf34eed667a70 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Sat, 29 Aug 2026 18:48:34 +0200 Subject: [PATCH] Give password_get_info() the array shape it always returns It returned a bare array, so $info['algo'] was mixed and a key typo went unreported. The shape is fixed and documented: algo, algoName and options, with algo changing from int to string|null in PHP 8.0. Measured on the two targets, before and after: phpVersion 70400 array -> array{algo: int, ...} $info['algo'] mixed -> int phpVersion 80300 array -> array{algo: string|null, ...} $info['algo'] mixed -> string|null The baseline entry carries the pre-8.0 signature and the 8.0 delta carries the newer one, matching password_hash() directly above it, since the deltas apply forward from the oldest map. This removes a false negative rather than only sharpening a type. In real code "if (!$info['algo'])" is a condition on mixed, which phpstan-strict-rules passes through its implicit-mixed branch and never reports. With the shape it is a condition on string|null and gets reported, which is what the rule is for. --- resources/functionMap.php | 2 +- resources/functionMap_php80delta.php | 2 ++ .../PHPStan/Analyser/nsrt/password-get-info.php | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/password-get-info.php diff --git a/resources/functionMap.php b/resources/functionMap.php index 3602437267d..20d79d0bdcc 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -7525,7 +7525,7 @@ 'parsekit_compile_string' => ['array', 'phpcode'=>'string', 'errors='=>'array', 'options='=>'int'], 'parsekit_func_arginfo' => ['array', 'function'=>'mixed'], 'passthru' => ['void', 'command'=>'string', '&w_return_value='=>'int'], -'password_get_info' => ['array', 'hash'=>'string'], +'password_get_info' => ['array{algo: int, algoName: string, options: array}', 'hash'=>'string'], 'password_hash' => ['__benevolent', 'password'=>'string', 'algo'=>'string|int', 'options='=>'array'], 'password_make_salt' => ['bool', 'password'=>'string', 'hash'=>'string'], 'password_needs_rehash' => ['bool', 'hash'=>'string', 'algo'=>'int', 'options='=>'array'], diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index e27225eabf1..2a4d9e6a79e 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -96,6 +96,7 @@ 'odbc_do' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string'], 'opcache_get_configuration' => ['array{directives: array{\'opcache.enable\': bool, \'opcache.enable_cli\': bool, \'opcache.use_cwd\': bool, \'opcache.validate_timestamps\': bool, \'opcache.validate_permission\': bool, \'opcache.validate_root\'?: bool, \'opcache.dups_fix\': bool, \'opcache.revalidate_path\': bool, \'opcache.log_verbosity_level\': int, \'opcache.memory_consumption\': int, \'opcache.interned_strings_buffer\': int, \'opcache.max_accelerated_files\': int, \'opcache.max_wasted_percentage\': float, \'opcache.consistency_checks\': int, \'opcache.force_restart_timeout\': int, \'opcache.revalidate_freq\': int, \'opcache.preferred_memory_model\': string, \'opcache.blacklist_filename\': string, \'opcache.max_file_size\': int, \'opcache.error_log\': string, \'opcache.protect_memory\': bool, \'opcache.save_comments\': bool, \'opcache.record_warnings\': bool, \'opcache.enable_file_override\': bool, \'opcache.optimization_level\': int, \'opcache.lockfile_path\'?: string, \'opcache.mmap_base\'?: string, \'opcache.file_cache\': string, \'opcache.file_cache_only\': bool, \'opcache.file_cache_consistency_checks\': bool, \'opcache.file_cache_fallback\'?: bool, \'opcache.file_update_protection\': int, \'opcache.opt_debug_level\': int, \'opcache.restrict_api\': string, \'opcache.huge_code_pages\'?: bool, \'opcache.preload\': string, \'opcache.preload_user\'?: string, \'opcache.cache_id\'?: string, \'opcache.jit\'?: string, \'opcache.jit_buffer_size\'?: int, \'opcache.jit_debug\'?: int, \'opcache.jit_bisect_limit\'?: int, \'opcache.jit_blacklist_root_trace\'?: int, \'opcache.jit_blacklist_side_trace\'?: int, \'opcache.jit_hot_func\'?: int, \'opcache.jit_hot_loop\'?: int, \'opcache.jit_hot_return\'?: int, \'opcache.jit_hot_side_exit\'?: int, \'opcache.jit_max_exit_counters\'?: int, \'opcache.jit_max_loop_unrolls\'?: int, \'opcache.jit_max_polymorphic_calls\'?: int, \'opcache.jit_max_recursive_calls\'?: int, \'opcache.jit_max_recursive_returns\'?: int, \'opcache.jit_max_root_traces\'?: int, \'opcache.jit_max_side_traces\'?: int, \'opcache.jit_prof_threshold\'?: int}, version: array{version: non-empty-string, opcache_product_name: non-empty-string}, blacklist: list}|false'], 'parse_str' => ['void', 'encoded_string'=>'string', '&w_result'=>'array'], + 'password_get_info' => ['array{algo: string|null, algoName: string, options: array}', 'hash'=>'string'], 'password_hash' => ['non-empty-string', 'password'=>'string', 'algo'=>'string|int|null', 'options='=>'array'], 'PDOStatement::fetchAll' => ['array', 'mode='=>'int'], 'PDOStatement::fetchAll\'1' => ['array', 'mode'=>'int', 'column'=>'int'], @@ -251,6 +252,7 @@ 'odbc_exec' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string', 'flags='=>'int'], 'opcache_get_configuration' => ['array{directives: array{\'opcache.enable\': bool, \'opcache.enable_cli\': bool, \'opcache.use_cwd\': bool, \'opcache.validate_timestamps\': bool, \'opcache.validate_permission\': bool, \'opcache.validate_root\'?: bool, \'opcache.dups_fix\': bool, \'opcache.revalidate_path\': bool, \'opcache.log_verbosity_level\': int, \'opcache.memory_consumption\': int, \'opcache.interned_strings_buffer\': int, \'opcache.max_accelerated_files\': int, \'opcache.max_wasted_percentage\': float, \'opcache.consistency_checks\': int, \'opcache.force_restart_timeout\': int, \'opcache.revalidate_freq\': int, \'opcache.preferred_memory_model\': string, \'opcache.blacklist_filename\': string, \'opcache.max_file_size\': int, \'opcache.error_log\': string, \'opcache.protect_memory\': bool, \'opcache.save_comments\': bool, \'opcache.enable_file_override\': bool, \'opcache.optimization_level\': int, \'opcache.lockfile_path\'?: string, \'opcache.mmap_base\'?: string, \'opcache.file_cache\': string, \'opcache.file_cache_only\': bool, \'opcache.file_cache_consistency_checks\': bool, \'opcache.file_cache_fallback\'?: bool, \'opcache.file_update_protection\': int, \'opcache.opt_debug_level\': int, \'opcache.restrict_api\': string, \'opcache.huge_code_pages\'?: bool, \'opcache.preload\': string, \'opcache.preload_user\'?: string, \'opcache.cache_id\'?: string}, version: array{version: non-empty-string, opcache_product_name: non-empty-string}, blacklist: list}|false'], 'parse_str' => ['void', 'encoded_string'=>'string', '&w_result='=>'array'], + 'password_get_info' => ['array{algo: int, algoName: string, options: array}', 'hash'=>'string'], 'password_hash' => ['__benevolent', 'password'=>'string', 'algo'=>'string|int', 'options='=>'array'], 'png2wbmp' => ['bool', 'pngname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'], 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}|false', 'process'=>'resource'], diff --git a/tests/PHPStan/Analyser/nsrt/password-get-info.php b/tests/PHPStan/Analyser/nsrt/password-get-info.php new file mode 100644 index 00000000000..6a2274d387d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/password-get-info.php @@ -0,0 +1,16 @@ += 8.0 + +namespace PasswordGetInfo; + +use function PHPStan\Testing\assertType; +use function password_get_info; + +function knownShape(string $hash): void +{ + $info = password_get_info($hash); + + assertType('array{algo: string|null, algoName: string, options: array}', $info); + assertType('string|null', $info['algo']); + assertType('string', $info['algoName']); + assertType('array', $info['options']); +}