Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ec6f6b5
generate stubs for WordPress 7.1
IanDelMar Aug 20, 2026
f737ed7
WP_Filesystem: remove obsolete function map entries
IanDelMar Aug 20, 2026
73ffe7c
don't generate a `@phpstan-param` tag when the docblock already has one
IanDelMar Aug 20, 2026
e12b9cb
sanitize_post: remove obsolete function map entry
IanDelMar Aug 20, 2026
c112a21
block_core_...: remove obsolete function map entries
IanDelMar Aug 20, 2026
12cb421
fix incompatible return type in final-tag test data
IanDelMar Aug 20, 2026
4ebe1f1
don't infer void or never when the docblock already has a return type
IanDelMar Aug 20, 2026
4d51c3c
wpdb: remove obsolete function map entries
IanDelMar Aug 20, 2026
e05d18e
get_approved_comments: remove obsolete function map entry
IanDelMar Aug 20, 2026
bdb2c17
get_comment: remove obsolete function map entry
IanDelMar Aug 20, 2026
1544f11
stripslashes_deep: remove obsolete function map entry
IanDelMar Aug 20, 2026
03ed63e
restore legacy data for monitoring WP core types
IanDelMar Aug 20, 2026
ff84b56
_get_cron_array: remove obsolete function map entry
IanDelMar Aug 20, 2026
e3015d0
wp_slash: remove obsolete return type function map entry
IanDelMar Aug 20, 2026
84b0470
wp_unslash: remove obsolete function map entry
IanDelMar Aug 20, 2026
54efadc
wp_parse_list: remove obsolete function map entry
IanDelMar Aug 20, 2026
47d7a9f
Update wp-is-numeric-array.php
IanDelMar Aug 20, 2026
4aa46d4
add monitoring data to PHPStan exclusions
IanDelMar Aug 20, 2026
61fd26d
Update phpcs.xml.dist
IanDelMar Aug 20, 2026
0d75e5d
visitor: fix types for generic PHPStan tags
IanDelMar Aug 20, 2026
c55d8d1
Update term-exists.php
IanDelMar Aug 20, 2026
9e626fb
Revert "Update term-exists.php"
IanDelMar Aug 20, 2026
0aec055
check_admin_referer: add conditional return type
IanDelMar Aug 20, 2026
6a0cf5d
check_ajax_referer: add conditional return type
IanDelMar Aug 20, 2026
47c158e
get_post_ancestors: remove obsolete function map entry
IanDelMar Aug 21, 2026
6e51a8f
get_post: remove obsolete param type function map entry
IanDelMar Aug 21, 2026
d272222
wp_get_speculation_rules_configuration: remove obsolete function map …
IanDelMar Aug 21, 2026
fddd35e
block_core_navigation_render_submenu_icon: remove obsolete function m…
IanDelMar Aug 21, 2026
c2b4e1a
edit_term_link: update return type
IanDelMar Aug 21, 2026
38e4cdc
wp_unique_id & wp_unique_prefixed_id: narrow return type
IanDelMar Aug 21, 2026
ccca9c4
wp_is_numeric_array: remove obsolete assert fn map entry
IanDelMar Aug 21, 2026
94713bb
WP_Widget_Factory::$widgets: update property type
IanDelMar Aug 22, 2026
44836e8
WP_Widget_Factory::[un]register: add param test
IanDelMar Aug 22, 2026
7f2bf33
get_page_by_path: remove obsolete function map entry
IanDelMar Aug 22, 2026
e9af817
get_post: remove obsolete function map entry
IanDelMar Aug 22, 2026
5abda53
wp_get_elements_class_name: narrow return type
IanDelMar Aug 23, 2026
479cfa1
use `...` for unsealed arrays
IanDelMar Aug 23, 2026
ed9d9f8
use `...` for unsealed arrays
IanDelMar Aug 23, 2026
e170e07
bump PHPStan to ^2.2
IanDelMar Aug 23, 2026
a9ad3aa
Update Visitor.php
IanDelMar Aug 24, 2026
f2ef05c
wp_upload_dir: fix return type
IanDelMar Aug 24, 2026
87f8df8
Merge remote-tracking branch 'upstream/master' into wp71
IanDelMar Aug 24, 2026
c584af8
Merge remote-tracking branch 'upstream/master' into wp71
IanDelMar Aug 25, 2026
673e7c9
wp_upload_dir: widen return type
IanDelMar Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"nikic/php-parser": "^5.5",
"php-stubs/generator": "^0.9",
"phpdocumentor/reflection-docblock": "^6.0",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan": "^2.2",
"phpunit/phpunit": "^9.5",
"symfony/polyfill-php80": "*",
"szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.2",
Expand Down
87 changes: 34 additions & 53 deletions functionMap.php

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<exclude-pattern>tests/data/param/wpdb.php</exclude-pattern>
<exclude-pattern>tests/data/param/wp-robots.php</exclude-pattern>
<exclude-pattern>tests/data/param/absint.php</exclude-pattern>
<exclude-pattern>tests/monitoring-data/param/wpdb.php</exclude-pattern>
</rule>
<rule ref="PSR1.Files.SideEffects.FoundWithSymbols">
<exclude-pattern>tests/data/**/__demo.php</exclude-pattern>
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ parameters:
- tests/
excludePaths:
- tests/data/
- tests/monitoring-data/
phpVersion:
min: 80000
max: 80300
Expand Down
2 changes: 1 addition & 1 deletion source/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"ext-mbstring": "*",
"ext-openssl": "*",
"ext-sodium": "*",
"johnpbloch/wordpress": "7.0.1"
"johnpbloch/wordpress": "7.1"
},
"minimum-stability": "stable",
"config": {
Expand Down
58 changes: 54 additions & 4 deletions src/Visitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlock\Description;
use phpDocumentor\Reflection\DocBlock\Tags\Generic;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
use phpDocumentor\Reflection\DocBlock\Tags\Var_;
Expand Down Expand Up @@ -223,12 +224,11 @@ private function generateAdditionalTagsFromDoc(Doc $docComment): array
/** @var list<\phpDocumentor\Reflection\DocBlock\Tags\Return_> $returnTags */
$returnTags = $docblock->getTagsByName('return');

/** @var list<\phpDocumentor\Reflection\DocBlock\Tag> $phpStanReturnTags */
$phpStanReturnTags = $docblock->getTagsByName('phpstan-return');

/** @var list<\phpDocumentor\Reflection\DocBlock\Tags\Var_> $varTags */
$varTags = $docblock->getTagsByName('var');

$phpStanParamNames = $this->getVariableNamesFromTags($docblock->getTagsByName('phpstan-param'));

/** @var list<\PhpStubs\WordPress\Core\WordPressTag> $additions */
$additions = [];

Expand All @@ -237,6 +237,10 @@ private function generateAdditionalTagsFromDoc(Doc $docComment): array
continue;
}

