Skip to content

Commit 7efb688

Browse files
authored
Add WpParseUrlFunctionDynamicReturnTypeExtension (#110)
1 parent 01b3175 commit 7efb688

File tree

4 files changed

+232
-0
lines changed

4 files changed

+232
-0
lines changed

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ services:
7575
class: SzepeViktor\PHPStan\WordPress\TermExistsDynamicFunctionReturnTypeExtension
7676
tags:
7777
- phpstan.broker.dynamicFunctionReturnTypeExtension
78+
-
79+
class: SzepeViktor\PHPStan\WordPress\WpParseUrlFunctionDynamicReturnTypeExtension
80+
tags:
81+
- phpstan.broker.dynamicFunctionReturnTypeExtension
7882
-
7983
class: SzepeViktor\PHPStan\WordPress\HookDocsVisitor
8084
tags:
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
3+
/**
4+
* Set return type of wp_parse_url().
5+
*
6+
* Based on ParseUrlFunctionDynamicReturnTypeExtension in PHPStan itself.
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\Reflection\ParametersAcceptorSelector;
17+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
18+
use PHPStan\Type\Constant\ConstantBooleanType;
19+
use PHPStan\Type\Constant\ConstantIntegerType;
20+
use PHPStan\Type\Constant\ConstantStringType;
21+
use PHPStan\Type\ConstantType;
22+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
23+
use PHPStan\Type\IntegerType;
24+
use PHPStan\Type\NullType;
25+
use PHPStan\Type\StringType;
26+
use PHPStan\Type\Type;
27+
use PHPStan\Type\TypeCombinator;
28+
29+
use function count;
30+
use function parse_url;
31+
32+
use const PHP_URL_FRAGMENT;
33+
use const PHP_URL_HOST;
34+
use const PHP_URL_PASS;
35+
use const PHP_URL_PATH;
36+
use const PHP_URL_PORT;
37+
use const PHP_URL_QUERY;
38+
use const PHP_URL_SCHEME;
39+
use const PHP_URL_USER;
40+
41+
final class WpParseUrlFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
42+
{
43+
44+
/** @var array<int,Type>|null */
45+
private $componentTypesPairedConstants = null;
46+
47+
/** @var array<string,Type>|null */
48+
private $componentTypesPairedStrings = null;
49+
50+
/** @var Type|null */
51+
private $allComponentsTogetherType = null;
52+
53+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
54+
{
55+
return $functionReflection->getName() === 'wp_parse_url';
56+
}
57+
58+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
59+
{
60+
if (count($functionCall->getArgs()) < 1) {
61+
return ParametersAcceptorSelector::selectSingle(
62+
$functionReflection->getVariants()
63+
)->getReturnType();
64+
}
65+
66+
$this->cacheReturnTypes();
67+
68+
$urlType = $scope->getType($functionCall->getArgs()[0]->value);
69+
if (count($functionCall->getArgs()) > 1) {
70+
$componentType = $scope->getType($functionCall->getArgs()[1]->value);
71+
72+
if (!$componentType instanceof ConstantType) {
73+
return $this->createAllComponentsReturnType();
74+
}
75+
76+
$componentType = $componentType->toInteger();
77+
78+
if (!$componentType instanceof ConstantIntegerType) {
79+
throw new \PHPStan\ShouldNotHappenException();
80+
}
81+
} else {
82+
$componentType = new ConstantIntegerType(-1);
83+
}
84+
85+
if ($urlType instanceof ConstantStringType) {
86+
try {
87+
$result = @parse_url($urlType->getValue(), $componentType->getValue());
88+
} catch (\ValueError $e) {
89+
return new ConstantBooleanType(false);
90+
}
91+
92+
return $scope->getTypeFromValue($result);
93+
}
94+
95+
if ($componentType->getValue() === -1) {
96+
return $this->createAllComponentsReturnType();
97+
}
98+
99+
return $this->componentTypesPairedConstants[$componentType->getValue()] ?? new ConstantBooleanType(false);
100+
}
101+
102+
private function createAllComponentsReturnType(): Type
103+
{
104+
if ($this->allComponentsTogetherType === null) {
105+
$returnTypes = [
106+
new ConstantBooleanType(false),
107+
];
108+
109+
$builder = ConstantArrayTypeBuilder::createEmpty();
110+
111+
if ($this->componentTypesPairedStrings === null) {
112+
throw new \PHPStan\ShouldNotHappenException();
113+
}
114+
115+
foreach ($this->componentTypesPairedStrings as $componentName => $componentValueType) {
116+
$builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true);
117+
}
118+
119+
$returnTypes[] = $builder->getArray();
120+
121+
$this->allComponentsTogetherType = TypeCombinator::union(...$returnTypes);
122+
}
123+
124+
return $this->allComponentsTogetherType;
125+
}
126+
127+
private function cacheReturnTypes(): void
128+
{
129+
if ($this->componentTypesPairedConstants !== null) {
130+
return;
131+
}
132+
133+
$string = new StringType();
134+
$integer = new IntegerType();
135+
$false = new ConstantBooleanType(false);
136+
$null = new NullType();
137+
138+
$stringOrFalseOrNull = TypeCombinator::union($string, $false, $null);
139+
$integerOrFalseOrNull = TypeCombinator::union($integer, $false, $null);
140+
141+
$this->componentTypesPairedConstants = [
142+
PHP_URL_SCHEME => $stringOrFalseOrNull,
143+
PHP_URL_HOST => $stringOrFalseOrNull,
144+
PHP_URL_PORT => $integerOrFalseOrNull,
145+
PHP_URL_USER => $stringOrFalseOrNull,
146+
PHP_URL_PASS => $stringOrFalseOrNull,
147+
PHP_URL_PATH => $stringOrFalseOrNull,
148+
PHP_URL_QUERY => $stringOrFalseOrNull,
149+
PHP_URL_FRAGMENT => $stringOrFalseOrNull,
150+
];
151+
152+
$this->componentTypesPairedStrings = [
153+
'scheme' => $string,
154+
'host' => $string,
155+
'port' => $integer,
156+
'user' => $string,
157+
'pass' => $string,
158+
'path' => $string,
159+
'query' => $string,
160+
'fragment' => $string,
161+
];
162+
}
163+
164+
}

