Skip to content

Commit 4b8e604

Browse files
authored
Add dynamic return type extension for Options::get() (#9)
1 parent e60578f commit 4b8e604

9 files changed

+222
-10
lines changed

Diff for: composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"type": "library",
77
"require": {
88
"szepeviktor/phpstan-wordpress": "^1.0",
9-
"wpsyntex/polylang-stubs": "*"
9+
"wpsyntex/polylang-stubs": "dev-master"
1010
},
1111
"require-dev": {
1212
"phpunit/phpunit": "^7 || ^9"

Diff for: extension.neon

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ services:
1515
class: WPSyntex\Polylang\PHPStan\PLLModelGetLanguagesListDynamicMethodReturnTypeExtension
1616
tags:
1717
- phpstan.broker.dynamicMethodReturnTypeExtension
18+
-
19+
class: WPSyntex\Polylang\PHPStan\OptionsGetDynamicMethodReturnTypeExtension
20+
tags:
21+
- phpstan.broker.dynamicMethodReturnTypeExtension
1822
includes:
1923
- ../../szepeviktor/phpstan-wordpress/extension.neon
2024
parameters:

Diff for: src/LanguageReturnTypeExtension.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
class LanguageReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension {
1515
public function isFunctionSupported( FunctionReflection $functionReflection ) : bool {
16-
return in_array( $functionReflection->getName(), array( 'pll_current_language', 'pll_default_language' ), true );
16+
return in_array( $functionReflection->getName(), [ 'pll_current_language', 'pll_default_language' ], true );
1717
}
1818

1919
public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope ) : Type {

Diff for: src/OptionsGetDynamicMethodReturnTypeExtension.php

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php
2+
/**
3+
* Dynamic return type for `WP_Syntex\Polylang\Options\Options->get()`.
4+
*/
5+
6+
declare(strict_types=1);
7+
8+
namespace WPSyntex\Polylang\PHPStan;
9+
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Reflection\MethodReflection;
13+
use PHPStan\Type\Accessory\AccessoryArrayListType;
14+
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
15+
use PHPStan\Type\ArrayType;
16+
use PHPStan\Type\BooleanType;
17+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
18+
use PHPStan\Type\IntegerRangeType;
19+
use PHPStan\Type\IntegerType;
20+
use PHPStan\Type\IntersectionType;
21+
use PHPStan\Type\NullType;
22+
use PHPStan\Type\StringType;
23+
use PHPStan\Type\Type;
24+
use PHPStan\Type\TypeCombinator;
25+
26+
class OptionsGetDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension {
27+
public function getClass(): string {
28+
return \WP_Syntex\Polylang\Options\Options::class;
29+
}
30+
31+
public function isMethodSupported( MethodReflection $methodReflection ): bool {
32+
return in_array( $methodReflection->getName(), [ 'get', 'reset', 'offsetGet' ], true );
33+
}
34+
35+
public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope ): ?Type {
36+
if ( count( $methodCall->getArgs() ) === 0 ) {
37+
return null;
38+
}
39+
40+
$argumentType = $scope->getType( $methodCall->getArgs()[0]->value );
41+
42+
// When called with a type that isn't a constant string, return default return type.
43+
if ( count( $argumentType->getConstantStrings() ) === 0 ) {
44+
return null;
45+
}
46+
47+
// Called with a constant string type.
48+
$returnType = [];
49+
50+
foreach ( $argumentType->getConstantStrings() as $constantString ) {
51+
switch ( $constantString->getValue() ) {
52+
case 'browser':
53+
case 'hide_default':
54+
case 'media_support':
55+
case 'redirect_lang':
56+
case 'rewrite':
57+
$returnType[] = new BooleanType();
58+
break;
59+
60+
case 'default_lang':
61+
case 'previous_version':
62+
case 'version':
63+
$returnType[] = new StringType();
64+
break;
65+
66+
case 'domains':
67+
$returnType[] = new ArrayType(
68+
$this->getNonFalsyStringType(),
69+
new StringType()
70+
);
71+
break;
72+
73+
case 'language_taxonomies':
74+
case 'post_types':
75+
case 'sync':
76+
case 'taxonomies':
77+
$returnType[] = AccessoryArrayListType::intersectWith(
78+
new ArrayType(
79+
new IntegerType(),
80+
$this->getNonFalsyStringType()
81+
)
82+
);
83+
break;
84+
85+
case 'nav_menus':
86+
$returnType[] = new ArrayType(
87+
$this->getNonFalsyStringType(),
88+
new ArrayType(
89+
$this->getNonFalsyStringType(),
90+
new ArrayType(
91+
$this->getNonFalsyStringType(),
92+
IntegerRangeType::fromInterval( 0, \PHP_INT_MAX )
93+
)
94+
)
95+
);
96+
break;
97+
98+
case 'first_activation':
99+
$returnType[] = IntegerRangeType::fromInterval( 0, \PHP_INT_MAX );
100+
break;
101+
102+
case 'force_lang':
103+
$returnType[] = IntegerRangeType::fromInterval( 0, 3 );
104+
break;
105+
106+
default:
107+
$returnType[] = $this->getDefaultReturnType( $constantString->getValue() );
108+
}
109+
}
110+
111+
return TypeCombinator::union( ...$returnType );
112+
}
113+
114+
protected function getNonFalsyStringType(): Type {
115+
return new IntersectionType(
116+
[
117+
new StringType(),
118+
new AccessoryNonFalsyStringType(),
119+
]
120+
);
121+
}
122+
123+
/**
124+
* Returns the type to return (!) when the option name is unknown.
125+
* Currently handles Polylang Pro's options.
126+
* Can be overwritten to handle more option names.
127+
*
128+
* @param string $option_name Option name.
129+
* @return Type
130+
*/
131+
protected function getDefaultReturnType( string $option_name ): Type {
132+
if ( ! defined( 'POLYLANG_PRO_PHPSTAN' ) || ! POLYLANG_PRO_PHPSTAN ) { // Constant specific to PHPStan in Polylang Pro.
133+
return new NullType();
134+
}
135+
136+
switch ( $option_name ) {
137+
case 'media':
138+
return new ArrayType(
139+
$this->getNonFalsyStringType(),
140+
new BooleanType()
141+
);
142+
143+
case 'machine_translation_enabled':
144+
return new BooleanType();
145+
146+
case 'machine_translation_services':
147+
return new ArrayType(
148+
$this->getNonFalsyStringType(),
149+
new ArrayType(
150+
$this->getNonFalsyStringType(),
151+
new StringType()
152+
)
153+
);
154+
155+
default:
156+
return new NullType();
157+
}
158+
}
159+
}

Diff for: tests/DynamicReturnTypeExtensionTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public function dataFileAsserts(): iterable
1515
yield from $this->gatherAssertTypes(__DIR__ . '/data/the_languages.php');
1616
yield from $this->gatherAssertTypes(__DIR__ . '/data/pll_the_languages.php');
1717
yield from $this->gatherAssertTypes(__DIR__ . '/data/get_languages_list.php');
18+
yield from $this->gatherAssertTypes(__DIR__ . '/data/options_get.php');
1819
}
1920

2021
/**

Diff for: tests/data/get_languages_list.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@
2626
// With 'fields' key set in a variable.
2727
assertType('array<int, string>', $model->get_languages_list($args));
2828

29-
// With a variable containing unkown data.
29+
// With a variable containing unknown data.
3030
assertType('array<int, mixed>', $model->get_languages_list($array));
3131

3232
// With array_merge() result passed as parameter.
3333
assertType('array<int, mixed>', $model->get_languages_list(array_merge($array, ['fields' => 'slug'])));
3434

35-
// With 'fields' key set on top of variable containing unkown data.
35+
// With 'fields' key set on top of variable containing unknown data.
3636
$array['fields'] = 'slug';
3737
assertType('array<int, string>', $model->get_languages_list($array));

Diff for: tests/data/options_get.php

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WPSyntex\Polylang\PHPStan\Tests;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/** @var \WP_Syntex\Polylang\Options\Options */
10+
$options = $options;
11+
12+
// With an unknown option.
13+
assertType('null', $options->get('foo'));
14+
15+
// With a Pro option in PLL Free.
16+
assertType('null', $options->get('media'));
17+
18+
// With a boolean type option.
19+
foreach ( [ 'browser', 'hide_default', 'media_support', 'redirect_lang', 'rewrite' ] as $option_name ) {
20+
assertType('bool', $options->get($option_name));
21+
}
22+
23+
// With a string type option.
24+
foreach ( [ 'default_lang', 'previous_version', 'version' ] as $option_name ) {
25+
assertType('string', $options->get($option_name));
26+
}
27+
28+
// Domains.
29+
assertType('array<non-falsy-string, string>', $options->get('domains'));
30+
31+
// With a list type option.
32+
foreach ( [ 'language_taxonomies', 'post_types', 'sync', 'taxonomies' ] as $option_name ) {
33+
assertType('array<int, non-falsy-string>', $options->get($option_name));
34+
}
35+
36+
// With the nav menus option.
37+
assertType('array<non-falsy-string, array<non-falsy-string, array<non-falsy-string, int<0, 9223372036854775807>>>>', $options->get('nav_menus'));
38+
39+
// With the first activation option.
40+
assertType('int<0, 9223372036854775807>', $options->get('first_activation'));
41+
42+
// With the force lang option.
43+
assertType('int<0, 3>', $options->get('force_lang'));
44+
45+
//define( 'POLYLANG_PRO', true );
46+
47+
// With a Pro option in PLL Pro.
48+
//assertType('array<non-falsy-string, bool>', $options->get('media'));

Diff for: tests/data/pll_the_languages.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@
3535
// Default attributes.
3636
assertType('string', pll_the_languages());
3737

38-
// Unkown attributes.
38+
// Unknown attributes.
3939
assertType('array<string, mixed>|string', pll_the_languages($array));
4040

41-
// With unkown variable merged.
42-
$args = array_merge( array( 'raw' => 1 ), $options );
41+
// With unknown variable merged.
42+
$args = array_merge( [ 'raw' => 1 ], $options );
4343
assertType('array<string, mixed>|string', pll_the_languages($args));
4444

4545
// With raw attribute set to true outside.

Diff for: tests/data/the_languages.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@
3838
// Default attributes.
3939
assertType('string', $switcher->the_languages($link));
4040

41-
// Unkown attributes.
41+
// Unknown attributes.
4242
assertType('array<string, mixed>|string', $switcher->the_languages($link, $array));
4343

44-
// With unkown variable merged.
45-
$args = array_merge( $array, array( 'raw' => 1 ) );
44+
// With unknown variable merged.
45+
$args = array_merge( $array, [ 'raw' => 1 ] );
4646
assertType('array<string, mixed>|string', $switcher->the_languages($link, $args));
4747

4848
// With raw attribute set to true outside.

0 commit comments

Comments
 (0)