if (in_array($paramTag->getVariableName(), $phpStanParamNames, true)) {
continue;
}

$addition = self::getAdditionFromParam($paramTag);

if (! ($addition instanceof WordPressTag)) {
Expand All @@ -246,7 +250,7 @@ private function generateAdditionalTagsFromDoc(Doc $docComment): array
$additions[] = $addition;
}

if (! count($phpStanReturnTags)) {
if (! count($docblock->getTagsByName('phpstan-return'))) {

@szepeviktor szepeviktor Aug 27, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnbillion Does this make your wish come true?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@szepeviktor this has been added in #467

foreach ($returnTags as $returnTag) {
$addition = self::getAdditionFromReturn($returnTag);

Expand All @@ -271,6 +275,29 @@ private function generateAdditionalTagsFromDoc(Doc $docComment): array
return $additions;
}

/**
* @param array<\phpDocumentor\Reflection\DocBlock\Tag> $tags
* @return list<string>
*/
private function getVariableNamesFromTags(array $tags): array
{
$names = [];

foreach ($tags as $tag) {
if (! ($tag instanceof Generic)) {
continue;
}

if (preg_match('#^(.+)\s+\$([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)#s', (string)$tag, $matches) !== 1) {
continue;
}

$names[] = $matches[2];
}

return $names;
}

private function addTags(string $name, Doc $docComment): ?Doc
{
$additions = $this->additionalTags[$name] ?? [];
Expand Down Expand Up @@ -329,11 +356,17 @@ static function (WordPressTag $addition): bool {
}
);

$phpStanParamNames = $this->getVariableNamesFromTags($docblock->getTagsByName('phpstan-param'));

foreach ($params as $param) {
if (! $param instanceof Param) {
continue;
}

if (in_array($param->getVariableName(), $phpStanParamNames, true)) {
continue;
}

$inherited = $this->getInheritedTagsForParam($param);

if (count($inherited) === 0) {
Expand Down Expand Up @@ -820,6 +853,23 @@ private function voidOrNever(Node $node): ?Type
return null;
}

$doc = $node->getDocComment();
if (! ($doc instanceof Doc)) {
return null;
}

$docblock = $this->docBlockFactory->create($doc->getText());

$returnTags = $docblock->getTagsByName('return');
if (count($returnTags) > 0) {
return null;
}

$phpStanReturnTags = $docblock->getTagsByName('phpstan-return');
if (count($phpStanReturnTags) > 0) {
return null;
}

$yields = $this->nodeFinder->findFirst(
$node,
static function (Node $node): bool {
Expand Down
14 changes: 14 additions & 0 deletions tests/Faker.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,18 @@ public static function union(...$types)
public static function intersection($type1, $type2)
{
}

/**
* Fakes a class string.
*
* @template T
* @param T $theClass
* @return (T is null ? class-string : class-string<T>)
*
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
*/
public static function classString($theClass = null): string
{
return self::class;
}
}
30 changes: 21 additions & 9 deletions tests/ParameterTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,10 @@ public function testRegisterWidget(): void
$this->analyse(
__DIR__ . '/data/param/register-widget.php',
[
['Parameter #1 $widget of function register_widget expects class-string<WP_Widget>|WP_Widget, PhpStubs\WordPress\Core\Tests\NoWidget given.', 21],
['Parameter #1 $widget of function register_widget expects class-string<WP_Widget>|WP_Widget, PhpStubs\WordPress\Core\Tests\NoWidget given.', 22],
['Parameter #1 $widget of function register_widget expects class-string<WP_Widget>|WP_Widget, string given.', 23],
['Parameter #1 $widget of function register_widget expects class-string<WP_Widget>|WP_Widget, string given.', 24],
['Parameter #1 $widget of function register_widget expects class-string<WP_Widget>|WP_Widget, WP_Post given.', 11],
['Parameter #1 $widget of function register_widget expects class-string<WP_Widget>|WP_Widget, WP_Post given.', 12],
['Parameter #1 $widget of function register_widget expects class-string<WP_Widget>|WP_Widget, class-string<WP_Post> given.', 13],
['Parameter #1 $widget of function register_widget expects class-string<WP_Widget>|WP_Widget, string given.', 14],
]
);
}
Expand All @@ -345,11 +345,8 @@ public function testWpdbGetRow(): void
$this->analyse(
__DIR__ . '/data/param/wpdb.php',
[
["Parameter #2 \$output of method wpdb::get_row() expects 'ARRAY_A'|'ARRAY_N'|'OBJECT', 'OBJECT_K' given.", 25],
["Parameter #2 \$output of method wpdb::get_row() expects 'ARRAY_A'|'ARRAY_N'|'OBJECT', string given.", 26],
["Parameter #2 \$output of method wpdb::get_row() expects 'ARRAY_A'|'ARRAY_N'|'OBJECT', int given.", 27],
['Parameter #3 $y of method wpdb::get_row() expects int<0, max>, -1 given.', 28],
['Parameter #3 $y of method wpdb::get_row() expects int<0, max>, int given.', 29],
['Parameter #3 $y of method wpdb::get_row() expects int<0, max>, -1 given.', 21],
['Parameter #3 $y of method wpdb::get_row() expects int<0, max>, int given.', 22],
]
);
}
Expand Down Expand Up @@ -423,6 +420,21 @@ public function testWpTriggerError(): void
);
}

