diff --git a/src/Visitor.php b/src/Visitor.php index f15fc5a..a6dd986 100644 --- a/src/Visitor.php +++ b/src/Visitor.php @@ -552,6 +552,25 @@ private function addStringTags(string $name, Doc $docComment): ?Doc $additions = $this->additionalTagStrings[$name]; $docCommentText = $docComment->getText(); + try { + $docblock = $this->docBlockFactory->create($docCommentText); + } catch (\RuntimeException | \InvalidArgumentException $e) { + return null; + } + + if (count($docblock->getTagsByName('phpstan-return')) > 0) { + $additions = array_filter( + $additions, + static function (string $tag): bool { + return ! str_starts_with($tag, '@phpstan-return '); + } + ); + } + + if (count($additions) === 0) { + return null; + } + $newDocComment = sprintf( "%s\n * %s\n */", substr($docCommentText, 0, -self::WP_INDENT_SIZE), diff --git a/tests/VisitorTest.php b/tests/VisitorTest.php index a36c7b5..4cac654 100644 --- a/tests/VisitorTest.php +++ b/tests/VisitorTest.php @@ -67,6 +67,33 @@ function wpstubs_test_connector_with_phpstan_return( string $id ): ?array { ); } + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testMapReturnDoesNotOverrideExistingReturn(): void + { + $code = <<<'PHP' + ', get_post_types([])); -assertType('array', get_post_types([], 'names')); - -// Objects output -assertType('array', get_post_types([], 'objects')); - -// Unknown string -assertType('array', get_post_types([], Faker::string())); - -// Unexpected output -assertType('array', get_post_types([], 'Hello')); diff --git a/tests/data/return/get-posts.php b/tests/data/return/get-posts.php deleted file mode 100644 index 32c6f3a..0000000 --- a/tests/data/return/get-posts.php +++ /dev/null @@ -1,67 +0,0 @@ -', get_posts()); -assertType('array', get_posts(['key' => 'value'])); -assertType('array', get_posts(['fields' => ''])); -assertType('array', get_posts(['fields' => 'ids'])); -assertType('array', get_posts(['fields' => 'id=>parent'])); -assertType('array', get_posts(['fields' => 'Hello'])); - -// Nonconstant array -assertType('array', get_posts(Faker::array())); - -// Unions -$union = Faker::union(['key' => 'value'], ['some' => 'thing']); -assertType('array', get_posts($union)); - -$union = Faker::union(['key' => 'value'], ['fields' => 'ids']); -assertType('array', get_posts($union)); - -$union = Faker::union(['key' => 'value'], ['fields' => '']); -assertType('array', get_posts($union)); - -$union = Faker::union(['key' => 'value'], ['fields' => 'id=>parent']); -assertType('array', get_posts($union)); - -$union = Faker::union(['fields' => ''], ['fields' => 'ids']); -assertType('array', get_posts($union)); - -$union = Faker::union(['fields' => ''], ['fields' => 'id=>parent']); -assertType('array', get_posts($union)); - -$union = Faker::union(['fields' => 'ids'], ['fields' => 'id=>parent']); -assertType('array', get_posts($union)); - -$union = Faker::union(Faker::array(), ['fields' => '']); -assertType('array', get_posts($union)); - -$union = Faker::union(Faker::array(), ['fields' => 'ids']); -assertType('array', get_posts($union)); - -$union = Faker::union(Faker::array(), ['fields' => 'id=>parent']); -assertType('array', get_posts($union)); - -$union = Faker::union(Faker::string(), ''); -assertType('array', get_posts(['fields' => $union])); - -$union = Faker::union(Faker::string(), 'ids'); -assertType('array', get_posts(['fields' => $union])); - -$union = Faker::union(Faker::string(), 'id=>parent'); -assertType('array', get_posts(['fields' => $union])); - -$union = Faker::union(Faker::string(), 'fields'); -assertType('array', get_posts([$union => ''])); - -$union = Faker::union(Faker::string(), 'fields'); -assertType('array', get_posts([$union => 'ids'])); - -$union = Faker::union(Faker::string(), 'fields'); -assertType('array', get_posts([$union => 'id=>parent'])); diff --git a/tests/data/return/sanitize-post-field.php b/tests/data/return/sanitize-post-field.php index f6f2e7f..0f52fc7 100644 --- a/tests/data/return/sanitize-post-field.php +++ b/tests/data/return/sanitize-post-field.php @@ -9,86 +9,27 @@ $postId = Faker::int(); -// Int fields => int - regardless of (non-constant) $value assertType('int', sanitize_post_field('ID', Faker::mixed(), $postId, 'raw')); assertType('int', sanitize_post_field('post_parent', Faker::mixed(), $postId, 'raw')); assertType('int', sanitize_post_field('menu_order', Faker::mixed(), $postId, 'raw')); -// Also in any other context assertType('int', sanitize_post_field('ID', Faker::mixed(), $postId, Faker::string())); -// But as-is if $value is int or int range -assertType('123', sanitize_post_field('ID', 123, $postId, 'raw')); -assertType('int<1, max>', sanitize_post_field('ID', Faker::positiveInt(), $postId, 'raw')); -assertType('123', sanitize_post_field('ID', 123, $postId, Faker::string())); -// Int array fields => array> - regardless of (non-constant) $value and $context assertType('array>', sanitize_post_field('ancestors', Faker::mixed(), $postId, Faker::string())); -// But as-is if $value is array> or list> - regardless of $context -assertType('array{41, 42, 43}', sanitize_post_field('ancestors', [41, 42, 43], $postId, Faker::string())); -assertType('array{foo: 42}', sanitize_post_field('ancestors', ['foo' => 42], $postId, Faker::string())); -// And retain list type - regardless of $context -assertType('array, int<0, max>>', sanitize_post_field('ancestors', Faker::list(), $postId, Faker::string())); -// All other constant fields: - -// In raw context => as-is -assertType("'field value'", sanitize_post_field('field', 'field value', $postId, 'raw')); -assertType('123', sanitize_post_field('field', 123, $postId, 'raw')); -assertType("array{foo: 'bar'}", sanitize_post_field('field', ['foo' => 'bar'], $postId, 'raw')); -assertType('true', sanitize_post_field('field', true, $postId, 'raw')); -assertType('bool', sanitize_post_field('field', Faker::bool(), $postId, 'raw')); -assertType('int', sanitize_post_field('field', Faker::int(), $postId, 'raw')); assertType('string', sanitize_post_field('field', Faker::string(), $postId, 'raw')); -// Field values in edit may be filtered to mixed, but are escaped using esc_html or esc_attr => string assertType('string', sanitize_post_field('field', 'field value', $postId, 'edit')); assertType('string', sanitize_post_field('field', Faker::string(), $postId, 'edit')); -// Except for field 'post_content' which is only escaped if ! user_can_richedit() -assertType('mixed', sanitize_post_field('post_content', 'field value', $postId, 'edit')); -assertType('mixed', sanitize_post_field('post_content', Faker::string(), $postId, 'edit')); -// Field values in attribute/js context are not filtered, but are escaped using esc_attr/esc_js -// => string, but not as given in the argument assertType('string', sanitize_post_field('field', 'field value', $postId, 'attribute')); assertType('string', sanitize_post_field('field', Faker::string(), $postId, 'attribute')); assertType('string', sanitize_post_field('field', 'field value', $postId, 'js')); assertType('string', sanitize_post_field('field', Faker::string(), $postId, 'js')); -// Field values in any other context may be filtered to mixed => mixed -assertType('mixed', sanitize_post_field('field', Faker::array(), $postId, 'db')); -assertType('mixed', sanitize_post_field('field', Faker::array(), $postId, 'display')); -assertType('mixed', sanitize_post_field('field', Faker::array(), $postId, 'rss')); -assertType('mixed', sanitize_post_field('field', Faker::bool(), $postId, 'db')); -assertType('mixed', sanitize_post_field('field', Faker::bool(), $postId, 'display')); -assertType('mixed', sanitize_post_field('field', Faker::bool(), $postId, 'rss')); -assertType('mixed', sanitize_post_field('field', Faker::int(), $postId, 'db')); -assertType('mixed', sanitize_post_field('field', Faker::int(), $postId, 'display')); -assertType('mixed', sanitize_post_field('field', Faker::int(), $postId, 'rss')); -assertType('mixed', sanitize_post_field('field', Faker::object(), $postId, 'db')); -assertType('mixed', sanitize_post_field('field', Faker::object(), $postId, 'display')); -assertType('mixed', sanitize_post_field('field', Faker::object(), $postId, 'rss')); -assertType('mixed', sanitize_post_field('field', Faker::string(), $postId, 'db')); -assertType('mixed', sanitize_post_field('field', Faker::string(), $postId, 'display')); -assertType('mixed', sanitize_post_field('field', Faker::string(), $postId, 'rss')); - -// Non constant field: - -// Non constant field in raw context => int (int field) or array> (int array field) or T (other field) -assertType("'field value'|array>|int", sanitize_post_field(Faker::string(), 'field value', $postId, 'raw')); assertType('array>|int|string', sanitize_post_field(Faker::string(), Faker::string(), $postId, 'raw')); -// Non constant field in attribute|js context -// => int (int field) or array> (int array field) or string (other field) assertType('array>|int|string', sanitize_post_field(Faker::string(), Faker::string(), $postId, 'attribute')); assertType('array>|int|string', sanitize_post_field(Faker::string(), Faker::string(), $postId, 'js')); -// Non constant field in attribute|js context than raw -// => int (int field) or array> (int array field) or string (other field) assertType('array>|int|string', sanitize_post_field(Faker::string(), Faker::string(), $postId, 'attribute')); assertType('array>|int|string', sanitize_post_field(Faker::string(), Faker::string(), $postId, 'js')); - -// Non constant field in any other context than attribute|js|raw => mixed -assertType('mixed', sanitize_post_field(Faker::string(), 123, $postId, 'db')); -assertType('mixed', sanitize_post_field(Faker::string(), 'field value', $postId, 'display')); -assertType('mixed', sanitize_post_field(Faker::string(), Faker::array(), $postId, 'rss')); -// And because of 'post_content': -assertType('mixed', sanitize_post_field(Faker::string(), Faker::string(), $postId, 'edit')); diff --git a/tests/data/return/stripslashes.php b/tests/data/return/stripslashes.php index ee28b72..fead077 100644 --- a/tests/data/return/stripslashes.php +++ b/tests/data/return/stripslashes.php @@ -15,9 +15,6 @@ assertType('int', stripslashes_from_strings_only(Faker::int())); assertType('1.23', stripslashes_from_strings_only(1.23)); assertType('float', stripslashes_from_strings_only(Faker::float())); -assertType("''", stripslashes_from_strings_only('')); -assertType('string', stripslashes_from_strings_only('value')); -assertType('string', stripslashes_from_strings_only(Faker::string())); assertType("array{key: 'value'}", stripslashes_from_strings_only(['key' => 'value'])); assertType('array', stripslashes_from_strings_only(Faker::array())); assertType('resource', stripslashes_from_strings_only(Faker::resource())); diff --git a/tests/data/return/term-exists.php b/tests/data/return/term-exists.php index a81b3ba..500b30d 100644 --- a/tests/data/return/term-exists.php +++ b/tests/data/return/term-exists.php @@ -12,41 +12,12 @@ $termInt = Faker::int(); $termIntStr = Faker::union(Faker::int(), Faker::string()); -// Empty taxonomy -assertType('string|null', term_exists(123)); -assertType('string|null', term_exists(123, '')); -assertType('string|null', term_exists($termStr)); -assertType('string|null', term_exists($termStr, '')); -assertType('0|string|null', term_exists($termInt)); -assertType('0|string|null', term_exists($termInt, '')); -assertType('0|string|null', term_exists($termIntStr)); -assertType('0|string|null', term_exists($termIntStr, '')); - -// Fixed taxonomy string -assertType('array{term_id: string, term_taxonomy_id: string}|null', term_exists(123, 'category')); -assertType('array{term_id: string, term_taxonomy_id: string}|null', term_exists($termStr, 'category')); -assertType('0|array{term_id: string, term_taxonomy_id: string}|null', term_exists($termInt, 'category')); -assertType('0|array{term_id: string, term_taxonomy_id: string}|null', term_exists($termIntStr, 'category')); - -// Unknown taxonomy type -$taxonomy = (string)$_GET['taxonomy'] ?? ''; -assertType('array{term_id: string, term_taxonomy_id: string}|string|null', term_exists(123, Faker::string())); -assertType('array{term_id: string, term_taxonomy_id: string}|string|null', term_exists($termStr, $taxonomy)); -assertType('0|array{term_id: string, term_taxonomy_id: string}|string|null', term_exists($termInt, Faker::string())); -assertType('0|array{term_id: string, term_taxonomy_id: string}|string|null', term_exists($termIntStr, Faker::string())); - // Term 0 assertType('0', term_exists(0)); assertType('0', term_exists(0, '')); assertType('0', term_exists(0, 'category')); assertType('0', term_exists(0, Faker::string())); -// Term empty string -assertType('null', term_exists('')); -assertType('null', term_exists('', '')); -assertType('null', term_exists('', 'category')); -assertType('null', term_exists('', Faker::string())); - // tag_exists() assertType('0', tag_exists(0)); assertType('null', tag_exists('')); diff --git a/tests/data/return/wp-error-parameter.php b/tests/data/return/wp-error-parameter.php index f221575..22695ee 100644 --- a/tests/data/return/wp-error-parameter.php +++ b/tests/data/return/wp-error-parameter.php @@ -5,9 +5,7 @@ namespace PhpStubs\WordPress\Core\Tests; use function wp_clear_scheduled_hook; -use function wp_insert_attachment; use function wp_insert_category; -use function wp_insert_post; use function wp_reschedule_event; use function wp_schedule_event; use function wp_schedule_single_event; @@ -15,7 +13,6 @@ use function wp_unschedule_event; use function wp_unschedule_hook; use function wp_update_comment; -use function wp_update_post; use function PHPStan\Testing\assertType; /* @@ -94,48 +91,3 @@ assertType('int<0, max>|false', wp_unschedule_hook('hook', false)); assertType('int<0, max>|WP_Error', wp_unschedule_hook('hook', true)); assertType('int<0, max>|WP_Error|false', wp_unschedule_hook('hook', Faker::bool())); - -/* - * wp_insert_post() - */ -assertType('int<0, max>', wp_insert_post([])); -assertType('int<0, max>', wp_insert_post([], false)); -assertType('int<0, max>', wp_insert_post([], false, true)); -assertType('int<0, max>', wp_insert_post([], false, false)); -assertType('int<1, max>|WP_Error', wp_insert_post([], true)); -assertType('int<1, max>|WP_Error', wp_insert_post([], true, true)); -assertType('int<1, max>|WP_Error', wp_insert_post([], true, false)); -assertType('int<0, max>|WP_Error', wp_insert_post([], Faker::bool())); -assertType('int<0, max>|WP_Error', wp_insert_post([], Faker::bool(), true)); -assertType('int<0, max>|WP_Error', wp_insert_post([], Faker::bool(), false)); - -/* - * wp_update_post() - */ -assertType('int<0, max>', wp_update_post([])); -assertType('int<0, max>', wp_update_post([], false)); -assertType('int<0, max>', wp_update_post([], false, true)); -assertType('int<0, max>', wp_update_post([], false, false)); -assertType('int<1, max>|WP_Error', wp_update_post([], true)); -assertType('int<1, max>|WP_Error', wp_update_post([], true, true)); -assertType('int<1, max>|WP_Error', wp_update_post([], true, false)); -assertType('int<0, max>|WP_Error', wp_update_post([], Faker::bool())); -assertType('int<0, max>|WP_Error', wp_update_post([], Faker::bool(), true)); -assertType('int<0, max>|WP_Error', wp_update_post([], Faker::bool(), false)); - -/* - * wp_insert_attachment() - */ -assertType('int<0, max>', wp_insert_attachment([])); -assertType('int<0, max>', wp_insert_attachment([], true)); -assertType('int<0, max>', wp_insert_attachment([], false)); -assertType('int<0, max>', wp_insert_attachment([], true, 1)); -assertType('int<0, max>', wp_insert_attachment([], true, 1, false)); -assertType('int<0, max>', wp_insert_attachment([], true, 1, false, false)); -assertType('int<0, max>', wp_insert_attachment([], true, 1, false, true)); -assertType('int<1, max>|WP_Error', wp_insert_attachment([], false, 0, true)); -assertType('int<1, max>|WP_Error', wp_insert_attachment([], false, 0, true, true)); -assertType('int<1, max>|WP_Error', wp_insert_attachment([], false, 0, true, false)); -assertType('int<0, max>|WP_Error', wp_insert_attachment([], true, 1, Faker::bool())); -assertType('int<0, max>|WP_Error', wp_insert_attachment([], true, 1, Faker::bool(), true)); -assertType('int<0, max>|WP_Error', wp_insert_attachment([], true, 1, Faker::bool(), false)); diff --git a/tests/data/return/wp-query.php b/tests/data/return/wp-query.php index 98e47d7..a10ab3e 100644 --- a/tests/data/return/wp-query.php +++ b/tests/data/return/wp-query.php @@ -18,29 +18,6 @@ assertType('bool|string', Faker::wpQuery()->query_vars_hash); assertType('null', Faker::wpQuery()->init_query_flags()); -// WP_Query::query(): default fields value. -assertType('array', Faker::wpQuery()->query([])); -assertType('array', Faker::wpQuery()->query(['fields' => ''])); -assertType('array', Faker::wpQuery()->query(['fields' => 'all'])); - -// WP_Query::query(): requesting IDs. -assertType('array', Faker::wpQuery()->query(['fields' => 'ids'])); -assertType('array', Faker::wpQuery()->query(['post_type' => 'page', 'fields' => 'ids'])); - -// WP_Query::query(): requesting parent IDs keyed by post ID. -assertType('array', Faker::wpQuery()->query(['fields' => 'id=>parent'])); - -// WP_Query::query(): unrecognized fields values fall back to post objects. -assertType('array', Faker::wpQuery()->query(['fields' => 'foo'])); - -// WP_Query::query(): unknown fields value. -assertType('array', Faker::wpQuery()->query(['fields' => Faker::string()])); - -// WP_Query::query(): a URL query string is parsed at runtime, so the fields -// value is not visible to PHPStan and every result shape stays possible. -assertType('array', Faker::wpQuery()->query('fields=ids')); -assertType('array', Faker::wpQuery()->query(Faker::string())); - // WP_Query::get_posts() reads the query vars off the instance instead of taking // them as an argument, so there is nothing to narrow the return type on. assertType('array', Faker::wpQuery()->get_posts()); diff --git a/tests/data/return/wp-upload-dir.php b/tests/data/return/wp-upload-dir.php index 9016b3b..fa06891 100644 --- a/tests/data/return/wp-upload-dir.php +++ b/tests/data/return/wp-upload-dir.php @@ -5,10 +5,6 @@ namespace PhpStubs\WordPress\Core\Tests; use function _wp_upload_dir; -use function wp_get_upload_dir; -use function wp_upload_dir; use function PHPStan\Testing\assertType; assertType('array{path: non-falsy-string, url: non-falsy-string, subdir: string, basedir: non-falsy-string, baseurl: non-falsy-string, error: false}', _wp_upload_dir()); -assertType('array{path: string, url: string, subdir: string, basedir: string, baseurl: string, error: string|false}', wp_get_upload_dir()); -assertType('array{path: string, url: string, subdir: string, basedir: string, baseurl: string, error: string|false}', wp_upload_dir()); diff --git a/wordpress-stubs.php b/wordpress-stubs.php index 9565127..c924d65 100644 --- a/wordpress-stubs.php +++ b/wordpress-stubs.php @@ -45004,7 +45004,6 @@ class WP_Http * filename: string|null, * http_response: WP_HTTP_Requests_Response|null, * } - * @phpstan-return array{headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, body: string, response: array{code: int, message: string}, cookies: array, filename: string|null, http_response: \WP_HTTP_Requests_Response}|\WP_Error */ public function request($url, $args = array()) { @@ -50965,7 +50964,6 @@ public function rewind_comments() * @phpstan-return ( * $query is array{ fields: 'ids', ... } ? int[] : WP_Post[] * ) - * @phpstan-return ($query is array ? ($query is array{fields: 'id=>parent'|'ids', ...} ? array : array) : array|array) */ public function query($query) { @@ -130545,7 +130543,6 @@ function stripslashes_deep($value) * @phpstan-template T * @phpstan-param T|string $value * @phpstan-pure - * @phpstan-return ($value is string ? ($value is '' ? '' : string) : T) */ function stripslashes_from_strings_only($value) { @@ -132779,7 +132776,6 @@ function win_is_writable($path) * baseurl: non-empty-string, * } * |array{ error: non-empty-string } - * @phpstan-return array{path: string, url: string, subdir: string, basedir: string, baseurl: string, error: false|string} */ function wp_get_upload_dir() { @@ -132828,7 +132824,6 @@ function wp_get_upload_dir() * baseurl: non-empty-string, * } * |array{ error: non-empty-string } - * @phpstan-return array{path: string, url: string, subdir: string, basedir: string, baseurl: string, error: false|string} */ function wp_upload_dir($time = \null, $create_dir = \true, $refresh_cache = \false) { @@ -133158,7 +133153,6 @@ function wp_nonce_ays($action) * code?: string, * exit?: bool, * } $args - * @phpstan-return ($args is array{exit: false, ...} ? void : never) */ function wp_die($message = '', $title = '', $args = array()) { @@ -150588,7 +150582,6 @@ function auth_redirect() * as the function otherwise exits rather than returning false. * @phpstan-return ( $action is -1 ? int|false : int ) * @phpstan-param string $action - * @phpstan-return ($action is -1 ? 1|2|false : 1|2) */ function check_admin_referer($action = -1, $query_arg = '_wpnonce') { @@ -150610,7 +150603,6 @@ function check_admin_referer($action = -1, $query_arg = '_wpnonce') * as the function otherwise exits rather than returning false. * @phpstan-return ( $stop is true ? int : int|false ) * @phpstan-param string $action - * @phpstan-return ($stop is true ? 1|2 : 1|2|false) */ function check_ajax_referer($action = -1, $query_arg = \false, $stop = \true) { @@ -153294,7 +153286,6 @@ function get_post_type_object($post_type) * @return string[]|WP_Post_Type[] An array of post type names or objects. * @phpstan-return ( $output is 'names' ? string[] : WP_Post_Type[] ) * @phpstan-param 'names'|'objects' $output - * @phpstan-return ($output is 'names' ? array : array) */ function get_post_types($args = array(), $output = 'names', $operator = 'and') { @@ -154025,7 +154016,6 @@ function is_post_embeddable($post = \null) * w?: int, * year?: int, * } $args - * @phpstan-return ($args is array{fields: 'id=>parent'|'ids', ...} ? array : array) */ function get_posts($args = \null) { @@ -154298,7 +154288,6 @@ function sanitize_post($post, $context = 'display') * ) * @phpstan-template T of mixed * @phpstan-param T $value - * @phpstan-return ($field is 'ID'|'post_parent'|'menu_order' ? (T is int ? T : int) : ($field is 'ancestors' ? (T is array>|list> ? T : (T is list ? list> : array>)) : ($context is 'raw' ? T : ($context is 'attribute'|'js' ? string : ($context is 'edit' ? ($field is not 'post_content' ? string : mixed) : mixed))))) */ function sanitize_post_field($field, $value, $post_id, $context = 'display') { @@ -154823,7 +154812,6 @@ function wp_get_recent_posts($args = array(), $output = \ARRAY_A) * meta_input?: array, * page_template?: string, * } $postarr - * @phpstan-return ($wp_error is false ? int<0, max> : int<1, max>|\WP_Error) */ function wp_insert_post($postarr, $wp_error = \false, $fire_after_hooks = \true) { @@ -154876,7 +154864,6 @@ function wp_insert_post($postarr, $wp_error = \false, $fire_after_hooks = \true) * meta_input?: array, * page_template?: string, * } $postarr See wp_insert_post() - * @phpstan-return ($wp_error is false ? int<0, max> : int<1, max>|\WP_Error) */ function wp_update_post($postarr = array(), $wp_error = \false, $fire_after_hooks = \true) { @@ -155367,7 +155354,6 @@ function is_local_attachment($url) * @phpstan-return ( * $wp_error is false ? int : int|WP_Error * ) - * @phpstan-return ($wp_error is false ? int<0, max> : int<1, max>|\WP_Error) */ function wp_insert_attachment($args, $file = \false, $parent_post_id = 0, $wp_error = \false, $fire_after_hooks = \true) { @@ -161313,7 +161299,6 @@ function unregister_term_meta($taxonomy, $meta_key) * ) * ) * ) - * @phpstan-return ($term is 0 ? 0 : ($term is '' ? null : ($taxonomy is '' ? string|null : array{term_id: string, term_taxonomy_id: string}|null))) */ function term_exists($term, $taxonomy = '', $parent_term = \null) {