diff --git a/resources/constantToFunctionParameterMap.php b/resources/constantToFunctionParameterMap.php new file mode 100644 index 00000000000..db150a3ad73 --- /dev/null +++ b/resources/constantToFunctionParameterMap.php @@ -0,0 +1,2451 @@ + 'single' | 'bitmask' + * 'constants' => list of constant names valid for this parameter + * 'exclusiveGroups' => (optional, bitmask only) groups of constants that are mutually exclusive + */ +return [ + + // ———————————————————————————————————————————— + // JSON + // ———————————————————————————————————————————— + + 'json_encode' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'JSON_HEX_QUOT', + 'JSON_HEX_TAG', + 'JSON_HEX_AMP', + 'JSON_HEX_APOS', + 'JSON_NUMERIC_CHECK', + 'JSON_PRETTY_PRINT', + 'JSON_UNESCAPED_SLASHES', + 'JSON_FORCE_OBJECT', + 'JSON_PRESERVE_ZERO_FRACTION', + 'JSON_UNESCAPED_UNICODE', + 'JSON_PARTIAL_OUTPUT_ON_ERROR', + 'JSON_UNESCAPED_LINE_TERMINATORS', + 'JSON_THROW_ON_ERROR', + 'JSON_INVALID_UTF8_IGNORE', + 'JSON_INVALID_UTF8_SUBSTITUTE', + ], + ], + ], + + 'json_decode' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'JSON_BIGINT_AS_STRING', + 'JSON_OBJECT_AS_ARRAY', + 'JSON_THROW_ON_ERROR', + 'JSON_INVALID_UTF8_IGNORE', + 'JSON_INVALID_UTF8_SUBSTITUTE', + ], + ], + ], + + 'json_validate' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'JSON_INVALID_UTF8_IGNORE', + ], + ], + ], + + // ———————————————————————————————————————————— + // PCRE + // ———————————————————————————————————————————— + + 'preg_match' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PREG_OFFSET_CAPTURE', + 'PREG_UNMATCHED_AS_NULL', + ], + ], + ], + + 'preg_match_all' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PREG_PATTERN_ORDER', + 'PREG_SET_ORDER', + 'PREG_OFFSET_CAPTURE', + 'PREG_UNMATCHED_AS_NULL', + ], + 'exclusiveGroups' => [ + ['PREG_PATTERN_ORDER', 'PREG_SET_ORDER'], + ], + ], + ], + + 'preg_split' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PREG_SPLIT_NO_EMPTY', + 'PREG_SPLIT_DELIM_CAPTURE', + 'PREG_SPLIT_OFFSET_CAPTURE', + ], + ], + ], + + 'preg_grep' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'PREG_GREP_INVERT', + ], + ], + ], + + // ———————————————————————————————————————————— + // Sorting + // ———————————————————————————————————————————— + + 'sort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'rsort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'asort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'arsort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'ksort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'krsort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'array_unique' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + ], + ], + ], + + // ———————————————————————————————————————————— + // Array functions + // ———————————————————————————————————————————— + + 'array_change_key_case' => [ + 'case' => [ + 'type' => 'single', + 'constants' => [ + 'CASE_LOWER', + 'CASE_UPPER', + ], + ], + ], + + 'array_filter' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'ARRAY_FILTER_USE_KEY', + 'ARRAY_FILTER_USE_BOTH', + ], + ], + ], + + 'count' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'COUNT_NORMAL', + 'COUNT_RECURSIVE', + ], + ], + ], + + 'extract' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'EXTR_OVERWRITE', + 'EXTR_SKIP', + 'EXTR_PREFIX_SAME', + 'EXTR_PREFIX_ALL', + 'EXTR_PREFIX_INVALID', + 'EXTR_IF_EXISTS', + 'EXTR_PREFIX_IF_EXISTS', + 'EXTR_REFS', + ], + 'exclusiveGroups' => [ + ['EXTR_OVERWRITE', 'EXTR_SKIP', 'EXTR_PREFIX_SAME', 'EXTR_PREFIX_ALL', 'EXTR_PREFIX_INVALID', 'EXTR_IF_EXISTS', 'EXTR_PREFIX_IF_EXISTS'], + ], + ], + ], + + // ———————————————————————————————————————————— + // HTML entities + // ———————————————————————————————————————————— + + 'htmlspecialchars' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ENT_COMPAT', + 'ENT_QUOTES', + 'ENT_NOQUOTES', + 'ENT_IGNORE', + 'ENT_SUBSTITUTE', + 'ENT_DISALLOWED', + 'ENT_HTML401', + 'ENT_XML1', + 'ENT_XHTML', + 'ENT_HTML5', + ], + 'exclusiveGroups' => [ + ['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], + ['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], + ], + ], + ], + + 'htmlentities' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ENT_COMPAT', + 'ENT_QUOTES', + 'ENT_NOQUOTES', + 'ENT_IGNORE', + 'ENT_SUBSTITUTE', + 'ENT_DISALLOWED', + 'ENT_HTML401', + 'ENT_XML1', + 'ENT_XHTML', + 'ENT_HTML5', + ], + 'exclusiveGroups' => [ + ['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], + ['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], + ], + ], + ], + + 'html_entity_decode' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ENT_COMPAT', + 'ENT_QUOTES', + 'ENT_NOQUOTES', + 'ENT_IGNORE', + 'ENT_SUBSTITUTE', + 'ENT_DISALLOWED', + 'ENT_HTML401', + 'ENT_XML1', + 'ENT_XHTML', + 'ENT_HTML5', + ], + 'exclusiveGroups' => [ + ['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], + ['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], + ], + ], + ], + + 'htmlspecialchars_decode' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ENT_COMPAT', + 'ENT_QUOTES', + 'ENT_NOQUOTES', + 'ENT_IGNORE', + 'ENT_SUBSTITUTE', + 'ENT_DISALLOWED', + 'ENT_HTML401', + 'ENT_XML1', + 'ENT_XHTML', + 'ENT_HTML5', + ], + 'exclusiveGroups' => [ + ['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], + ['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], + ], + ], + ], + + // ———————————————————————————————————————————— + // URL / Path + // ———————————————————————————————————————————— + + 'parse_url' => [ + 'component' => [ + 'type' => 'single', + 'constants' => [ + 'PHP_URL_SCHEME', + 'PHP_URL_HOST', + 'PHP_URL_PORT', + 'PHP_URL_USER', + 'PHP_URL_PASS', + 'PHP_URL_PATH', + 'PHP_URL_QUERY', + 'PHP_URL_FRAGMENT', + ], + ], + ], + + 'pathinfo' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PATHINFO_DIRNAME', + 'PATHINFO_BASENAME', + 'PATHINFO_EXTENSION', + 'PATHINFO_FILENAME', + 'PATHINFO_ALL', + ], + ], + ], + + 'http_build_query' => [ + 'encoding_type' => [ + 'type' => 'single', + 'constants' => [ + 'PHP_QUERY_RFC1738', + 'PHP_QUERY_RFC3986', + ], + ], + ], + + // ———————————————————————————————————————————— + // File operations + // ———————————————————————————————————————————— + + 'file_put_contents' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILE_USE_INCLUDE_PATH', + 'FILE_APPEND', + 'LOCK_EX', + ], + ], + ], + + 'file' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILE_USE_INCLUDE_PATH', + 'FILE_IGNORE_NEW_LINES', + 'FILE_SKIP_EMPTY_LINES', + 'FILE_NO_DEFAULT_CONTEXT', + ], + ], + ], + + 'flock' => [ + 'operation' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LOCK_SH', + 'LOCK_EX', + 'LOCK_UN', + 'LOCK_NB', + ], + 'exclusiveGroups' => [ + ['LOCK_SH', 'LOCK_EX', 'LOCK_UN'], + ], + ], + ], + + 'glob' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'GLOB_MARK', + 'GLOB_NOSORT', + 'GLOB_NOCHECK', + 'GLOB_NOESCAPE', + 'GLOB_BRACE', + 'GLOB_ONLYDIR', + 'GLOB_ERR', + ], + ], + ], + + 'fnmatch' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FNM_NOESCAPE', + 'FNM_PATHNAME', + 'FNM_PERIOD', + 'FNM_CASEFOLD', + ], + ], + ], + + 'scandir' => [ + 'sorting_order' => [ + 'type' => 'single', + 'constants' => [ + 'SCANDIR_SORT_ASCENDING', + 'SCANDIR_SORT_DESCENDING', + 'SCANDIR_SORT_NONE', + ], + ], + ], + + // ———————————————————————————————————————————— + // Math + // ———————————————————————————————————————————— + + 'round' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'PHP_ROUND_HALF_UP', + 'PHP_ROUND_HALF_DOWN', + 'PHP_ROUND_HALF_EVEN', + 'PHP_ROUND_HALF_ODD', + ], + ], + ], + + // ———————————————————————————————————————————— + // Random + // ———————————————————————————————————————————— + + 'srand' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'MT_RAND_MT19937', + 'MT_RAND_PHP', + ], + ], + ], + + 'mt_srand' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'MT_RAND_MT19937', + 'MT_RAND_PHP', + ], + ], + ], + + // ———————————————————————————————————————————— + // Filter + // ———————————————————————————————————————————— + + 'filter_var' => [ + 'filter' => [ + 'type' => 'single', + 'constants' => [ + 'FILTER_VALIDATE_INT', + 'FILTER_VALIDATE_BOOLEAN', + 'FILTER_VALIDATE_FLOAT', + 'FILTER_VALIDATE_REGEXP', + 'FILTER_VALIDATE_DOMAIN', + 'FILTER_VALIDATE_URL', + 'FILTER_VALIDATE_EMAIL', + 'FILTER_VALIDATE_IP', + 'FILTER_VALIDATE_MAC', + 'FILTER_SANITIZE_STRING', + 'FILTER_SANITIZE_STRIPPED', + 'FILTER_SANITIZE_ENCODED', + 'FILTER_SANITIZE_SPECIAL_CHARS', + 'FILTER_SANITIZE_FULL_SPECIAL_CHARS', + 'FILTER_SANITIZE_EMAIL', + 'FILTER_SANITIZE_URL', + 'FILTER_SANITIZE_NUMBER_INT', + 'FILTER_SANITIZE_NUMBER_FLOAT', + 'FILTER_SANITIZE_ADD_SLASHES', + 'FILTER_UNSAFE_RAW', + 'FILTER_DEFAULT', + 'FILTER_CALLBACK', + ], + ], + ], + + 'filter_input' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'INPUT_POST', + 'INPUT_GET', + 'INPUT_COOKIE', + 'INPUT_ENV', + 'INPUT_SERVER', + ], + ], + 'filter' => [ + 'type' => 'single', + 'constants' => [ + 'FILTER_VALIDATE_INT', + 'FILTER_VALIDATE_BOOLEAN', + 'FILTER_VALIDATE_FLOAT', + 'FILTER_VALIDATE_REGEXP', + 'FILTER_VALIDATE_DOMAIN', + 'FILTER_VALIDATE_URL', + 'FILTER_VALIDATE_EMAIL', + 'FILTER_VALIDATE_IP', + 'FILTER_VALIDATE_MAC', + 'FILTER_SANITIZE_STRING', + 'FILTER_SANITIZE_STRIPPED', + 'FILTER_SANITIZE_ENCODED', + 'FILTER_SANITIZE_SPECIAL_CHARS', + 'FILTER_SANITIZE_FULL_SPECIAL_CHARS', + 'FILTER_SANITIZE_EMAIL', + 'FILTER_SANITIZE_URL', + 'FILTER_SANITIZE_NUMBER_INT', + 'FILTER_SANITIZE_NUMBER_FLOAT', + 'FILTER_SANITIZE_ADD_SLASHES', + 'FILTER_UNSAFE_RAW', + 'FILTER_DEFAULT', + 'FILTER_CALLBACK', + ], + ], + ], + + 'filter_input_array' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'INPUT_POST', + 'INPUT_GET', + 'INPUT_COOKIE', + 'INPUT_ENV', + 'INPUT_SERVER', + ], + ], + ], + + // ———————————————————————————————————————————— + // Password hashing + // ———————————————————————————————————————————— + + 'password_hash' => [ + 'algo' => [ + 'type' => 'single', + 'constants' => [ + 'PASSWORD_DEFAULT', + 'PASSWORD_BCRYPT', + 'PASSWORD_ARGON2I', + 'PASSWORD_ARGON2ID', + ], + ], + ], + + 'password_needs_rehash' => [ + 'algo' => [ + 'type' => 'single', + 'constants' => [ + 'PASSWORD_DEFAULT', + 'PASSWORD_BCRYPT', + 'PASSWORD_ARGON2I', + 'PASSWORD_ARGON2ID', + ], + ], + ], + + // ———————————————————————————————————————————— + // Error handling + // ———————————————————————————————————————————— + + 'error_reporting' => [ + 'error_level' => [ + 'type' => 'bitmask', + 'constants' => [ + 'E_ALL', + 'E_ERROR', + 'E_WARNING', + 'E_PARSE', + 'E_NOTICE', + 'E_STRICT', + 'E_RECOVERABLE_ERROR', + 'E_DEPRECATED', + 'E_CORE_ERROR', + 'E_CORE_WARNING', + 'E_COMPILE_ERROR', + 'E_COMPILE_WARNING', + 'E_USER_ERROR', + 'E_USER_WARNING', + 'E_USER_NOTICE', + 'E_USER_DEPRECATED', + ], + ], + ], + + 'trigger_error' => [ + 'error_level' => [ + 'type' => 'single', + 'constants' => [ + 'E_USER_NOTICE', + 'E_USER_WARNING', + 'E_USER_ERROR', + 'E_USER_DEPRECATED', + ], + ], + ], + + 'user_error' => [ + 'error_level' => [ + 'type' => 'single', + 'constants' => [ + 'E_USER_NOTICE', + 'E_USER_WARNING', + 'E_USER_ERROR', + 'E_USER_DEPRECATED', + ], + ], + ], + + // ———————————————————————————————————————————— + // Multibyte string + // ———————————————————————————————————————————— + + 'mb_convert_case' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'MB_CASE_UPPER', + 'MB_CASE_LOWER', + 'MB_CASE_TITLE', + 'MB_CASE_FOLD', + 'MB_CASE_UPPER_SIMPLE', + 'MB_CASE_LOWER_SIMPLE', + 'MB_CASE_TITLE_SIMPLE', + 'MB_CASE_FOLD_SIMPLE', + ], + ], + ], + + // ———————————————————————————————————————————— + // Fileinfo + // ———————————————————————————————————————————— + + 'finfo_file' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + // ———————————————————————————————————————————— + // Debug + // ———————————————————————————————————————————— + + 'debug_backtrace' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'DEBUG_BACKTRACE_PROVIDE_OBJECT', + 'DEBUG_BACKTRACE_IGNORE_ARGS', + ], + ], + ], + + 'debug_print_backtrace' => [ + 'options' => [ + 'type' => 'single', + 'constants' => [ + 'DEBUG_BACKTRACE_IGNORE_ARGS', + ], + ], + ], + + // ———————————————————————————————————————————— + // Tokenizer + // ———————————————————————————————————————————— + + 'token_get_all' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'TOKEN_PARSE', + ], + ], + ], + + // cURL constants are excluded from this map because the constant lists + // are large and grow with each PHP/libcurl release, making them impractical + // to maintain without false positives. + + + 'image_type_to_extension' => [ + 'image_type' => [ + 'type' => 'single', + 'constants' => [ + 'IMAGETYPE_GIF', + 'IMAGETYPE_JPEG', + 'IMAGETYPE_PNG', + 'IMAGETYPE_SWF', + 'IMAGETYPE_PSD', + 'IMAGETYPE_BMP', + 'IMAGETYPE_WBMP', + 'IMAGETYPE_XBM', + 'IMAGETYPE_TIFF_II', + 'IMAGETYPE_TIFF_MM', + 'IMAGETYPE_ICO', + 'IMAGETYPE_WEBP', + 'IMAGETYPE_AVIF', + 'IMAGETYPE_JPC', + 'IMAGETYPE_JP2', + 'IMAGETYPE_JPX', + 'IMAGETYPE_JB2', + 'IMAGETYPE_SWC', + 'IMAGETYPE_IFF', + ], + ], + ], + + 'image_type_to_mime_type' => [ + 'image_type' => [ + 'type' => 'single', + 'constants' => [ + 'IMAGETYPE_GIF', + 'IMAGETYPE_JPEG', + 'IMAGETYPE_PNG', + 'IMAGETYPE_SWF', + 'IMAGETYPE_PSD', + 'IMAGETYPE_BMP', + 'IMAGETYPE_WBMP', + 'IMAGETYPE_XBM', + 'IMAGETYPE_TIFF_II', + 'IMAGETYPE_TIFF_MM', + 'IMAGETYPE_ICO', + 'IMAGETYPE_WEBP', + 'IMAGETYPE_AVIF', + 'IMAGETYPE_JPC', + 'IMAGETYPE_JP2', + 'IMAGETYPE_JPX', + 'IMAGETYPE_JB2', + 'IMAGETYPE_SWC', + 'IMAGETYPE_IFF', + ], + ], + ], + + // ———————————————————————————————————————————— + // GD image functions + // ———————————————————————————————————————————— + + 'imagecropauto' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'IMG_CROP_DEFAULT', + 'IMG_CROP_TRANSPARENT', + 'IMG_CROP_BLACK', + 'IMG_CROP_WHITE', + 'IMG_CROP_SIDES', + 'IMG_CROP_THRESHOLD', + ], + ], + ], + + 'imagelayereffect' => [ + 'effect' => [ + 'type' => 'single', + 'constants' => [ + 'IMG_EFFECT_REPLACE', + 'IMG_EFFECT_ALPHABLEND', + 'IMG_EFFECT_NORMAL', + 'IMG_EFFECT_OVERLAY', + 'IMG_EFFECT_MULTIPLY', + ], + ], + ], + + 'imageflip' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'IMG_FLIP_HORIZONTAL', + 'IMG_FLIP_VERTICAL', + 'IMG_FLIP_BOTH', + ], + ], + ], + + 'imagefilter' => [ + 'filter' => [ + 'type' => 'single', + 'constants' => [ + 'IMG_FILTER_NEGATE', + 'IMG_FILTER_GRAYSCALE', + 'IMG_FILTER_BRIGHTNESS', + 'IMG_FILTER_CONTRAST', + 'IMG_FILTER_COLORIZE', + 'IMG_FILTER_EDGEDETECT', + 'IMG_FILTER_GAUSSIAN_BLUR', + 'IMG_FILTER_SELECTIVE_BLUR', + 'IMG_FILTER_EMBOSS', + 'IMG_FILTER_MEAN_REMOVAL', + 'IMG_FILTER_SMOOTH', + 'IMG_FILTER_PIXELATE', + 'IMG_FILTER_SCATTER', + ], + ], + ], + + // ———————————————————————————————————————————— + // Iconv + // ———————————————————————————————————————————— + + 'iconv_mime_decode' => [ + 'mode' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ICONV_MIME_DECODE_STRICT', + 'ICONV_MIME_DECODE_CONTINUE_ON_ERROR', + ], + ], + ], + + 'iconv_mime_decode_headers' => [ + 'mode' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ICONV_MIME_DECODE_STRICT', + 'ICONV_MIME_DECODE_CONTINUE_ON_ERROR', + ], + ], + ], + + // ———————————————————————————————————————————— + // Output buffering + // ———————————————————————————————————————————— + + 'ob_start' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PHP_OUTPUT_HANDLER_CLEANABLE', + 'PHP_OUTPUT_HANDLER_FLUSHABLE', + 'PHP_OUTPUT_HANDLER_REMOVABLE', + 'PHP_OUTPUT_HANDLER_STDFLAGS', + ], + ], + ], + + // ———————————————————————————————————————————— + // Streams + // ———————————————————————————————————————————— + + 'stream_socket_client' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'STREAM_CLIENT_CONNECT', + 'STREAM_CLIENT_ASYNC_CONNECT', + 'STREAM_CLIENT_PERSISTENT', + ], + ], + ], + + 'stream_socket_server' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'STREAM_SERVER_BIND', + 'STREAM_SERVER_LISTEN', + ], + ], + ], + + 'stream_socket_recvfrom' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'STREAM_OOB', + 'STREAM_PEEK', + ], + ], + ], + + 'stream_socket_sendto' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'STREAM_OOB', + ], + ], + ], + + 'stream_wrapper_register' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'STREAM_IS_URL', + ], + ], + ], + + 'stream_socket_shutdown' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'STREAM_SHUT_RD', + 'STREAM_SHUT_WR', + 'STREAM_SHUT_RDWR', + ], + ], + ], + + // ———————————————————————————————————————————— + // Syslog + // ———————————————————————————————————————————— + + 'openlog' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LOG_CONS', + 'LOG_NDELAY', + 'LOG_ODELAY', + 'LOG_NOWAIT', + 'LOG_PERROR', + 'LOG_PID', + ], + ], + 'facility' => [ + 'type' => 'single', + 'constants' => [ + 'LOG_AUTH', + 'LOG_AUTHPRIV', + 'LOG_CRON', + 'LOG_DAEMON', + 'LOG_KERN', + 'LOG_LOCAL0', + 'LOG_LOCAL1', + 'LOG_LOCAL2', + 'LOG_LOCAL3', + 'LOG_LOCAL4', + 'LOG_LOCAL5', + 'LOG_LOCAL6', + 'LOG_LOCAL7', + 'LOG_LPR', + 'LOG_MAIL', + 'LOG_NEWS', + 'LOG_SYSLOG', + 'LOG_USER', + 'LOG_UUCP', + ], + ], + ], + + 'syslog' => [ + 'priority' => [ + 'type' => 'single', + 'constants' => [ + 'LOG_EMERG', + 'LOG_ALERT', + 'LOG_CRIT', + 'LOG_ERR', + 'LOG_WARNING', + 'LOG_NOTICE', + 'LOG_INFO', + 'LOG_DEBUG', + ], + ], + ], + + // ———————————————————————————————————————————— + // Sockets + // ———————————————————————————————————————————— + + 'socket_recv' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_OOB', + 'MSG_PEEK', + 'MSG_WAITALL', + 'MSG_DONTWAIT', + ], + ], + ], + + 'socket_recvfrom' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_OOB', + 'MSG_PEEK', + 'MSG_WAITALL', + 'MSG_DONTWAIT', + ], + ], + ], + + 'socket_send' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_OOB', + 'MSG_EOR', + 'MSG_EOF', + 'MSG_DONTROUTE', + ], + ], + ], + + 'socket_sendto' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_OOB', + 'MSG_EOR', + 'MSG_EOF', + 'MSG_DONTROUTE', + ], + ], + ], + + // ———————————————————————————————————————————— + // DNS + // ———————————————————————————————————————————— + + 'dns_get_record' => [ + 'type' => [ + 'type' => 'bitmask', + 'constants' => [ + 'DNS_ANY', + 'DNS_ALL', + 'DNS_A', + 'DNS_AAAA', + 'DNS_CNAME', + 'DNS_HINFO', + 'DNS_MX', + 'DNS_NS', + 'DNS_PTR', + 'DNS_SOA', + 'DNS_SRV', + 'DNS_TXT', + 'DNS_NAPTR', + 'DNS_A6', + 'DNS_CAA', + ], + ], + ], + + // ———————————————————————————————————————————— + // FTP + // ———————————————————————————————————————————— + + 'ftp_get' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'FTP_ASCII', + 'FTP_BINARY', + ], + ], + ], + + 'ftp_fget' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'FTP_ASCII', + 'FTP_BINARY', + ], + ], + ], + + 'ftp_put' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'FTP_ASCII', + 'FTP_BINARY', + ], + ], + ], + + 'ftp_fput' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'FTP_ASCII', + 'FTP_BINARY', + ], + ], + ], + + // ———————————————————————————————————————————— + // IMAP + // ———————————————————————————————————————————— + + 'imap_close' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'CL_EXPUNGE', + ], + ], + ], + + // ———————————————————————————————————————————— + // OpenSSL + // ———————————————————————————————————————————— + + 'openssl_pkcs7_verify' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PKCS7_TEXT', + 'PKCS7_BINARY', + 'PKCS7_NOINTERN', + 'PKCS7_NOVERIFY', + 'PKCS7_NOCHAIN', + 'PKCS7_NOCERTS', + 'PKCS7_NOATTR', + 'PKCS7_DETACHED', + 'PKCS7_NOSIGS', + ], + ], + ], + + 'openssl_pkcs7_sign' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PKCS7_TEXT', + 'PKCS7_BINARY', + 'PKCS7_NOINTERN', + 'PKCS7_NOVERIFY', + 'PKCS7_NOCHAIN', + 'PKCS7_NOCERTS', + 'PKCS7_NOATTR', + 'PKCS7_DETACHED', + 'PKCS7_NOSIGS', + ], + ], + ], + + 'openssl_pkcs7_encrypt' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PKCS7_TEXT', + 'PKCS7_BINARY', + 'PKCS7_NOINTERN', + 'PKCS7_NOVERIFY', + 'PKCS7_NOCHAIN', + 'PKCS7_NOCERTS', + 'PKCS7_NOATTR', + 'PKCS7_DETACHED', + 'PKCS7_NOSIGS', + ], + ], + 'cipher_algo' => [ + 'type' => 'single', + 'constants' => [ + 'OPENSSL_CIPHER_RC2_40', + 'OPENSSL_CIPHER_RC2_128', + 'OPENSSL_CIPHER_RC2_64', + 'OPENSSL_CIPHER_DES', + 'OPENSSL_CIPHER_3DES', + 'OPENSSL_CIPHER_AES_128_CBC', + 'OPENSSL_CIPHER_AES_192_CBC', + 'OPENSSL_CIPHER_AES_256_CBC', + ], + ], + ], + + // ———————————————————————————————————————————— + // IDN + // ———————————————————————————————————————————— + + 'idn_to_ascii' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'IDNA_DEFAULT', + 'IDNA_ALLOW_UNASSIGNED', + 'IDNA_CHECK_BIDI', + 'IDNA_CHECK_CONTEXTJ', + 'IDNA_NONTRANSITIONAL_TO_ASCII', + 'IDNA_NONTRANSITIONAL_TO_UNICODE', + 'IDNA_USE_STD3_RULES', + ], + ], + 'variant' => [ + 'type' => 'single', + 'constants' => [ + 'INTL_IDNA_VARIANT_UTS46', + 'INTL_IDNA_VARIANT_2003', + ], + ], + ], + + 'idn_to_utf8' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'IDNA_DEFAULT', + 'IDNA_ALLOW_UNASSIGNED', + 'IDNA_CHECK_BIDI', + 'IDNA_CHECK_CONTEXTJ', + 'IDNA_NONTRANSITIONAL_TO_ASCII', + 'IDNA_NONTRANSITIONAL_TO_UNICODE', + 'IDNA_USE_STD3_RULES', + ], + ], + 'variant' => [ + 'type' => 'single', + 'constants' => [ + 'INTL_IDNA_VARIANT_UTS46', + 'INTL_IDNA_VARIANT_2003', + ], + ], + ], + + // ———————————————————————————————————————————— + // String functions + // ———————————————————————————————————————————— + + 'str_pad' => [ + 'pad_type' => [ + 'type' => 'single', + 'constants' => [ + 'STR_PAD_RIGHT', + 'STR_PAD_LEFT', + 'STR_PAD_BOTH', + ], + ], + ], + + // ———————————————————————————————————————————— + // File seeking + // ———————————————————————————————————————————— + + 'fseek' => [ + 'whence' => [ + 'type' => 'single', + 'constants' => [ + 'SEEK_SET', + 'SEEK_CUR', + 'SEEK_END', + ], + ], + ], + + // ———————————————————————————————————————————— + // INI parsing + // ———————————————————————————————————————————— + + 'parse_ini_file' => [ + 'scanner_mode' => [ + 'type' => 'single', + 'constants' => [ + 'INI_SCANNER_NORMAL', + 'INI_SCANNER_RAW', + 'INI_SCANNER_TYPED', + ], + ], + ], + + 'parse_ini_string' => [ + 'scanner_mode' => [ + 'type' => 'single', + 'constants' => [ + 'INI_SCANNER_NORMAL', + 'INI_SCANNER_RAW', + 'INI_SCANNER_TYPED', + ], + ], + ], + + // ———————————————————————————————————————————— + // Message queues + // ———————————————————————————————————————————— + + 'msg_receive' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_IPC_NOWAIT', + 'MSG_EXCEPT', + 'MSG_NOERROR', + ], + ], + ], + + // ———————————————————————————————————————————— + // Locale + // ———————————————————————————————————————————— + + 'setlocale' => [ + 'category' => [ + 'type' => 'single', + 'constants' => [ + 'LC_CTYPE', + 'LC_NUMERIC', + 'LC_TIME', + 'LC_COLLATE', + 'LC_MONETARY', + 'LC_MESSAGES', + 'LC_ALL', + ], + ], + ], + + // ———————————————————————————————————————————— + // libxml (functions) + // ———————————————————————————————————————————— + + 'simplexml_load_file' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + 'simplexml_load_string' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + // ———————————————————————————————————————————— + // mysqli (functions) + // ———————————————————————————————————————————— + + 'mysqli_begin_transaction' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_START_READ_ONLY', + 'MYSQLI_TRANS_START_READ_WRITE', + 'MYSQLI_TRANS_START_WITH_CONSISTENT_SNAPSHOT', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_START_READ_ONLY', 'MYSQLI_TRANS_START_READ_WRITE'], + ], + ], + ], + + 'mysqli_commit' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_COR_AND_CHAIN', + 'MYSQLI_TRANS_COR_AND_NO_CHAIN', + 'MYSQLI_TRANS_COR_RELEASE', + 'MYSQLI_TRANS_COR_NO_RELEASE', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_COR_AND_CHAIN', 'MYSQLI_TRANS_COR_AND_NO_CHAIN'], + ['MYSQLI_TRANS_COR_RELEASE', 'MYSQLI_TRANS_COR_NO_RELEASE'], + ], + ], + ], + + 'mysqli_rollback' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_COR_AND_CHAIN', + 'MYSQLI_TRANS_COR_AND_NO_CHAIN', + 'MYSQLI_TRANS_COR_RELEASE', + 'MYSQLI_TRANS_COR_NO_RELEASE', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_COR_AND_CHAIN', 'MYSQLI_TRANS_COR_AND_NO_CHAIN'], + ['MYSQLI_TRANS_COR_RELEASE', 'MYSQLI_TRANS_COR_NO_RELEASE'], + ], + ], + ], + + // ———————————————————————————————————————————— + // Methods with global constants + // ———————————————————————————————————————————— + + // finfo methods (FILEINFO_* global constants) + + 'finfo::__construct' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + 'finfo::file' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + 'finfo::buffer' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + 'finfo::set_flags' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + // SplFileObject methods (global constants) + + 'SplFileObject::flock' => [ + 'operation' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LOCK_SH', + 'LOCK_EX', + 'LOCK_UN', + 'LOCK_NB', + ], + 'exclusiveGroups' => [ + ['LOCK_SH', 'LOCK_EX', 'LOCK_UN'], + ], + ], + ], + + 'SplFileObject::fseek' => [ + 'whence' => [ + 'type' => 'single', + 'constants' => [ + 'SEEK_SET', + 'SEEK_CUR', + 'SEEK_END', + ], + ], + ], + + // DOMDocument methods (LIBXML_* global constants) + + 'DOMDocument::load' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + 'DOMDocument::loadXML' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + 'DOMDocument::loadHTML' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + 'LIBXML_HTML_NOIMPLIED', + 'LIBXML_HTML_NODEFDTD', + ], + ], + ], + + 'DOMDocument::loadHTMLFile' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + 'LIBXML_HTML_NOIMPLIED', + 'LIBXML_HTML_NODEFDTD', + ], + ], + ], + + 'DOMDocument::save' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOEMPTYTAG', + ], + ], + ], + + 'DOMDocument::saveXML' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOEMPTYTAG', + ], + ], + ], + + 'DOMDocument::schemaValidate' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_SCHEMA_CREATE', + ], + ], + ], + + 'DOMDocument::schemaValidateSource' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_SCHEMA_CREATE', + ], + ], + ], + + // XMLReader methods (LIBXML_* global constants) + + 'XMLReader::open' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + 'XMLReader::XML' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + // mysqli methods (global constants) + + 'mysqli::begin_transaction' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_START_READ_ONLY', + 'MYSQLI_TRANS_START_READ_WRITE', + 'MYSQLI_TRANS_START_WITH_CONSISTENT_SNAPSHOT', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_START_READ_ONLY', 'MYSQLI_TRANS_START_READ_WRITE'], + ], + ], + ], + + 'mysqli::commit' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_COR_AND_CHAIN', + 'MYSQLI_TRANS_COR_AND_NO_CHAIN', + 'MYSQLI_TRANS_COR_RELEASE', + 'MYSQLI_TRANS_COR_NO_RELEASE', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_COR_AND_CHAIN', 'MYSQLI_TRANS_COR_AND_NO_CHAIN'], + ['MYSQLI_TRANS_COR_RELEASE', 'MYSQLI_TRANS_COR_NO_RELEASE'], + ], + ], + ], + + 'mysqli::rollback' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_COR_AND_CHAIN', + 'MYSQLI_TRANS_COR_AND_NO_CHAIN', + 'MYSQLI_TRANS_COR_RELEASE', + 'MYSQLI_TRANS_COR_NO_RELEASE', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_COR_AND_CHAIN', 'MYSQLI_TRANS_COR_AND_NO_CHAIN'], + ['MYSQLI_TRANS_COR_RELEASE', 'MYSQLI_TRANS_COR_NO_RELEASE'], + ], + ], + ], + + // Collator methods (class constants) + + 'Collator::sort' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'Collator::SORT_REGULAR', + 'Collator::SORT_STRING', + 'Collator::SORT_NUMERIC', + ], + ], + ], + + 'Collator::asort' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'Collator::SORT_REGULAR', + 'Collator::SORT_STRING', + 'Collator::SORT_NUMERIC', + ], + ], + ], + + 'Collator::setAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'Collator::FRENCH_COLLATION', + 'Collator::ALTERNATE_HANDLING', + 'Collator::CASE_FIRST', + 'Collator::CASE_LEVEL', + 'Collator::NORMALIZATION_MODE', + 'Collator::STRENGTH', + 'Collator::HIRAGANA_QUATERNARY_MODE', + 'Collator::NUMERIC_COLLATION', + ], + ], + ], + + 'Collator::getAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'Collator::FRENCH_COLLATION', + 'Collator::ALTERNATE_HANDLING', + 'Collator::CASE_FIRST', + 'Collator::CASE_LEVEL', + 'Collator::NORMALIZATION_MODE', + 'Collator::STRENGTH', + 'Collator::HIRAGANA_QUATERNARY_MODE', + 'Collator::NUMERIC_COLLATION', + ], + ], + ], + + // ———————————————————————————————————————————— + // Methods with class constants + // ———————————————————————————————————————————— + + // PDO::setAttribute/getAttribute are excluded because PDO drivers add + // their own attribute constants (PGSQL_ATTR_*, MYSQL_ATTR_*, etc.) + + // PDOStatement + + 'PDOStatement::fetch' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::FETCH_DEFAULT', + 'PDO::FETCH_LAZY', + 'PDO::FETCH_ASSOC', + 'PDO::FETCH_NUM', + 'PDO::FETCH_BOTH', + 'PDO::FETCH_OBJ', + 'PDO::FETCH_BOUND', + 'PDO::FETCH_COLUMN', + 'PDO::FETCH_CLASS', + 'PDO::FETCH_INTO', + 'PDO::FETCH_FUNC', + 'PDO::FETCH_GROUP', + 'PDO::FETCH_UNIQUE', + 'PDO::FETCH_KEY_PAIR', + 'PDO::FETCH_CLASSTYPE', + 'PDO::FETCH_SERIALIZE', + 'PDO::FETCH_PROPS_LATE', + 'PDO::FETCH_NAMED', + ], + ], + 'cursorOrientation' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::FETCH_ORI_NEXT', + 'PDO::FETCH_ORI_PRIOR', + 'PDO::FETCH_ORI_FIRST', + 'PDO::FETCH_ORI_LAST', + 'PDO::FETCH_ORI_ABS', + 'PDO::FETCH_ORI_REL', + ], + ], + ], + + 'PDOStatement::fetchAll' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::FETCH_DEFAULT', + 'PDO::FETCH_LAZY', + 'PDO::FETCH_ASSOC', + 'PDO::FETCH_NUM', + 'PDO::FETCH_BOTH', + 'PDO::FETCH_OBJ', + 'PDO::FETCH_BOUND', + 'PDO::FETCH_COLUMN', + 'PDO::FETCH_CLASS', + 'PDO::FETCH_INTO', + 'PDO::FETCH_FUNC', + 'PDO::FETCH_GROUP', + 'PDO::FETCH_UNIQUE', + 'PDO::FETCH_KEY_PAIR', + 'PDO::FETCH_CLASSTYPE', + 'PDO::FETCH_SERIALIZE', + 'PDO::FETCH_PROPS_LATE', + 'PDO::FETCH_NAMED', + ], + ], + ], + + 'PDOStatement::setFetchMode' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::FETCH_DEFAULT', + 'PDO::FETCH_LAZY', + 'PDO::FETCH_ASSOC', + 'PDO::FETCH_NUM', + 'PDO::FETCH_BOTH', + 'PDO::FETCH_OBJ', + 'PDO::FETCH_BOUND', + 'PDO::FETCH_COLUMN', + 'PDO::FETCH_CLASS', + 'PDO::FETCH_INTO', + 'PDO::FETCH_FUNC', + 'PDO::FETCH_GROUP', + 'PDO::FETCH_UNIQUE', + 'PDO::FETCH_KEY_PAIR', + 'PDO::FETCH_CLASSTYPE', + 'PDO::FETCH_SERIALIZE', + 'PDO::FETCH_PROPS_LATE', + 'PDO::FETCH_NAMED', + ], + ], + ], + + 'PDOStatement::bindColumn' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::PARAM_NULL', + 'PDO::PARAM_BOOL', + 'PDO::PARAM_INT', + 'PDO::PARAM_STR', + 'PDO::PARAM_LOB', + 'PDO::PARAM_STMT', + 'PDO::PARAM_INPUT_OUTPUT', + 'PDO::PARAM_STR_NATL', + 'PDO::PARAM_STR_CHAR', + ], + ], + ], + + 'PDOStatement::bindParam' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::PARAM_NULL', + 'PDO::PARAM_BOOL', + 'PDO::PARAM_INT', + 'PDO::PARAM_STR', + 'PDO::PARAM_LOB', + 'PDO::PARAM_STMT', + 'PDO::PARAM_INPUT_OUTPUT', + 'PDO::PARAM_STR_NATL', + 'PDO::PARAM_STR_CHAR', + ], + ], + ], + + 'PDOStatement::bindValue' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::PARAM_NULL', + 'PDO::PARAM_BOOL', + 'PDO::PARAM_INT', + 'PDO::PARAM_STR', + 'PDO::PARAM_LOB', + 'PDO::PARAM_STMT', + 'PDO::PARAM_INPUT_OUTPUT', + 'PDO::PARAM_STR_NATL', + 'PDO::PARAM_STR_CHAR', + ], + ], + ], + + // ZipArchive + + 'ZipArchive::open' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ZipArchive::CREATE', + 'ZipArchive::EXCL', + 'ZipArchive::CHECKCONS', + 'ZipArchive::OVERWRITE', + 'ZipArchive::RDONLY', + ], + ], + ], + + 'ZipArchive::setCompressionName' => [ + 'method' => [ + 'type' => 'single', + 'constants' => [ + 'ZipArchive::CM_DEFAULT', + 'ZipArchive::CM_STORE', + 'ZipArchive::CM_SHRINK', + 'ZipArchive::CM_REDUCE_1', + 'ZipArchive::CM_REDUCE_2', + 'ZipArchive::CM_REDUCE_3', + 'ZipArchive::CM_REDUCE_4', + 'ZipArchive::CM_IMPLODE', + 'ZipArchive::CM_DEFLATE', + 'ZipArchive::CM_DEFLATE64', + 'ZipArchive::CM_PKWARE_IMPLODE', + 'ZipArchive::CM_BZIP2', + 'ZipArchive::CM_LZMA', + 'ZipArchive::CM_LZMA2', + 'ZipArchive::CM_ZSTD', + 'ZipArchive::CM_XZ', + ], + ], + ], + + 'ZipArchive::setCompressionIndex' => [ + 'method' => [ + 'type' => 'single', + 'constants' => [ + 'ZipArchive::CM_DEFAULT', + 'ZipArchive::CM_STORE', + 'ZipArchive::CM_SHRINK', + 'ZipArchive::CM_REDUCE_1', + 'ZipArchive::CM_REDUCE_2', + 'ZipArchive::CM_REDUCE_3', + 'ZipArchive::CM_REDUCE_4', + 'ZipArchive::CM_IMPLODE', + 'ZipArchive::CM_DEFLATE', + 'ZipArchive::CM_DEFLATE64', + 'ZipArchive::CM_PKWARE_IMPLODE', + 'ZipArchive::CM_BZIP2', + 'ZipArchive::CM_LZMA', + 'ZipArchive::CM_LZMA2', + 'ZipArchive::CM_ZSTD', + 'ZipArchive::CM_XZ', + ], + ], + ], + + 'ZipArchive::setEncryptionName' => [ + 'method' => [ + 'type' => 'single', + 'constants' => [ + 'ZipArchive::EM_NONE', + 'ZipArchive::EM_TRAD_PKWARE', + 'ZipArchive::EM_AES_128', + 'ZipArchive::EM_AES_192', + 'ZipArchive::EM_AES_256', + ], + ], + ], + + 'ZipArchive::setEncryptionIndex' => [ + 'method' => [ + 'type' => 'single', + 'constants' => [ + 'ZipArchive::EM_NONE', + 'ZipArchive::EM_TRAD_PKWARE', + 'ZipArchive::EM_AES_128', + 'ZipArchive::EM_AES_192', + 'ZipArchive::EM_AES_256', + ], + ], + ], + + // IntlDateFormatter + + 'IntlDateFormatter::__construct' => [ + 'dateType' => [ + 'type' => 'single', + 'constants' => [ + 'IntlDateFormatter::FULL', + 'IntlDateFormatter::LONG', + 'IntlDateFormatter::MEDIUM', + 'IntlDateFormatter::SHORT', + 'IntlDateFormatter::NONE', + 'IntlDateFormatter::RELATIVE_FULL', + 'IntlDateFormatter::RELATIVE_LONG', + 'IntlDateFormatter::RELATIVE_MEDIUM', + 'IntlDateFormatter::RELATIVE_SHORT', + ], + ], + 'timeType' => [ + 'type' => 'single', + 'constants' => [ + 'IntlDateFormatter::FULL', + 'IntlDateFormatter::LONG', + 'IntlDateFormatter::MEDIUM', + 'IntlDateFormatter::SHORT', + 'IntlDateFormatter::NONE', + 'IntlDateFormatter::RELATIVE_FULL', + 'IntlDateFormatter::RELATIVE_LONG', + 'IntlDateFormatter::RELATIVE_MEDIUM', + 'IntlDateFormatter::RELATIVE_SHORT', + ], + ], + ], + + 'IntlDateFormatter::create' => [ + 'dateType' => [ + 'type' => 'single', + 'constants' => [ + 'IntlDateFormatter::FULL', + 'IntlDateFormatter::LONG', + 'IntlDateFormatter::MEDIUM', + 'IntlDateFormatter::SHORT', + 'IntlDateFormatter::NONE', + 'IntlDateFormatter::RELATIVE_FULL', + 'IntlDateFormatter::RELATIVE_LONG', + 'IntlDateFormatter::RELATIVE_MEDIUM', + 'IntlDateFormatter::RELATIVE_SHORT', + ], + ], + 'timeType' => [ + 'type' => 'single', + 'constants' => [ + 'IntlDateFormatter::FULL', + 'IntlDateFormatter::LONG', + 'IntlDateFormatter::MEDIUM', + 'IntlDateFormatter::SHORT', + 'IntlDateFormatter::NONE', + 'IntlDateFormatter::RELATIVE_FULL', + 'IntlDateFormatter::RELATIVE_LONG', + 'IntlDateFormatter::RELATIVE_MEDIUM', + 'IntlDateFormatter::RELATIVE_SHORT', + ], + ], + ], + + // NumberFormatter + + 'NumberFormatter::__construct' => [ + 'style' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::PATTERN_DECIMAL', + 'NumberFormatter::DECIMAL', + 'NumberFormatter::CURRENCY', + 'NumberFormatter::PERCENT', + 'NumberFormatter::SCIENTIFIC', + 'NumberFormatter::SPELLOUT', + 'NumberFormatter::ORDINAL', + 'NumberFormatter::DURATION', + 'NumberFormatter::PATTERN_RULEBASED', + 'NumberFormatter::IGNORE', + 'NumberFormatter::CURRENCY_ACCOUNTING', + 'NumberFormatter::DEFAULT_STYLE', + ], + ], + ], + + 'NumberFormatter::create' => [ + 'style' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::PATTERN_DECIMAL', + 'NumberFormatter::DECIMAL', + 'NumberFormatter::CURRENCY', + 'NumberFormatter::PERCENT', + 'NumberFormatter::SCIENTIFIC', + 'NumberFormatter::SPELLOUT', + 'NumberFormatter::ORDINAL', + 'NumberFormatter::DURATION', + 'NumberFormatter::PATTERN_RULEBASED', + 'NumberFormatter::IGNORE', + 'NumberFormatter::CURRENCY_ACCOUNTING', + 'NumberFormatter::DEFAULT_STYLE', + ], + ], + ], + + 'NumberFormatter::format' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::TYPE_DEFAULT', + 'NumberFormatter::TYPE_INT32', + 'NumberFormatter::TYPE_INT64', + 'NumberFormatter::TYPE_DOUBLE', + 'NumberFormatter::TYPE_CURRENCY', + ], + ], + ], + + 'NumberFormatter::setAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::PARSE_INT_ONLY', + 'NumberFormatter::GROUPING_USED', + 'NumberFormatter::DECIMAL_ALWAYS_SHOWN', + 'NumberFormatter::MAX_INTEGER_DIGITS', + 'NumberFormatter::MIN_INTEGER_DIGITS', + 'NumberFormatter::INTEGER_DIGITS', + 'NumberFormatter::MAX_FRACTION_DIGITS', + 'NumberFormatter::MIN_FRACTION_DIGITS', + 'NumberFormatter::FRACTION_DIGITS', + 'NumberFormatter::MULTIPLIER', + 'NumberFormatter::GROUPING_SIZE', + 'NumberFormatter::ROUNDING_MODE', + 'NumberFormatter::ROUNDING_INCREMENT', + 'NumberFormatter::FORMAT_WIDTH', + 'NumberFormatter::PADDING_POSITION', + 'NumberFormatter::SECONDARY_GROUPING_SIZE', + 'NumberFormatter::SIGNIFICANT_DIGITS_USED', + 'NumberFormatter::MIN_SIGNIFICANT_DIGITS', + 'NumberFormatter::MAX_SIGNIFICANT_DIGITS', + 'NumberFormatter::LENIENT_PARSE', + ], + ], + ], + + 'NumberFormatter::getAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::PARSE_INT_ONLY', + 'NumberFormatter::GROUPING_USED', + 'NumberFormatter::DECIMAL_ALWAYS_SHOWN', + 'NumberFormatter::MAX_INTEGER_DIGITS', + 'NumberFormatter::MIN_INTEGER_DIGITS', + 'NumberFormatter::INTEGER_DIGITS', + 'NumberFormatter::MAX_FRACTION_DIGITS', + 'NumberFormatter::MIN_FRACTION_DIGITS', + 'NumberFormatter::FRACTION_DIGITS', + 'NumberFormatter::MULTIPLIER', + 'NumberFormatter::GROUPING_SIZE', + 'NumberFormatter::ROUNDING_MODE', + 'NumberFormatter::ROUNDING_INCREMENT', + 'NumberFormatter::FORMAT_WIDTH', + 'NumberFormatter::PADDING_POSITION', + 'NumberFormatter::SECONDARY_GROUPING_SIZE', + 'NumberFormatter::SIGNIFICANT_DIGITS_USED', + 'NumberFormatter::MIN_SIGNIFICANT_DIGITS', + 'NumberFormatter::MAX_SIGNIFICANT_DIGITS', + 'NumberFormatter::LENIENT_PARSE', + ], + ], + ], + + 'NumberFormatter::setTextAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::POSITIVE_PREFIX', + 'NumberFormatter::POSITIVE_SUFFIX', + 'NumberFormatter::NEGATIVE_PREFIX', + 'NumberFormatter::NEGATIVE_SUFFIX', + 'NumberFormatter::PADDING_CHARACTER', + 'NumberFormatter::CURRENCY_CODE', + 'NumberFormatter::DEFAULT_RULESET', + 'NumberFormatter::PUBLIC_RULESETS', + ], + ], + ], + + 'NumberFormatter::getTextAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::POSITIVE_PREFIX', + 'NumberFormatter::POSITIVE_SUFFIX', + 'NumberFormatter::NEGATIVE_PREFIX', + 'NumberFormatter::NEGATIVE_SUFFIX', + 'NumberFormatter::PADDING_CHARACTER', + 'NumberFormatter::CURRENCY_CODE', + 'NumberFormatter::DEFAULT_RULESET', + 'NumberFormatter::PUBLIC_RULESETS', + ], + ], + ], + + // SplPriorityQueue + + 'SplPriorityQueue::setExtractFlags' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'SplPriorityQueue::EXTR_BOTH', + 'SplPriorityQueue::EXTR_PRIORITY', + 'SplPriorityQueue::EXTR_DATA', + ], + ], + ], + + // FilesystemIterator / GlobIterator / RecursiveDirectoryIterator + + 'FilesystemIterator::__construct' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FilesystemIterator::CURRENT_AS_PATHNAME', + 'FilesystemIterator::CURRENT_AS_FILEINFO', + 'FilesystemIterator::CURRENT_AS_SELF', + 'FilesystemIterator::KEY_AS_PATHNAME', + 'FilesystemIterator::KEY_AS_FILENAME', + 'FilesystemIterator::FOLLOW_SYMLINKS', + 'FilesystemIterator::NEW_CURRENT_AND_KEY', + 'FilesystemIterator::SKIP_DOTS', + 'FilesystemIterator::UNIX_PATHS', + ], + 'exclusiveGroups' => [ + ['FilesystemIterator::CURRENT_AS_PATHNAME', 'FilesystemIterator::CURRENT_AS_FILEINFO', 'FilesystemIterator::CURRENT_AS_SELF'], + ['FilesystemIterator::KEY_AS_PATHNAME', 'FilesystemIterator::KEY_AS_FILENAME'], + ], + ], + ], + + 'FilesystemIterator::setFlags' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FilesystemIterator::CURRENT_AS_PATHNAME', + 'FilesystemIterator::CURRENT_AS_FILEINFO', + 'FilesystemIterator::CURRENT_AS_SELF', + 'FilesystemIterator::KEY_AS_PATHNAME', + 'FilesystemIterator::KEY_AS_FILENAME', + 'FilesystemIterator::FOLLOW_SYMLINKS', + 'FilesystemIterator::NEW_CURRENT_AND_KEY', + 'FilesystemIterator::SKIP_DOTS', + 'FilesystemIterator::UNIX_PATHS', + ], + 'exclusiveGroups' => [ + ['FilesystemIterator::CURRENT_AS_PATHNAME', 'FilesystemIterator::CURRENT_AS_FILEINFO', 'FilesystemIterator::CURRENT_AS_SELF'], + ['FilesystemIterator::KEY_AS_PATHNAME', 'FilesystemIterator::KEY_AS_FILENAME'], + ], + ], + ], + + 'GlobIterator::__construct' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FilesystemIterator::CURRENT_AS_PATHNAME', + 'FilesystemIterator::CURRENT_AS_FILEINFO', + 'FilesystemIterator::CURRENT_AS_SELF', + 'FilesystemIterator::KEY_AS_PATHNAME', + 'FilesystemIterator::KEY_AS_FILENAME', + 'FilesystemIterator::FOLLOW_SYMLINKS', + 'FilesystemIterator::NEW_CURRENT_AND_KEY', + 'FilesystemIterator::SKIP_DOTS', + 'FilesystemIterator::UNIX_PATHS', + ], + 'exclusiveGroups' => [ + ['FilesystemIterator::CURRENT_AS_PATHNAME', 'FilesystemIterator::CURRENT_AS_FILEINFO', 'FilesystemIterator::CURRENT_AS_SELF'], + ['FilesystemIterator::KEY_AS_PATHNAME', 'FilesystemIterator::KEY_AS_FILENAME'], + ], + ], + ], + + 'RecursiveDirectoryIterator::__construct' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FilesystemIterator::CURRENT_AS_PATHNAME', + 'FilesystemIterator::CURRENT_AS_FILEINFO', + 'FilesystemIterator::CURRENT_AS_SELF', + 'FilesystemIterator::KEY_AS_PATHNAME', + 'FilesystemIterator::KEY_AS_FILENAME', + 'FilesystemIterator::FOLLOW_SYMLINKS', + 'FilesystemIterator::NEW_CURRENT_AND_KEY', + 'FilesystemIterator::SKIP_DOTS', + 'FilesystemIterator::UNIX_PATHS', + ], + 'exclusiveGroups' => [ + ['FilesystemIterator::CURRENT_AS_PATHNAME', 'FilesystemIterator::CURRENT_AS_FILEINFO', 'FilesystemIterator::CURRENT_AS_SELF'], + ['FilesystemIterator::KEY_AS_PATHNAME', 'FilesystemIterator::KEY_AS_FILENAME'], + ], + ], + ], +]; diff --git a/src/Reflection/AllowedConstantsResult.php b/src/Reflection/AllowedConstantsResult.php new file mode 100644 index 00000000000..8dd9f315a4d --- /dev/null +++ b/src/Reflection/AllowedConstantsResult.php @@ -0,0 +1,55 @@ + $disallowedConstants + * @param list> $violatedExclusiveGroups + */ + public function __construct( + private array $disallowedConstants, + private array $violatedExclusiveGroups, + private bool $bitmaskNotAllowed, + ) + { + } + + public function isOk(): bool + { + return $this->disallowedConstants === [] && $this->violatedExclusiveGroups === [] && !$this->bitmaskNotAllowed; + } + + public function isBitmaskNotAllowed(): bool + { + return $this->bitmaskNotAllowed; + } + + /** + * @return list + */ + public function getDisallowedConstants(): array + { + return $this->disallowedConstants; + } + + /** + * @return list> + */ + public function getViolatedExclusiveGroups(): array + { + return $this->violatedExclusiveGroups; + } + +} diff --git a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php index b01a6db6ff8..93941cf0698 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -2,7 +2,9 @@ namespace PHPStan\Reflection\Annotations; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -80,4 +82,14 @@ public function getAttributes(): array return []; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return null; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + return new AllowedConstantsResult([], [], false); + } + } diff --git a/src/Reflection/ExtendedParameterReflection.php b/src/Reflection/ExtendedParameterReflection.php index 890b0493469..1ccd1d4b935 100644 --- a/src/Reflection/ExtendedParameterReflection.php +++ b/src/Reflection/ExtendedParameterReflection.php @@ -29,4 +29,11 @@ public function getClosureThisType(): ?Type; */ public function getAttributes(): array; + public function getAllowedConstants(): ?ParameterAllowedConstants; + + /** + * @param list $constants Global and/or class constant reflections + */ + public function checkAllowedConstants(array $constants): AllowedConstantsResult; + } diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index d9b75bf3e0e..9698b42ea8f 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -105,6 +105,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc TrinaryLogic::createMaybe(), null, [], + null, ), $parameters), $parametersAcceptor->isVariadic(), $returnType, diff --git a/src/Reflection/Native/ExtendedNativeParameterReflection.php b/src/Reflection/Native/ExtendedNativeParameterReflection.php index 00e2ea1a99e..5539d9132a1 100644 --- a/src/Reflection/Native/ExtendedNativeParameterReflection.php +++ b/src/Reflection/Native/ExtendedNativeParameterReflection.php @@ -2,8 +2,10 @@ namespace PHPStan\Reflection\Native; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -28,6 +30,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private ?ParameterAllowedConstants $allowedConstants, ) { } @@ -97,4 +100,18 @@ public function getAttributes(): array return $this->attributes; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return $this->allowedConstants; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + if ($this->allowedConstants === null) { + return new AllowedConstantsResult([], [], false); + } + + return $this->allowedConstants->check($constants); + } + } diff --git a/src/Reflection/ParameterAllowedConstants.php b/src/Reflection/ParameterAllowedConstants.php new file mode 100644 index 00000000000..2f844e28a82 --- /dev/null +++ b/src/Reflection/ParameterAllowedConstants.php @@ -0,0 +1,103 @@ + $constants + * @param list> $exclusiveGroups + */ + public function __construct( + private string $type, + private array $constants, + private array $exclusiveGroups, + ) + { + } + + public function isBitmask(): bool + { + return $this->type === 'bitmask'; + } + + /** + * @return list> + */ + public function getExclusiveGroups(): array + { + return $this->exclusiveGroups; + } + + private function resolveConstantName(ConstantReflection $constant): string + { + if ($constant instanceof ClassConstantReflection) { + return $constant->getDeclaringClass()->getName() . '::' . $constant->getName(); + } + + return $constant->getName(); + } + + /** + * @param list $constants + */ + public function check(array $constants): AllowedConstantsResult + { + $bitmaskNotAllowed = !$this->isBitmask() && count($constants) > 1; + + $disallowed = []; + $names = []; + + foreach ($constants as $constant) { + $name = $this->resolveConstantName($constant); + $names[] = $name; + + if (in_array($name, $this->constants, true)) { + continue; + } + + $disallowed[] = $constant; + } + + $violated = []; + if ($this->isBitmask()) { + foreach ($this->exclusiveGroups as $group) { + $matched = []; + foreach ($names as $name) { + if (!in_array($name, $group, true)) { + continue; + } + + $matched[] = $name; + } + + if (count($matched) < 2) { + continue; + } + + $violated[] = $matched; + } + } + + return new AllowedConstantsResult($disallowed, $violated, $bitmaskNotAllowed); + } + +} diff --git a/src/Reflection/ParameterAllowedConstantsMapProvider.php b/src/Reflection/ParameterAllowedConstantsMapProvider.php new file mode 100644 index 00000000000..22c0a9fd31e --- /dev/null +++ b/src/Reflection/ParameterAllowedConstantsMapProvider.php @@ -0,0 +1,50 @@ +, exclusiveGroups?: list>}>>|null */ + private ?array $map = null; + + public function getForFunctionParameter(string $functionName, string $parameterName): ?ParameterAllowedConstants + { + return $this->get($functionName, $parameterName); + } + + public function getForMethodParameter(string $className, string $methodName, string $parameterName): ?ParameterAllowedConstants + { + return $this->get($className . '::' . $methodName, $parameterName); + } + + private function get(string $key, string $parameterName): ?ParameterAllowedConstants + { + $map = $this->getMap(); + + if (!isset($map[$key][$parameterName])) { + return null; + } + + /** @var array{type: 'single'|'bitmask', constants: list, exclusiveGroups?: list>} $config */ + $config = $map[$key][$parameterName]; + + return new ParameterAllowedConstants( + $config['type'], + $config['constants'], + $config['exclusiveGroups'] ?? [], + ); + } + + /** + * @return array, exclusiveGroups?: list>}>> + */ + private function getMap(): array + { + return $this->map ??= require __DIR__ . '/../../resources/constantToFunctionParameterMap.php'; + } + +} diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index b4c9b3a3821..608f3fb93d1 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -771,6 +771,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $parameter instanceof ExtendedParameterReflection ? $parameter->isImmediatelyInvokedCallable() : TrinaryLogic::createMaybe(), $parameter instanceof ExtendedParameterReflection ? $parameter->getClosureThisType() : null, $parameter instanceof ExtendedParameterReflection ? $parameter->getAttributes() : [], + $parameter instanceof ExtendedParameterReflection ? $parameter->getAllowedConstants() : null, ); continue; } @@ -830,6 +831,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $immediatelyInvokedCallable, $closureThisType, $attributes, + null, ); if ($isVariadic) { @@ -928,6 +930,7 @@ private static function wrapParameter(ParameterReflection $parameter): ExtendedP TrinaryLogic::createMaybe(), null, [], + null, ); } diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index b15ec9401b9..41c278f08dd 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -98,6 +98,7 @@ public function getVariants(): array TrinaryLogic::createMaybe(), null, [], + null, ), $parameters), $this->closureType->isVariadic(), $this->closureType->getReturnType(), diff --git a/src/Reflection/Php/ExitFunctionReflection.php b/src/Reflection/Php/ExitFunctionReflection.php index c4ab5219df2..79c3f6034c8 100644 --- a/src/Reflection/Php/ExitFunctionReflection.php +++ b/src/Reflection/Php/ExitFunctionReflection.php @@ -59,6 +59,7 @@ public function getVariants(): array TrinaryLogic::createNo(), null, [], + null, ), ], false, diff --git a/src/Reflection/Php/ExtendedDummyParameter.php b/src/Reflection/Php/ExtendedDummyParameter.php index 19a917e0a17..69a19ccbf3a 100644 --- a/src/Reflection/Php/ExtendedDummyParameter.php +++ b/src/Reflection/Php/ExtendedDummyParameter.php @@ -2,8 +2,10 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -28,6 +30,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private ?ParameterAllowedConstants $allowedConstants, ) { parent::__construct($name, $type, $optional, $passedByReference, $variadic, $defaultValue); @@ -68,4 +71,18 @@ public function getAttributes(): array return $this->attributes; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return $this->allowedConstants; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + if ($this->allowedConstants === null) { + return new AllowedConstantsResult([], [], false); + } + + return $this->allowedConstants->check($constants); + } + } diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index e471943e87c..7dd05c682c6 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -31,6 +31,7 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\ExtendedNativeParameterReflection; use PHPStan\Reflection\Native\NativeMethodReflection; +use PHPStan\Reflection\ParameterAllowedConstantsMapProvider; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\SignatureMap\FunctionSignature; use PHPStan\Reflection\SignatureMap\ParameterSignature; @@ -100,6 +101,7 @@ public function __construct( private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, private FileTypeMapper $fileTypeMapper, private AttributeReflectionFactory $attributeReflectionFactory, + private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, private bool $inferPrivatePropertyTypeFromConstructor, ) { @@ -723,7 +725,7 @@ private function createMethod( } } } - $variantsByType[$signatureType][] = $this->createNativeMethodVariant($methodSignature, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $phpDocParameterOutTypes, $immediatelyInvokedCallableParameters, $closureThisParameters, $phpDocFromStubs, $signatureType !== 'named'); + $variantsByType[$signatureType][] = $this->createNativeMethodVariant($declaringClassName, $methodReflection->getName(), $methodSignature, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $phpDocParameterOutTypes, $immediatelyInvokedCallableParameters, $closureThisParameters, $phpDocFromStubs, $signatureType !== 'named'); } } @@ -971,6 +973,8 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla * @param array $closureThisParameters */ private function createNativeMethodVariant( + string $declaringClassName, + string $methodName, FunctionSignature $methodSignature, array $phpDocParameterTypes, ?Type $phpDocReturnType, @@ -1025,6 +1029,7 @@ private function createNativeMethodVariant( $immediatelyInvoked, $closureThisType, [], + $this->allowedConstantsMapProvider->getForMethodParameter($declaringClassName, $methodName, $parameterSignature->getName()), ); } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 7dbd7ca3c47..2dcb0c7b870 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -16,6 +16,7 @@ use PHPStan\Reflection\FunctionReflectionFactory; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\ParameterAllowedConstantsMapProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; @@ -44,6 +45,7 @@ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ReflectionFunction $reflection, private AttributeReflectionFactory $attributeReflectionFactory, + private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, private TemplateTypeMap $templateTypeMap, private array $phpDocParameterTypes, private ?Type $phpDocReturnType, @@ -127,6 +129,7 @@ private function getParameters(): array $immediatelyInvokedCallable, $this->phpDocParameterClosureThisTypes[$reflection->getName()] ?? null, $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), + $this->allowedConstantsMapProvider->getForFunctionParameter(strtolower($this->reflection->getName()), $reflection->getName()), ); }, $this->reflection->getParameters()); } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 34f28635007..d24532340d4 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -19,6 +19,7 @@ use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodPrototypeReflection; +use PHPStan\Reflection\ParameterAllowedConstantsMapProvider; use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; @@ -70,6 +71,7 @@ public function __construct( private ReflectionMethod $reflection, private ReflectionProvider $reflectionProvider, private AttributeReflectionFactory $attributeReflectionFactory, + private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, private TemplateTypeMap $templateTypeMap, private array $phpDocParameterTypes, private ?Type $phpDocReturnType, @@ -226,6 +228,7 @@ private function getParameters(): array $this->immediatelyInvokedCallableParameters[$reflection->getName()] ?? TrinaryLogic::createMaybe(), $this->phpDocClosureThisTypeParameters[$reflection->getName()] ?? null, $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), + $this->allowedConstantsMapProvider->getForMethodParameter($this->declaringClass->getName(), $this->reflection->getName(), $reflection->getName()), ), $this->reflection->getParameters()); } @@ -411,6 +414,7 @@ public function changePropertyGetHookPhpDocType(Type $phpDocType): self $this->reflection, $this->reflectionProvider, $this->attributeReflectionFactory, + $this->allowedConstantsMapProvider, $this->templateTypeMap, $this->phpDocParameterTypes, $phpDocType, @@ -444,6 +448,7 @@ public function changePropertySetHookPhpDocType(string $parameterName, Type $php $this->reflection, $this->reflectionProvider, $this->attributeReflectionFactory, + $this->allowedConstantsMapProvider, $this->templateTypeMap, $phpDocParameterTypes, $this->phpDocReturnType, diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index f048ea71006..7061d7f63e9 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -2,8 +2,10 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -113,4 +115,14 @@ public function getAttributes(): array return $this->attributes; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return null; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + return new AllowedConstantsResult([], [], false); + } + } diff --git a/src/Reflection/Php/PhpParameterReflection.php b/src/Reflection/Php/PhpParameterReflection.php index 8469f7bef44..17b55295159 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -3,11 +3,13 @@ namespace PHPStan\Reflection\Php; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -34,6 +36,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private ?ParameterAllowedConstants $allowedConstants, ) { } @@ -143,4 +146,18 @@ public function getAttributes(): array return $this->attributes; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return $this->allowedConstants; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + if ($this->allowedConstants === null) { + return new AllowedConstantsResult([], [], false); + } + + return $this->allowedConstants->check($constants); + } + } diff --git a/src/Reflection/ResolvedFunctionVariantWithOriginal.php b/src/Reflection/ResolvedFunctionVariantWithOriginal.php index 21108d658ef..cd57c8db9d0 100644 --- a/src/Reflection/ResolvedFunctionVariantWithOriginal.php +++ b/src/Reflection/ResolvedFunctionVariantWithOriginal.php @@ -121,6 +121,7 @@ function (ExtendedParameterReflection $param): ExtendedParameterReflection { $param->isImmediatelyInvokedCallable(), $closureThisType, $param->getAttributes(), + $param->getAllowedConstants(), ); }, $this->parametersAcceptor->getParameters(), diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index a2f12f6daad..efb3b508db3 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -16,6 +16,7 @@ use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\Native\ExtendedNativeParameterReflection; use PHPStan\Reflection\Native\NativeFunctionReflection; +use PHPStan\Reflection\ParameterAllowedConstantsMapProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeMap; @@ -41,6 +42,7 @@ public function __construct( private FileTypeMapper $fileTypeMapper, private StubPhpDocProvider $stubPhpDocProvider, private AttributeReflectionFactory $attributeReflectionFactory, + private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, ) { } @@ -107,13 +109,14 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $acceptsNamedArguments = $phpDoc->acceptsNamedArguments(); } + $allowedConstantsMapProvider = $this->allowedConstantsMapProvider; $variantsByType = ['positional' => []]; foreach ($functionSignaturesResult as $signatureType => $functionSignatures) { foreach ($functionSignatures ?? [] as $functionSignature) { $variantsByType[$signatureType][] = new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, - array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc): ExtendedNativeParameterReflection { + array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc, $lowerCasedFunctionName, $allowedConstantsMapProvider): ExtendedNativeParameterReflection { $type = $parameterSignature->getType(); $phpDocType = null; @@ -144,6 +147,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $immediatelyInvokedCallable, $closureThisType, [], + $allowedConstantsMapProvider->getForFunctionParameter($lowerCasedFunctionName, $parameterSignature->getName()), ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index 1f183844d53..026d3c36fb2 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -118,6 +118,7 @@ function (ExtendedParameterReflection $parameter): ExtendedParameterReflection { $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, $parameter->getAttributes(), + $parameter->getAllowedConstants(), ); }, $acceptor->getParameters(), diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index 68d3200bec0..8198ea1f954 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -105,6 +105,7 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, $parameter->getAttributes(), + $parameter->getAllowedConstants(), ), $acceptor->getParameters(), ), diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index d028d80d04b..7711a799580 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -77,6 +77,7 @@ public function getVariants(): array TrinaryLogic::createMaybe(), null, [], + null, ), $variant->getParameters()), $variant->isVariadic(), $variant->getReturnType(), diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 36b3c6faa61..3d8d8edf038 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -157,6 +157,9 @@ public function check( 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', '%s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.', 'Attribute class ' . $attributeClassName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of attribute class ' . $attributeClassName . ' constructor.', + 'Constants %s cannot be combined for %s of attribute class ' . $attributeClassName . ' constructor.', + 'Combining constants with | is not allowed for %s of attribute class ' . $attributeClassName . ' constructor.', ); foreach ($parameterErrors as $error) { diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index e92e18a03d4..fc4d2e13fc5 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -269,6 +269,9 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ 'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.', '%s of class ' . $classDisplayName . ' constructor contains unresolvable type.', 'Class ' . $classDisplayName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of class ' . $classDisplayName . ' constructor.', + 'Constants %s cannot be combined for %s of class ' . $classDisplayName . ' constructor.', + 'Combining constants with | is not allowed for %s of class ' . $classDisplayName . ' constructor.', )); } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 16c0938b069..a0dea2a1e0e 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -8,6 +8,7 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; @@ -36,6 +37,7 @@ use function in_array; use function is_int; use function is_string; +use function lcfirst; use function max; use function sprintf; @@ -87,6 +89,9 @@ public function check( string $unresolvableReturnTypeMessage, string $unresolvableParameterTypeMessage, string $namedArgumentMessage, + string $invalidConstantMessage, + string $exclusiveConstantsMessage, + string $bitmaskNotAllowedMessage, ): array { if ($funcCall instanceof Node\Expr\MethodCall || $funcCall instanceof Node\Expr\StaticCall || $funcCall instanceof Node\Expr\FuncCall) { @@ -407,6 +412,46 @@ public function check( ->line($argumentLine) ->build(); } + + if ( + $parameter instanceof ExtendedParameterReflection + && $parameter->getAllowedConstants() !== null + && $scope->getPhpVersion()->supportsNamedArguments()->yes() + ) { + $constantReflections = $this->resolveConstantReflections($argumentValue, $scope); + if ($constantReflections !== null) { + $result = $parameter->checkAllowedConstants($constantReflections); + foreach ($result->getDisallowedConstants() as $disallowedConstant) { + $errors[] = RuleErrorBuilder::message(sprintf( + $invalidConstantMessage, + $disallowedConstant->getName(), + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.invalidConstant') + ->line($argumentLine) + ->build(); + } + foreach ($result->getViolatedExclusiveGroups() as $group) { + $errors[] = RuleErrorBuilder::message(sprintf( + $exclusiveConstantsMessage, + implode(', ', $group), + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.exclusiveConstants') + ->line($argumentLine) + ->build(); + } + if ($result->isBitmaskNotAllowed()) { + $errors[] = RuleErrorBuilder::message(sprintf( + $bitmaskNotAllowedMessage, + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.bitmaskNotAllowed') + ->line($argumentLine) + ->build(); + } + } + } } if ( @@ -702,6 +747,53 @@ private function describeParameter(ParameterReflection $parameter, int|string|nu return implode(' ', $parts); } + /** + * @return list|null Null when the expression is not a constant or bitmask of constants + */ + private function resolveConstantReflections(Expr $expr, Scope $scope): ?array + { + if ($expr instanceof Expr\ConstFetch) { + if (!$this->reflectionProvider->hasConstant($expr->name, $scope)) { + return null; + } + + return [$this->reflectionProvider->getConstant($expr->name, $scope)]; + } + + if ($expr instanceof Expr\ClassConstFetch) { + if (!$expr->class instanceof Node\Name) { + return null; + } + if (!$expr->name instanceof Node\Identifier) { + return null; + } + + $className = $scope->resolveName($expr->class); + if (!$this->reflectionProvider->hasClass($className)) { + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if (!$classReflection->hasConstant($expr->name->name)) { + return null; + } + + return [$classReflection->getConstant($expr->name->name)]; + } + + if ($expr instanceof Expr\BinaryOp\BitwiseOr) { + $left = $this->resolveConstantReflections($expr->left, $scope); + $right = $this->resolveConstantReflections($expr->right, $scope); + if ($left === null || $right === null) { + return null; + } + + return [...$left, ...$right]; + } + + return null; + } + private function callReturnsByReference(Expr $expr, Scope $scope): bool { if ($expr instanceof Node\Expr\MethodCall) { diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index 4ab0af4b014..342498bacd6 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -139,6 +139,9 @@ public function processNode( 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', '%s of ' . $callableDescription . ' contains unresolvable type.', ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of ' . $callableDescription . '.', + 'Constants %s cannot be combined for %s of ' . $callableDescription . '.', + 'Combining constants with | is not allowed for %s of ' . $callableDescription . '.', ), ); } diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 39f6f7cfeac..515ea46fed8 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -68,6 +68,9 @@ public function processNode(Node $node, Scope $scope): array 'Return type of call to function ' . $functionName . ' contains unresolvable type.', '%s of function ' . $functionName . ' contains unresolvable type.', 'Function ' . $functionName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of function ' . $functionName . '.', + 'Constants %s cannot be combined for %s of function ' . $functionName . '.', + 'Combining constants with | is not allowed for %s of function ' . $functionName . '.', ); } diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php index 3dae092b529..da834fa226e 100644 --- a/src/Rules/Functions/CallUserFuncRule.php +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -84,6 +84,9 @@ public function processNode(Node $node, Scope $scope): array 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', '%s of ' . $callableDescription . ' contains unresolvable type.', ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of ' . $callableDescription . '.', + 'Constants %s cannot be combined for %s of ' . $callableDescription . '.', + 'Combining constants with | is not allowed for %s of ' . $callableDescription . '.', ); } diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index 1f042288f0e..e2d8f9e4a3d 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -99,6 +99,9 @@ private function processSingleMethodCall(Scope $scope, MethodCall $node, string 'Return type of call to method ' . $messagesMethodName . ' contains unresolvable type.', '%s of method ' . $messagesMethodName . ' contains unresolvable type.', 'Method ' . $messagesMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of method ' . $messagesMethodName . '.', + 'Constants %s cannot be combined for %s of method ' . $messagesMethodName . '.', + 'Combining constants with | is not allowed for %s of method ' . $messagesMethodName . '.', )); } diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index 166ad74aced..c19aaff2b72 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -108,6 +108,9 @@ private function processSingleMethodCall(Scope $scope, StaticCall $node, string 'Return type of call to ' . $lowercasedMethodName . ' contains unresolvable type.', '%s of ' . $lowercasedMethodName . ' contains unresolvable type.', $displayMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of ' . $lowercasedMethodName . '.', + 'Constants %s cannot be combined for %s of ' . $lowercasedMethodName . '.', + 'Combining constants with | is not allowed for %s of ' . $lowercasedMethodName . '.', )); return $errors; diff --git a/tests/PHPStan/Reflection/ConstantToFunctionParameterMapTest.php b/tests/PHPStan/Reflection/ConstantToFunctionParameterMapTest.php new file mode 100644 index 00000000000..99a4041d6c7 --- /dev/null +++ b/tests/PHPStan/Reflection/ConstantToFunctionParameterMapTest.php @@ -0,0 +1,162 @@ += 8.0')] +class ConstantToFunctionParameterMapTest extends PHPStanTestCase +{ + + public function testMapIsValid(): void + { + $map = require __DIR__ . '/../../../resources/constantToFunctionParameterMap.php'; + $this->assertIsArray($map); + + $reflectionProvider = self::createReflectionProvider(); + + foreach ($map as $entry => $parameters) { + $this->assertIsString($entry, 'Entry key must be a string.'); + $this->assertIsArray($parameters, sprintf('Parameters for %s must be an array.', $entry)); + + if (str_contains($entry, '::')) { + // Method entry: Class::method + [$className, $methodName] = explode('::', $entry, 2); + + $this->assertTrue( + $reflectionProvider->hasClass($className), + sprintf('Class %s not found in reflection (from %s).', $className, $entry), + ); + + $classReflection = $reflectionProvider->getClass($className); + $this->assertTrue( + $classReflection->hasMethod($methodName), + sprintf('Method %s not found in reflection.', $entry), + ); + + $methodReflection = $classReflection->getNativeMethod($methodName); + $variants = $methodReflection->getVariants(); + $this->assertNotEmpty($variants, sprintf('Method %s has no variants.', $entry)); + + $reflectionParameters = $variants[0]->getParameters(); + } else { + $this->assertNotSame('', $entry); + // Function entry + $nameNode = new Name($entry); + $this->assertTrue( + $reflectionProvider->hasFunction($nameNode, null), + sprintf('Function %s() not found in reflection.', $entry), + ); + + $functionReflection = $reflectionProvider->getFunction($nameNode, null); + $variants = $functionReflection->getVariants(); + $this->assertNotEmpty($variants, sprintf('Function %s() has no variants.', $entry)); + + $reflectionParameters = $variants[0]->getParameters(); + } + + $reflectionParameterNames = []; + foreach ($reflectionParameters as $reflectionParameter) { + $reflectionParameterNames[] = $reflectionParameter->getName(); + } + + foreach ($parameters as $parameterName => $config) { + $this->assertIsString($parameterName, sprintf('Parameter name for %s must be a string.', $entry)); + $this->assertContains( + $parameterName, + $reflectionParameterNames, + sprintf( + 'Parameter $%s not found in %s. Available parameters: $%s', + $parameterName, + $entry, + implode(', $', $reflectionParameterNames), + ), + ); + + $this->assertIsArray($config, sprintf('Config for %s($%s) must be an array.', $entry, $parameterName)); + $this->assertArrayHasKey('type', $config, sprintf('Missing "type" key for %s($%s).', $entry, $parameterName)); + $this->assertContains($config['type'], ['single', 'bitmask'], sprintf('Invalid type "%s" for %s($%s).', $config['type'], $entry, $parameterName)); + $this->assertArrayHasKey('constants', $config, sprintf('Missing "constants" key for %s($%s).', $entry, $parameterName)); + $this->assertIsArray($config['constants'], sprintf('Constants for %s($%s) must be an array.', $entry, $parameterName)); + $this->assertNotEmpty($config['constants'], sprintf('Constants for %s($%s) must not be empty.', $entry, $parameterName)); + + foreach ($config['constants'] as $constantName) { + $this->assertIsString($constantName, sprintf('Constant name for %s($%s) must be a string.', $entry, $parameterName)); + + if (str_contains($constantName, '::')) { + // Class constant: Class::CONSTANT + [$constClassName, $constName] = explode('::', $constantName, 2); + $this->assertTrue( + $reflectionProvider->hasClass($constClassName), + sprintf('Class %s not found in reflection (constant %s used in %s($%s)).', $constClassName, $constantName, $entry, $parameterName), + ); + $constClassReflection = $reflectionProvider->getClass($constClassName); + $this->assertTrue( + $constClassReflection->hasConstant($constName), + sprintf('Constant %s not found in reflection (used in %s($%s)).', $constantName, $entry, $parameterName), + ); + } else { + $this->assertNotSame('', $constantName); + // Global constant + $constantNameNode = new Name($constantName); + $this->assertTrue( + $reflectionProvider->hasConstant($constantNameNode, null), + sprintf('Constant %s (used in %s($%s)) not found in reflection.', $constantName, $entry, $parameterName), + ); + } + } + + $allowedKeys = ['type', 'constants', 'exclusiveGroups']; + foreach (array_keys($config) as $key) { + $this->assertContains($key, $allowedKeys, sprintf('Unknown key "%s" in config for %s($%s).', $key, $entry, $parameterName)); + } + + if (!isset($config['exclusiveGroups'])) { + continue; + } + + $this->assertSame('bitmask', $config['type'], sprintf('exclusiveGroups only makes sense for bitmask type in %s($%s).', $entry, $parameterName)); + $this->assertIsArray($config['exclusiveGroups']); + + foreach ($config['exclusiveGroups'] as $groupIndex => $group) { + $this->assertIsArray($group, sprintf('Exclusive group #%d for %s($%s) must be an array.', $groupIndex, $entry, $parameterName)); + $this->assertGreaterThanOrEqual(2, count($group), sprintf('Exclusive group #%d for %s($%s) must have at least 2 constants.', $groupIndex, $entry, $parameterName)); + + foreach ($group as $constantName) { + $this->assertContains( + $constantName, + $config['constants'], + sprintf( + 'Constant %s in exclusive group #%d for %s($%s) is not in the constants list.', + $constantName, + $groupIndex, + $entry, + $parameterName, + ), + ); + } + } + } + } + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/constantToFunctionParameterMap.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Reflection/ParameterAllowedConstantsTest.php b/tests/PHPStan/Reflection/ParameterAllowedConstantsTest.php new file mode 100644 index 00000000000..89f135dc78d --- /dev/null +++ b/tests/PHPStan/Reflection/ParameterAllowedConstantsTest.php @@ -0,0 +1,285 @@ += 8.0')] +class ParameterAllowedConstantsTest extends PHPStanTestCase +{ + + public function testJsonEncodeFlagsAllowsJsonConstant(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('json_encode'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + $this->assertNotNull($flagsParam->getAllowedConstants()); + $this->assertTrue($flagsParam->getAllowedConstants()->isBitmask()); + + $jsonThrowOnError = $reflectionProvider->getConstant(new Name('JSON_THROW_ON_ERROR'), null); + $result = $flagsParam->checkAllowedConstants([$jsonThrowOnError]); + $this->assertTrue($result->isOk()); + + $sortRegular = $reflectionProvider->getConstant(new Name('SORT_REGULAR'), null); + $result = $flagsParam->checkAllowedConstants([$sortRegular]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + $this->assertSame('SORT_REGULAR', $result->getDisallowedConstants()[0]->getName()); + } + + public function testJsonDecodeDoesNotAllowEncodeOnlyConstants(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('json_decode'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[3]; + + $this->assertSame('flags', $flagsParam->getName()); + + $jsonPrettyPrint = $reflectionProvider->getConstant(new Name('JSON_PRETTY_PRINT'), null); + $result = $flagsParam->checkAllowedConstants([$jsonPrettyPrint]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + + $jsonThrowOnError = $reflectionProvider->getConstant(new Name('JSON_THROW_ON_ERROR'), null); + $result = $flagsParam->checkAllowedConstants([$jsonThrowOnError]); + $this->assertTrue($result->isOk()); + } + + public function testSortFlagsExclusiveGroups(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('sort'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + + $config = $flagsParam->getAllowedConstants(); + $this->assertNotNull($config); + $this->assertTrue($config->isBitmask()); + $this->assertCount(1, $config->getExclusiveGroups()); + $this->assertSame( + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + $config->getExclusiveGroups()[0], + ); + + $sortFlagCase = $reflectionProvider->getConstant(new Name('SORT_FLAG_CASE'), null); + $result = $flagsParam->checkAllowedConstants([$sortFlagCase]); + $this->assertTrue($result->isOk()); + } + + public function testHtmlspecialcharsMultipleExclusiveGroups(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('htmlspecialchars'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + + $config = $flagsParam->getAllowedConstants(); + $this->assertNotNull($config); + $this->assertCount(2, $config->getExclusiveGroups()); + $this->assertSame(['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], $config->getExclusiveGroups()[0]); + $this->assertSame(['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], $config->getExclusiveGroups()[1]); + } + + public function testSingleTypeParameter(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('round'), null); + $modeParam = $function->getVariants()[0]->getParameters()[2]; + + $this->assertSame('mode', $modeParam->getName()); + + $config = $modeParam->getAllowedConstants(); + $this->assertNotNull($config); + $this->assertFalse($config->isBitmask()); + $this->assertSame([], $config->getExclusiveGroups()); + + $halfUp = $reflectionProvider->getConstant(new Name('PHP_ROUND_HALF_UP'), null); + $result = $modeParam->checkAllowedConstants([$halfUp]); + $this->assertTrue($result->isOk()); + } + + public function testUnmappedParameterReturnsOk(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('strlen'), null); + $param = $function->getVariants()[0]->getParameters()[0]; + + $this->assertNull($param->getAllowedConstants()); + + $anyConstant = $reflectionProvider->getConstant(new Name('JSON_THROW_ON_ERROR'), null); + $result = $param->checkAllowedConstants([$anyConstant]); + $this->assertTrue($result->isOk()); + } + + public function testMethodWithGlobalConstants(): void + { + $reflectionProvider = self::createReflectionProvider(); + $class = $reflectionProvider->getClass('finfo'); + $method = $class->getNativeMethod('file'); + $flagsParam = $method->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + $this->assertNotNull($flagsParam->getAllowedConstants()); + $this->assertTrue($flagsParam->getAllowedConstants()->isBitmask()); + + $fileinfoMime = $reflectionProvider->getConstant(new Name('FILEINFO_MIME'), null); + $result = $flagsParam->checkAllowedConstants([$fileinfoMime]); + $this->assertTrue($result->isOk()); + + $sortRegular = $reflectionProvider->getConstant(new Name('SORT_REGULAR'), null); + $result = $flagsParam->checkAllowedConstants([$sortRegular]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + } + + public function testMethodWithClassConstants(): void + { + $reflectionProvider = self::createReflectionProvider(); + $class = $reflectionProvider->getClass('PDOStatement'); + $method = $class->getNativeMethod('fetch'); + $modeParam = $method->getVariants()[0]->getParameters()[0]; + + $this->assertSame('mode', $modeParam->getName()); + $this->assertNotNull($modeParam->getAllowedConstants()); + $this->assertFalse($modeParam->getAllowedConstants()->isBitmask()); + + $pdoClass = $reflectionProvider->getClass('PDO'); + + $fetchAssoc = $pdoClass->getConstant('FETCH_ASSOC'); + $result = $modeParam->checkAllowedConstants([$fetchAssoc]); + $this->assertTrue($result->isOk()); + + $attrErrmode = $pdoClass->getConstant('ATTR_ERRMODE'); + $result = $modeParam->checkAllowedConstants([$attrErrmode]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + } + + public function testClassConstantNotAllowedWhenGlobalConstantsExpected(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('json_encode'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $pdoClass = $reflectionProvider->getClass('PDO'); + $fetchAssoc = $pdoClass->getConstant('FETCH_ASSOC'); + + $result = $flagsParam->checkAllowedConstants([$fetchAssoc]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + } + + public function testViolatedExclusiveGroupsSortFlags(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('sort'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $sortNumeric = $reflectionProvider->getConstant(new Name('SORT_NUMERIC'), null); + $sortString = $reflectionProvider->getConstant(new Name('SORT_STRING'), null); + $sortFlagCase = $reflectionProvider->getConstant(new Name('SORT_FLAG_CASE'), null); + + // Two mutually exclusive sort types + $result = $flagsParam->checkAllowedConstants([$sortNumeric, $sortString]); + $this->assertFalse($result->isOk()); + $this->assertSame([], $result->getDisallowedConstants()); + $this->assertCount(1, $result->getViolatedExclusiveGroups()); + $this->assertSame(['SORT_NUMERIC', 'SORT_STRING'], $result->getViolatedExclusiveGroups()[0]); + + // Sort type + modifier is fine + $result = $flagsParam->checkAllowedConstants([$sortString, $sortFlagCase]); + $this->assertTrue($result->isOk()); + } + + public function testViolatedExclusiveGroupsHtmlEntities(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('htmlspecialchars'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $entQuotes = $reflectionProvider->getConstant(new Name('ENT_QUOTES'), null); + $entNoquotes = $reflectionProvider->getConstant(new Name('ENT_NOQUOTES'), null); + $entHtml401 = $reflectionProvider->getConstant(new Name('ENT_HTML401'), null); + $entHtml5 = $reflectionProvider->getConstant(new Name('ENT_HTML5'), null); + $entSubstitute = $reflectionProvider->getConstant(new Name('ENT_SUBSTITUTE'), null); + + // Violates both exclusive groups + $result = $flagsParam->checkAllowedConstants([$entQuotes, $entNoquotes, $entHtml401, $entHtml5]); + $this->assertFalse($result->isOk()); + $this->assertSame([], $result->getDisallowedConstants()); + $this->assertCount(2, $result->getViolatedExclusiveGroups()); + $this->assertSame(['ENT_QUOTES', 'ENT_NOQUOTES'], $result->getViolatedExclusiveGroups()[0]); + $this->assertSame(['ENT_HTML401', 'ENT_HTML5'], $result->getViolatedExclusiveGroups()[1]); + + // One from each group is fine + $result = $flagsParam->checkAllowedConstants([$entQuotes, $entHtml5, $entSubstitute]); + $this->assertTrue($result->isOk()); + } + + public function testBitmaskNotAllowedOnSingleParameter(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('array_unique'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + $this->assertNotNull($flagsParam->getAllowedConstants()); + $this->assertFalse($flagsParam->getAllowedConstants()->isBitmask()); + + $sortRegular = $reflectionProvider->getConstant(new Name('SORT_REGULAR'), null); + $sortNumeric = $reflectionProvider->getConstant(new Name('SORT_NUMERIC'), null); + + // Single constant is fine + $result = $flagsParam->checkAllowedConstants([$sortRegular]); + $this->assertTrue($result->isOk()); + $this->assertFalse($result->isBitmaskNotAllowed()); + + // Bitmask on single-value parameter is not allowed + $result = $flagsParam->checkAllowedConstants([$sortRegular, $sortNumeric]); + $this->assertFalse($result->isOk()); + $this->assertTrue($result->isBitmaskNotAllowed()); + } + + public function testBitmaskAllowedOnBitmaskParameter(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('json_encode'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertNotNull($flagsParam->getAllowedConstants()); + $this->assertTrue($flagsParam->getAllowedConstants()->isBitmask()); + + $prettyPrint = $reflectionProvider->getConstant(new Name('JSON_PRETTY_PRINT'), null); + $unescaped = $reflectionProvider->getConstant(new Name('JSON_UNESCAPED_SLASHES'), null); + + $result = $flagsParam->checkAllowedConstants([$prettyPrint, $unescaped]); + $this->assertTrue($result->isOk()); + $this->assertFalse($result->isBitmaskNotAllowed()); + } + + public function testBothDisallowedAndExclusiveViolation(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('sort'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $sortNumeric = $reflectionProvider->getConstant(new Name('SORT_NUMERIC'), null); + $sortString = $reflectionProvider->getConstant(new Name('SORT_STRING'), null); + $jsonThrowOnError = $reflectionProvider->getConstant(new Name('JSON_THROW_ON_ERROR'), null); + + // Wrong constant AND exclusive group violation + $result = $flagsParam->checkAllowedConstants([$sortNumeric, $sortString, $jsonThrowOnError]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + $this->assertSame('JSON_THROW_ON_ERROR', $result->getDisallowedConstants()[0]->getName()); + $this->assertCount(1, $result->getViolatedExclusiveGroups()); + $this->assertSame(['SORT_NUMERIC', 'SORT_STRING'], $result->getViolatedExclusiveGroups()[0]); + } + +} diff --git a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php index 1926cb56e53..b00993697a6 100644 --- a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php +++ b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php @@ -92,6 +92,7 @@ public static function dataSelectFromTypes(): Generator $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType(), $parameter->getAttributes(), + $parameter->getAllowedConstants(), ), $datePeriodConstructorVariants[0]->getParameters()), false, new VoidType(), @@ -123,6 +124,7 @@ public static function dataSelectFromTypes(): Generator $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType(), $parameter->getAttributes(), + $parameter->getAllowedConstants(), ), $datePeriodConstructorVariants[1]->getParameters()), false, new VoidType(), diff --git a/tests/PHPStan/Reflection/constantToFunctionParameterMap.neon b/tests/PHPStan/Reflection/constantToFunctionParameterMap.neon new file mode 100644 index 00000000000..72ae924610a --- /dev/null +++ b/tests/PHPStan/Reflection/constantToFunctionParameterMap.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 80500 diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 8558296e149..413aba97670 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -615,4 +615,19 @@ public function testBug11006(): void $this->analyse([__DIR__ . '/data/bug-11006.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheckInstantiation(): void + { + $this->analyse([__DIR__ . '/data/constant-parameter-check-instantiation.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #1 $flags of class finfo constructor.', + 12, + ], + [ + 'Constant GREGORIAN is not allowed for parameter #2 $dateType of class IntlDateFormatter constructor.', + 18, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php b/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php new file mode 100644 index 00000000000..256c7639938 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php @@ -0,0 +1,18 @@ += 8.0')] + public function testConstantParameterCheckCallables(): void + { + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/constant-parameter-check-callables.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of closure.', + 10, + ], + ]); + } + public function testMaybeNotCallable(): void { $errors = []; diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index f78f5918f77..9a539b30a89 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1586,9 +1586,14 @@ public function testBenevolentSuperglobalKeys(): void $this->analyse([__DIR__ . '/data/benevolent-superglobal-keys.php'], []); } + #[RequiresPhp('>= 8.0')] public function testFileParams(): void { $this->analyse([__DIR__ . '/data/file.php'], [ + [ + 'Constant FILE_APPEND is not allowed for parameter #2 $flags of function file.', + 16, + ], [ 'Parameter #2 $flags of function file expects 0|1|2|3|4|5|6|7|16|17|18|19|20|21|22|23, 8 given.', 16, @@ -1596,9 +1601,14 @@ public function testFileParams(): void ]); } + #[RequiresPhp('>= 8.0')] public function testFlockParams(): void { $this->analyse([__DIR__ . '/data/flock.php'], [ + [ + 'Constant FILE_APPEND is not allowed for parameter #2 $operation of function flock.', + 45, + ], [ 'Parameter #2 $operation of function flock expects int<0, 7>, 8 given.', 45, @@ -1614,6 +1624,10 @@ public function testJsonValidate(): void 'Parameter #2 $depth of function json_validate expects int<1, max>, 0 given.', 6, ], + [ + 'Constant JSON_BIGINT_AS_STRING is not allowed for parameter #3 $flags of function json_validate.', + 7, + ], [ 'Parameter #3 $flags of function json_validate expects 0|1048576, 2 given.', 7, @@ -2758,4 +2772,70 @@ public function testBug14312b(): void $this->analyse([__DIR__ . '/data/bug-14312b.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheck(): void + { + $this->analyse([__DIR__ . '/data/constant-parameter-check.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of function json_encode.', + 12, + ], + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of function json_encode.', + 21, + ], + [ + 'Constants SORT_NUMERIC, SORT_STRING cannot be combined for parameter #2 $flags of function sort.', + 27, + ], + [ + 'Constants SORT_NUMERIC, SORT_STRING cannot be combined for parameter #2 $flags of function sort.', + 30, + ], + [ + 'Constants ENT_QUOTES, ENT_NOQUOTES cannot be combined for parameter #2 $flags of function htmlspecialchars.', + 33, + ], + [ + 'Constants ENT_HTML401, ENT_HTML5 cannot be combined for parameter #2 $flags of function htmlspecialchars.', + 33, + ], + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $filter of function filter_var.', + 39, + ], + [ + 'Constant JSON_PRETTY_PRINT is not allowed for parameter #4 $flags of function json_decode.', + 51, + ], + [ + 'Constants LOCK_SH, LOCK_EX cannot be combined for parameter #2 $operation of function flock.', + 54, + ], + [ + 'Constant SORT_REGULAR is not allowed for parameter $flags of function json_encode.', + 70, + ], + [ + 'Combining constants with | is not allowed for parameter #2 $flags of function array_unique.', + 76, + ], + [ + 'Combining constants with | is not allowed for parameter #2 $filter of function filter_var.', + 79, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug12850(): void + { + $this->analyse([__DIR__ . '/data/bug-12850.php'], [ + [ + 'Constants LOCK_EX, LOCK_SH cannot be combined for parameter #2 $operation of function flock.', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index dd4b3259a83..d695ffa2ad7 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -102,4 +102,15 @@ public function testNoNamedArguments(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheckCallUserFunc(): void + { + $this->analyse([__DIR__ . '/data/constant-parameter-check-call-user-func.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of callable passed to call_user_func().', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-12850.php b/tests/PHPStan/Rules/Functions/data/bug-12850.php new file mode 100644 index 00000000000..fa8a41ab5f0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12850.php @@ -0,0 +1,18 @@ += 8.0 + +namespace ConstantParameterCheckCallUserFunc; + +// call_user_func with correct constant +call_user_func('json_encode', [], JSON_PRETTY_PRINT); + +// call_user_func with wrong constant +call_user_func('json_encode', [], SORT_REGULAR); diff --git a/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php new file mode 100644 index 00000000000..6acb0ba1876 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php @@ -0,0 +1,10 @@ += 8.0')] + public function testConstantParameterCheckMethods(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/constant-parameter-check-methods.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of method finfo::file().', + 10, + ], + [ + 'Constant ATTR_ERRMODE is not allowed for parameter #1 $mode of method PDOStatement::fetch().', + 17, + ], + [ + 'Constant FRENCH_COLLATION is not allowed for parameter #2 $flags of method Collator::sort().', + 25, + ], + ]); + } + public function testBug11463(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 82ee7e15669..9345fce6418 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -997,4 +997,20 @@ public function testPipeOperator(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheckStatic(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/constant-parameter-check-static.php'], [ + [ + 'Constant GREGORIAN is not allowed for parameter #2 $dateType of static method IntlDateFormatter::create().', + 9, + ], + [ + 'Constant TYPE_INT32 is not allowed for parameter #2 $style of static method NumberFormatter::create().', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php new file mode 100644 index 00000000000..7ecae9f3f89 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php @@ -0,0 +1,25 @@ +file('test.txt', FILEINFO_MIME_TYPE); + +// finfo::file - wrong constant +$finfo->file('test.txt', SORT_REGULAR); + +// PDOStatement::fetch - correct class constant +/** @var \PDOStatement $stmt */ +$stmt->fetch(\PDO::FETCH_ASSOC); + +// PDOStatement::fetch - wrong class constant +$stmt->fetch(\PDO::ATTR_ERRMODE); + +// Collator::sort - correct class constant +/** @var \Collator $collator */ +$arr = []; +$collator->sort($arr, \Collator::SORT_STRING); + +// Collator::sort - wrong class constant +$collator->sort($arr, \Collator::FRENCH_COLLATION); diff --git a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php new file mode 100644 index 00000000000..16b7298f55c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php @@ -0,0 +1,15 @@ +