tests/DynamicReturnTypeExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public function dataFileAsserts(): iterable
2626
yield from $this->gatherAssertTypes(__DIR__ . '/data/shortcode_atts.php');
2727
yield from $this->gatherAssertTypes(__DIR__ . '/data/term_exists.php');
2828
yield from $this->gatherAssertTypes(__DIR__ . '/data/wp_error_parameter.php');
29+
yield from $this->gatherAssertTypes(__DIR__ . '/data/wp_parse_url.php');
2930
}
3031

3132
/**

tests/data/wp_parse_url.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/**
4+
* Test data for WpParseUrlFunctionDynamicReturnTypeExtension.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace SzepeViktor\PHPStan\WordPress\Tests;
10+
11+
use function PHPStan\Testing\assertType;
12+
use function wp_clear_scheduled_hook;
13+
use function wp_insert_attachment;
14+
use function wp_insert_category;
15+
use function wp_insert_link;
16+
use function wp_insert_post;
17+
use function wp_reschedule_event;
18+
use function wp_schedule_event;
19+
use function wp_schedule_single_event;
20+
use function wp_set_comment_status;
21+
use function wp_unschedule_event;
22+
use function wp_unschedule_hook;
23+
use function wp_update_comment;
24+
use function wp_update_post;
25+
26+
/** @var int $integer */
27+
$integer = doFoo();
28+
29+
/** @var string $string */
30+
$string = doFoo();
31+
32+
/**
33+
* wp_parse_url()
34+
*/
35+
$value = wp_parse_url();
36+
assertType('mixed', $value);
37+
38+
$value = wp_parse_url('http://abc.def');
39+
assertType("array{scheme: 'http', host: 'abc.def'}", $value);
40+
41+
$value = wp_parse_url('http://def.abc', -1);
42+
assertType("array{scheme: 'http', host: 'def.abc'}", $value);
43+
44+
$value = wp_parse_url('http://def.abc', $integer);
45+
assertType('array{scheme?: string, host?: string, port?: int, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $value);
46+
47+
$value = wp_parse_url('http://def.abc', PHP_URL_FRAGMENT);
48+
assertType('null', $value);
49+
50+
$value = wp_parse_url('http://def.abc#this-is-fragment', PHP_URL_FRAGMENT);
51+
assertType("'this-is-fragment'", $value);
52+
53+
$value = wp_parse_url('http://def.abc#this-is-fragment', 9999);
54+
assertType('false', $value);
55+
56+
$value = wp_parse_url($string, 9999);
57+
assertType('false', $value);
58+
59+
$value = wp_parse_url($string, PHP_URL_PORT);
60+
assertType('int|false|null', $value);
61+
62+
$value = wp_parse_url($string);
63+
assertType('array{scheme?: string, host?: string, port?: int, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $value);

0 commit comments

Comments
 (0)