public function testwpWidgetFactory(): void
{
$this->analyse(
__DIR__ . '/data/param/wp-widget-factory.php',
[
['Parameter #1 $widget of method WP_Widget_Factory::register() expects class-string<WP_Widget>|WP_Widget, WP_Post given.', 8],
['Parameter #1 $widget of method WP_Widget_Factory::register() expects class-string<WP_Widget>|WP_Widget, class-string<WP_Post> given.', 9],
['Parameter #1 $widget of method WP_Widget_Factory::register() expects class-string<WP_Widget>|WP_Widget, string given.', 10],
['Parameter #1 $widget of method WP_Widget_Factory::unregister() expects class-string<WP_Widget>|WP_Widget, WP_Post given.', 11],
['Parameter #1 $widget of method WP_Widget_Factory::unregister() expects class-string<WP_Widget>|WP_Widget, class-string<WP_Post> given.', 12],
['Parameter #1 $widget of method WP_Widget_Factory::unregister() expects class-string<WP_Widget>|WP_Widget, string given.', 13],
]
);
}

public function testWpUploadBits(): void
{
$this->analyse(
Expand Down
3 changes: 2 additions & 1 deletion tests/data/final-tag.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function update_callback($deprecated = 1): void
}

// No final tag
public function form($instance): void {
public function form($instance): string {
return '';
}
}
2 changes: 1 addition & 1 deletion tests/data/impure/wp-unique-id.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
use function PHPStan\Testing\assertType;

if (wp_unique_id() === '1') {
assertType('non-falsy-string&numeric-string', wp_unique_id());
assertType('lowercase-string&non-falsy-string&numeric-string', wp_unique_id());
}
2 changes: 1 addition & 1 deletion tests/data/impure/wp-unique-prefixed-id.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
use function PHPStan\Testing\assertType;

if (wp_unique_prefixed_id() === 'prefix1') {
assertType('non-falsy-string&numeric-string', wp_unique_prefixed_id());
assertType('lowercase-string&non-falsy-string&numeric-string', wp_unique_prefixed_id());
}
26 changes: 8 additions & 18 deletions tests/data/param/register-widget.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,18 @@

namespace PhpStubs\WordPress\Core\Tests;

use WP_Widget;
use function register_widget;

class MyWidget extends WP_Widget {
public function __construct(string $id_base, string $name)
{
parent::__construct($id_base, $name);
}
}

class NoWidget {}

// Incorrect
$noWidget = new NoWidget();
$noWidget = Faker::wpPost();
register_widget($noWidget);
register_widget(new NoWidget());
register_widget(NoWidget::class);
register_widget('\PhpStubs\WordPress\Core\Tests\NoWidget');
register_widget(Faker::wpPost());
register_widget(Faker::classString(Faker::wpPost()));
register_widget('\NoWidget');

// Correct
$widget = new MyWidget('my_widget', 'My Widget');
$widget = Faker::wpWidget();
register_widget($widget);
register_widget(new MyWidget('my_widget', 'My Widget'));
register_widget(MyWidget::class);
register_widget('\PhpStubs\WordPress\Core\Tests\MyWidget');
register_widget(Faker::wpWidget());
register_widget(Faker::classString(Faker::wpWidget()));
register_widget('\WP_Widget');
19 changes: 19 additions & 0 deletions tests/data/param/wp-widget-factory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace PhpStubs\WordPress\Core\Tests;

// Incorrect - not a WP_Widget subclass(-string)
Faker::wpWidgetFactory()->register(Faker::wpPost());
Faker::wpWidgetFactory()->register(Faker::classString(Faker::wpPost()));
Faker::wpWidgetFactory()->register('NoClassString');
Faker::wpWidgetFactory()->unregister(Faker::wpPost());
Faker::wpWidgetFactory()->unregister(Faker::classString(Faker::wpPost()));
Faker::wpWidgetFactory()->unregister('NoClassString');

// Correct
Faker::wpWidgetFactory()->register(Faker::wpWidget());
Faker::wpWidgetFactory()->register(Faker::classString(Faker::wpWidget()));
Faker::wpWidgetFactory()->unregister(Faker::wpWidget());
Faker::wpWidgetFactory()->unregister(Faker::classString(Faker::wpWidget()));
7 changes: 0 additions & 7 deletions tests/data/param/wpdb.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@
* Correct usage of $wpdb->get_row()
*/

$row = $wpdb->get_row(null);
$row = $wpdb->get_row(null, 'OBJECT');
$row = $wpdb->get_row(null, 'ARRAY_N');
$row = $wpdb->get_row(null, 'ARRAY_A');
$row = $wpdb->get_row(null, 'OBJECT', 0);
$row = $wpdb->get_row(null, 'OBJECT', 7);
$row = $wpdb->get_row(null, 'OBJECT', Faker::nonNegativeInt());
Expand All @@ -22,8 +18,5 @@
* Incorrect usage of $wpdb->get_row()
*/

$row = $wpdb->get_row(null, 'OBJECT_K');
$row = $wpdb->get_row(null, Faker::string());
$row = $wpdb->get_row(null, Faker::int());
$row = $wpdb->get_row(null, 'OBJECT', -1);
$row = $wpdb->get_row(null, 'OBJECT', Faker::int());
3 changes: 3 additions & 0 deletions tests/data/return/Faker.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,6 @@
assertType('WP_User', Faker::wpUser());
assertType('WP_Widget_Factory', Faker::wpWidgetFactory());
assertType('wpdb', Faker::wpdb());

// class-string
assertType('class-string<WP_Widget>', Faker::classString(Faker::wpWidget()));
3 changes: 0 additions & 3 deletions tests/data/return/block-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,11 @@

// Build CSS
assertType('array{css_classes: list<string>, inline_styles: string}', block_core_home_link_build_css_colors(Faker::array()));
assertType('array{css_classes: list<string>, inline_styles: string}', block_core_home_link_build_css_font_sizes(Faker::array()));
assertType('array{css_classes: list<string>, inline_styles: string, overlay_css_classes: list<string>, overlay_inline_styles: string}', block_core_navigation_build_css_colors(Faker::array()));
assertType('array{css_classes: list<string>, inline_styles: string}', block_core_navigation_build_css_font_sizes(Faker::array()));
assertType('array{css_classes: list<string>, inline_styles: string}', block_core_navigation_link_build_css_colors(Faker::array(), Faker::array()));
assertType('array{css_classes: list<string>, inline_styles: string}', block_core_navigation_link_build_css_font_sizes(Faker::array()));
assertType('array{css_classes: list<string>, inline_styles: string}', block_core_navigation_submenu_build_css_font_sizes(Faker::array()));
assertType('array{css_classes: list<string>, inline_styles: string, overlay_css_classes: list<string>, overlay_inline_styles: string}', block_core_page_list_build_css_colors(Faker::array(), Faker::array()));
assertType('array{css_classes: list<string>, inline_styles: string}', block_core_page_list_build_css_font_sizes(Faker::array()));

// Breadcrumbs
assertType('array{label: string, url?: string}', block_core_breadcrumbs_create_item(Faker::string(), Faker::bool()));
Expand Down
12 changes: 12 additions & 0 deletions tests/data/return/check-admin-referer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace PhpStubs\WordPress\Core\Tests;

use function check_admin_referer;
use function PHPStan\Testing\assertType;

assertType('1|2|false', check_admin_referer(-1, Faker::string()));
assertType('1|2', check_admin_referer(Faker::string(), Faker::string()));
assertType('1|2|false', check_admin_referer(Faker::int(), Faker::string()));
16 changes: 16 additions & 0 deletions tests/data/return/check-ajax-referer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace PhpStubs\WordPress\Core\Tests;

use function check_ajax_referer;
use function PHPStan\Testing\assertType;

$action = Faker::union(Faker::string(), Faker::int());
$queryArg = Faker::union(Faker::string(), Faker::bool());

assertType('1|2', check_ajax_referer($action, $queryArg));
assertType('1|2', check_ajax_referer($action, $queryArg, true));
assertType('1|2|false', check_ajax_referer($action, $queryArg, false));
assertType('1|2|false', check_ajax_referer($action, $queryArg, Faker::bool()));
2 changes: 1 addition & 1 deletion tests/data/return/get-posts.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
assertType('array<int, int|WP_Post>', get_posts(['fields' => $union]));

$union = Faker::union(Faker::string(), 'fields');
assertType('array<int, WP_Post>', get_posts([$union => '']));
assertType('array<int, int|WP_Post>', get_posts([$union => '']));

$union = Faker::union(Faker::string(), 'fields');
assertType('array<int, int|WP_Post>', get_posts([$union => 'ids']));
Expand Down
10 changes: 0 additions & 10 deletions tests/data/return/stripslashes.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,8 @@
namespace PhpStubs\WordPress\Core\Tests;

use function PHPStan\Testing\assertType;
use function stripslashes_deep;
use function stripslashes_from_strings_only;

assertType('null', stripslashes_deep(null));
assertType('bool', stripslashes_deep(Faker::bool()));
assertType('int', stripslashes_deep(Faker::int()));
assertType('float', stripslashes_deep(Faker::float()));
assertType('string', stripslashes_deep(Faker::string()));
assertType('array<mixed>', stripslashes_deep(Faker::array()));
assertType('resource', stripslashes_deep(Faker::resource()));
assertType('object', stripslashes_deep(Faker::object()));

assertType('null', stripslashes_from_strings_only(null));
assertType('true', stripslashes_from_strings_only(true));
assertType('false', stripslashes_from_strings_only(false));
Expand Down
Loading
Loading