Skip to content

Commit 265d06b

Browse files
authored
Add return type definitions for get_terms() (#50)
1 parent 7e5ed70 commit 265d06b

File tree

4 files changed

+254
-0
lines changed

4 files changed

+254
-0
lines changed

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ services:
1919
class: SzepeViktor\PHPStan\WordPress\StringOrArrayDynamicFunctionReturnTypeExtension
2020
tags:
2121
- phpstan.broker.dynamicFunctionReturnTypeExtension
22+
-
23+
class: SzepeViktor\PHPStan\WordPress\GetTermsDynamicFunctionReturnTypeExtension
24+
tags:
25+
- phpstan.broker.dynamicFunctionReturnTypeExtension
2226
-
2327
class: SzepeViktor\PHPStan\WordPress\GetPostDynamicFunctionReturnTypeExtension
2428
tags:
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php
2+
3+
//phpcs:disable SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter
4+
5+
/**
6+
* Set return type of get_terms() and related functions.
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace SzepeViktor\PHPStan\WordPress;
12+
13+
use PhpParser\Node\Expr\FuncCall;
14+
use PHPStan\Analyser\Scope;
15+
use PHPStan\Reflection\FunctionReflection;
16+
use PHPStan\Type\Type;
17+
use PHPStan\Type\ArrayType;
18+
use PHPStan\Type\IntegerType;
19+
use PHPStan\Type\ObjectType;
20+
use PHPStan\Type\StringType;
21+
use PHPStan\Type\Constant\ConstantArrayType;
22+
use PHPStan\Type\Constant\ConstantStringType;
23+
use PHPStan\Type\ConstantScalarType;
24+
use PHPStan\Type\TypeCombinator;
25+
26+
class GetTermsDynamicFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
27+
{
28+
private const SUPPORTED_FUNCTIONS = [
29+
'get_tags' => 0,
30+
'get_terms' => 0,
31+
'wp_get_object_terms' => 2,
32+
'wp_get_post_categories' => 1,
33+
'wp_get_post_tags' => 1,
34+
'wp_get_post_terms' => 2,
35+
];
36+
37+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
38+
{
39+
return array_key_exists($functionReflection->getName(), self::SUPPORTED_FUNCTIONS);
40+
}
41+
42+
/**
43+
* @see https://developer.wordpress.org/reference/functions/get_terms/
44+
* @see https://developer.wordpress.org/reference/classes/wp_term_query/__construct/
45+
*/
46+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
47+
{
48+
$name = $functionReflection->getName();
49+
$argsParameterPosition = self::SUPPORTED_FUNCTIONS[$name] ?? null;
50+
51+
if ($argsParameterPosition === null) {
52+
throw new \PHPStan\ShouldNotHappenException(
53+
sprintf(
54+
'Could not detect parameter position for function %s()',
55+
$name
56+
)
57+
);
58+
}
59+
60+
// Called without arguments
61+
if (! isset($functionCall->getArgs()[$argsParameterPosition])) {
62+
return self::termsType();
63+
}
64+
65+
$argument = $scope->getType($functionCall->getArgs()[$argsParameterPosition]->value);
66+
67+
if (!($argument instanceof ConstantArrayType)) {
68+
// Without constant array argument return default return type
69+
return self::defaultType();
70+
}
71+
72+
$args = self::getArgs($argument);
73+
74+
if ($args === null) {
75+
return self::defaultType();
76+
}
77+
78+
if (isset($args['count']) && $args['count'] === true) {
79+
return self::countType();
80+
}
81+
82+
if (! isset($args['fields'], $args['count']) || ! is_string($args['fields'])) {
83+
return self::defaultType();
84+
}
85+
86+
return self::deduceTypeFromFields($args['fields']);
87+
}
88+
89+
protected static function deduceTypeFromFields(string $fields): Type
90+
{
91+
switch ($fields) {
92+
case 'count':
93+
return self::countType();
94+
case 'names':
95+
case 'slugs':
96+
case 'id=>name':
97+
case 'id=>slug':
98+
return self::slugsType();
99+
case 'ids':
100+
case 'tt_ids':
101+
return self::idsType();
102+
case 'id=>parent':
103+
return self::parentsType();
104+
case 'all':
105+
case 'all_with_object_id':
106+
default:
107+
return self::termsType();
108+
}
109+
}
110+
111+
/**
112+
* @return array<string, mixed>
113+
*/
114+
protected static function getArgs(ConstantArrayType $argument): ?array
115+
{
116+
$args = [
117+
'fields' => 'all',
118+
'count' => false,
119+
];
120+
121+
foreach ($argument->getKeyTypes() as $index => $key) {
122+
if (! $key instanceof ConstantStringType) {
123+
return null;
124+
}
125+
126+
unset($args[$key->getValue()]);
127+
$fieldsType = $argument->getValueTypes()[$index];
128+
if (!($fieldsType instanceof ConstantScalarType)) {
129+
continue;
130+
}
131+
132+
$args[$key->getValue()] = $fieldsType->getValue();
133+
}
134+
135+
return $args;
136+
}
137+
138+
protected static function countType(): Type
139+
{
140+
return TypeCombinator::union(
141+
new StringType(),
142+
new ObjectType('WP_Error')
143+
);
144+
}
145+
146+
protected static function slugsType(): Type
147+
{
148+
return TypeCombinator::union(
149+
new ArrayType(new IntegerType(), new StringType()),
150+
new ObjectType('WP_Error')
151+
);
152+
}
153+
154+
protected static function idsType(): Type
155+
{
156+
return TypeCombinator::union(
157+
new ArrayType(new IntegerType(), new IntegerType()),
158+
new ObjectType('WP_Error')
159+
);
160+
}
161+
162+
protected static function parentsType(): Type
163+
{
164+
return TypeCombinator::union(
165+
new ArrayType(new IntegerType(), new StringType()),
166+
new ObjectType('WP_Error')
167+
);
168+
}
169+
170+
protected static function termsType(): Type
171+
{
172+
return TypeCombinator::union(
173+
new ArrayType(new IntegerType(), new ObjectType('WP_Term')),
174+
new ObjectType('WP_Error')
175+
);
176+
}
177+
178+
protected static function defaultType(): Type
179+
{
180+
return TypeCombinator::union(
181+
self::termsType(),
182+
self::idsType(),
183+
self::slugsType(),
184+
self::countType()
185+
);
186+
}
187+
}

tests/DynamicReturnTypeExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public function dataFileAsserts(): iterable
1818
yield from $this->gatherAssertTypes(__DIR__ . '/data/get_comment.php');
1919
yield from $this->gatherAssertTypes(__DIR__ . '/data/get_object_taxonomies.php');
2020
yield from $this->gatherAssertTypes(__DIR__ . '/data/get_post.php');
21+
yield from $this->gatherAssertTypes(__DIR__ . '/data/get_terms.php');
2122
yield from $this->gatherAssertTypes(__DIR__ . '/data/mysql2date.php');
2223
yield from $this->gatherAssertTypes(__DIR__ . '/data/shortcode_atts.php');
2324
yield from $this->gatherAssertTypes(__DIR__ . '/data/term_exists.php');

tests/data/get_terms.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SzepeViktor\PHPStan\WordPress\Tests;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
$fields = $_GET['fields'] ?? 'all';
10+
$key = $_GET['key'] ?? 'fields';
11+
$count = ! empty( $_GET['count'] );
12+
13+
// Default argument values
14+
assertType('array<int, WP_Term>|WP_Error', get_terms());
15+
assertType('array<int, WP_Term>|WP_Error', get_terms([]));
16+
17+
// Unknown values
18+
assertType('array<int, int|string|WP_Term>|string|WP_Error', get_terms(['fields'=>$fields]));
19+
assertType('array<int, int|string|WP_Term>|string|WP_Error', get_terms(['foo'=>'bar','fields'=>$fields]));
20+
assertType('array<int, int|string|WP_Term>|string|WP_Error', get_terms(['count'=>$count]));
21+
assertType('array<int, int|string|WP_Term>|string|WP_Error', get_terms(['count'=>$count,'fields'=>'ids']));
22+
assertType('array<int, int|string|WP_Term>|string|WP_Error', get_terms(['fields'=>$fields,'count'=>false]));
23+
24+
// Unknown keys
25+
assertType('array<int, int|string|WP_Term>|string|WP_Error', get_terms([$key=>'all']));
26+
assertType('array<int, int|string|WP_Term>|string|WP_Error', get_terms(['foo'=>'bar',$key=>'all']));
27+
28+
// Requesting a count
29+
assertType('string|WP_Error', get_terms(['fields'=>'count']));
30+
assertType('string|WP_Error', get_terms(['foo'=>'bar','fields'=>'count']));
31+
assertType('string|WP_Error', get_terms(['count'=>true]));
32+
assertType('string|WP_Error', get_terms(['foo'=>'bar','count'=>true]));
33+
assertType('string|WP_Error', get_terms(['fields'=>'ids','count'=>true]));
34+
assertType('string|WP_Error', get_terms(['fields'=>$fields,'count'=>true]));
35+
36+
// Requesting names or slugs
37+
assertType('array<int, string>|WP_Error', get_terms(['fields'=>'names']));
38+
assertType('array<int, string>|WP_Error', get_terms(['fields'=>'slugs']));
39+
assertType('array<int, string>|WP_Error', get_terms(['fields'=>'id=>name']));
40+
assertType('array<int, string>|WP_Error', get_terms(['fields'=>'id=>slug']));
41+
42+
// Requesting IDs
43+
assertType('array<int, int>|WP_Error', get_terms(['fields'=>'ids']));
44+
assertType('array<int, int>|WP_Error', get_terms(['fields'=>'tt_ids']));
45+
46+
// Requesting parent IDs (numeric strings)
47+
assertType('array<int, string>|WP_Error', get_terms(['fields'=>'id=>parent']));
48+
49+
// Requesting objects
50+
assertType('array<int, WP_Term>|WP_Error', get_terms(['fields'=>'all']));
51+
assertType('array<int, WP_Term>|WP_Error', get_terms(['fields'=>'all_with_object_id']));
52+
assertType('array<int, WP_Term>|WP_Error', get_terms(['fields'=>'foo']));
53+
54+
// Wrapper functions
55+
assertType('string|WP_Error', wp_get_object_terms(123, 'category', ['fields'=>'count']));
56+
assertType('string|WP_Error', wp_get_post_categories(123, ['fields'=>'count']));
57+
assertType('string|WP_Error', wp_get_post_tags(123, ['fields'=>'count']));
58+
assertType('string|WP_Error', wp_get_post_terms(123, 'category', ['fields'=>'count']));
59+
assertType('array<int, WP_Term>|WP_Error', wp_get_object_terms(123, 'category'));
60+
assertType('array<int, WP_Term>|WP_Error', wp_get_post_categories(123));
61+
assertType('array<int, WP_Term>|WP_Error', wp_get_post_tags(123));
62+
assertType('array<int, WP_Term>|WP_Error', wp_get_post_terms(123, 'category'));

0 commit comments

Comments
 (0)