Skip to content

Commit 69ab173

Browse files
committed
Add some extensions
1 parent 4865fa1 commit 69ab173

11 files changed

+309
-5
lines changed

composer.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515
"php-parallel-lint/php-console-highlighter": "^1.0",
1616
"php-parallel-lint/php-parallel-lint": "^1.3.1",
1717
"phpcompatibility/php-compatibility": "dev-develop",
18-
"phpstan/extension-installer": "^1.4.3",
18+
"phpstan/extension-installer": "^1.4",
1919
"phpstan/phpstan": "^1.12.26",
20+
"phpstan/phpstan-deprecation-rules": "^1.2",
21+
"phpstan/phpstan-phpunit": "^1.4",
22+
"phpstan/phpstan-strict-rules": "^1.6",
23+
"swissspidy/phpstan-no-private": "^0.2.1",
2024
"szepeviktor/phpstan-wordpress": "^v1.3.5",
2125
"wp-cli/config-command": "^1 || ^2",
2226
"wp-cli/core-command": "^1 || ^2",
@@ -41,6 +45,11 @@
4145
"branch-alias": {
4246
"dev-main": "4.0.x-dev"
4347
},
48+
"phpstan": {
49+
"includes": [
50+
"extension.neon"
51+
]
52+
},
4453
"readme": {
4554
"sections": [
4655
"Using",
@@ -58,6 +67,11 @@
5867
"WP_CLI\\Tests\\": "src"
5968
}
6069
},
70+
"autoload-dev": {
71+
"psr-4": {
72+
"WP_CLI\\Tests\\Tests\\": "tests/tests"
73+
}
74+
},
6175
"minimum-stability": "dev",
6276
"prefer-stable": true,
6377
"bin": [

extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
services:
2+
-
3+
class: WP_CLI\Tests\PHPStan\ParseUrlFunctionDynamicReturnTypeExtension
4+
tags:
5+
- phpstan.broker.dynamicFunctionReturnTypeExtension

phpcs.xml.dist

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@
6161

6262
<exclude-pattern>tests/phpstan/scan-files.php</exclude-pattern>
6363

64+
<rule ref="WordPress.NamingConventions">
65+
<exclude-pattern>tests/data/*</exclude-pattern>
66+
<exclude-pattern>src/PHPStan/*</exclude-pattern>
67+
<exclude-pattern>tests/tests/PHPStan/*</exclude-pattern>
68+
</rule>
69+
70+
<rule ref="WordPress.WP.AlternativeFunctions.parse_url_parse_url">
71+
<exclude-pattern>tests/data/*</exclude-pattern>
72+
</rule>
73+
6474
<!-- This is a procedural stand-alone file that is never loaded in a WordPress context,
6575
so this file does not have to comply with WP naming conventions. -->
6676
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">

phpstan.neon.dist

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
includes:
2+
- extension.neon
13
parameters:
24
level: 6
35
paths:
46
- src
57
- tests
8+
excludePaths:
9+
- tests/data
610
scanDirectories:
711
- vendor/wp-cli/wp-cli
812
- vendor/phpunit/php-code-coverage
@@ -11,9 +15,25 @@ parameters:
1115
- tests/phpstan/scan-files.php
1216
treatPhpDocTypesAsCertain: false
1317
dynamicConstantNames:
14-
- WP_DEBUG
15-
- WP_DEBUG_LOG
16-
- WP_DEBUG_DISPLAY
18+
- WP_DEBUG
19+
- WP_DEBUG_LOG
20+
- WP_DEBUG_DISPLAY
1721
ignoreErrors:
1822
# Needs fixing in WP-CLI.
1923
- message: '#Parameter \#1 \$cmd of function WP_CLI\\Utils\\esc_cmd expects array<string>#'
24+
- message: '#Dynamic call to static method#'
25+
path: 'tests/tests'
26+
strictRules:
27+
disallowedLooseComparison: false
28+
booleansInConditions: false
29+
uselessCast: false
30+
requireParentConstructorCall: false
31+
disallowedConstructs: false
32+
overwriteVariablesWithLoop: false
33+
closureUsesThis: false
34+
matchingInheritedMethodNames: false
35+
numericOperandsInArithmeticOperators: false
36+
strictCalls: true
37+
switchConditionsMatchingType: false
38+
noVariableVariables: false
39+
strictArrayFilter: false

src/Context/FeatureContext.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
/**
3131
* Features context.
32+
*
33+
* @phpstan-ignore class.implementsDeprecatedInterface
3234
*/
3335
class FeatureContext implements SnippetAcceptingContext {
3436

src/Context/GivenStepDefinitions.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public function given_a_specific_directory( $empty_or_nonexistent, $dir ): void
6666
);
6767
}
6868

69-
$this->remove_dir( $dir );
69+
self::remove_dir( $dir );
7070
if ( 'empty' === $empty_or_nonexistent ) {
7171
mkdir( $dir, 0777, true /*recursive*/ );
7272
}

src/Context/ThenStepDefinitions.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ public function then_an_email_should_be_sent( $expected ): void {
566566
* @param int $return_code Expected HTTP status code.
567567
*/
568568
public function then_the_http_status_code_should_be( $return_code ): void {
569+
// @phpstan-ignore staticMethod.deprecatedClass
569570
$response = Requests::request( 'http://localhost:8080' );
570571
$this->assert_equals( $return_code, $response->status_code );
571572
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
/**
4+
* Set return type of \WP_CLI\Utils\parse_url().
5+
*
6+
* Based on ParseUrlFunctionDynamicReturnTypeExtension in PHPStan itself
7+
* and WpParseUrlFunctionDynamicReturnTypeExtension in the PHPStan WordPress extension.
8+
*
9+
* phpcs:disable WordPress.WP.AlternativeFunctions.parse_url_parse_url
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace WP_CLI\Tests\PHPStan;
15+
16+
use PhpParser\Node\Expr\FuncCall;
17+
use PHPStan\Analyser\Scope;
18+
use PHPStan\Reflection\FunctionReflection;
19+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
20+
use PHPStan\Type\Constant\ConstantBooleanType;
21+
use PHPStan\Type\Constant\ConstantIntegerType;
22+
use PHPStan\Type\Constant\ConstantStringType;
23+
use PHPStan\Type\IntegerRangeType;
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 ParseUrlFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension {
42+
43+
/** @var array<int, \PHPStan\Type\Type>|null */
44+
private $componentTypesPairedConstants = null;
45+
46+
/** @var array<string, \PHPStan\Type\Type>|null */
47+
private $componentTypesPairedStrings = null;
48+
49+
/** @var \PHPStan\Type\Type|null */
50+
private $allComponentsTogetherType = null;
51+
52+
public function isFunctionSupported( FunctionReflection $functionReflection ): bool {
53+
return $functionReflection->getName() === 'WP_CLI\Utils\parse_url';
54+
}
55+
56+
public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope ): ?Type {
57+
if ( count( $functionCall->getArgs() ) < 1 ) {
58+
return null;
59+
}
60+
61+
$this->cacheReturnTypes();
62+
63+
$componentType = new ConstantIntegerType( -1 );
64+
65+
if ( count( $functionCall->getArgs() ) > 1 ) {
66+
$componentType = $scope->getType( $functionCall->getArgs()[1]->value );
67+
68+
if ( ! $componentType->isConstantValue()->yes() ) {
69+
return $this->createAllComponentsReturnType();
70+
}
71+
72+
$componentType = $componentType->toInteger();
73+
74+
if ( ! $componentType instanceof ConstantIntegerType ) {
75+
return $this->createAllComponentsReturnType();
76+
}
77+
}
78+
79+
$urlType = $scope->getType( $functionCall->getArgs()[0]->value );
80+
if ( count( $urlType->getConstantStrings() ) > 0 ) {
81+
$types = [];
82+
foreach ( $urlType->getConstantStrings() as $constantString ) {
83+
try {
84+
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
85+
$result = @parse_url( $constantString->getValue(), $componentType->getValue() );
86+
} catch ( \Error $e ) {
87+
$types[] = new ConstantBooleanType( false );
88+
continue;
89+
}
90+
91+
$types[] = $scope->getTypeFromValue( $result );
92+
}
93+
94+
return TypeCombinator::union( ...$types );
95+
}
96+
97+
if ( $componentType->getValue() === -1 ) {
98+
return TypeCombinator::union( $this->createComponentsArray(), new ConstantBooleanType( false ) );
99+
}
100+
101+
return $this->componentTypesPairedConstants[ $componentType->getValue() ] ?? new ConstantBooleanType( false );
102+
}
103+
104+
private function createAllComponentsReturnType(): Type {
105+
if ( null === $this->allComponentsTogetherType ) {
106+
$returnTypes = [
107+
new ConstantBooleanType( false ),
108+
new NullType(),
109+
IntegerRangeType::fromInterval( 0, 65535 ),
110+
new StringType(),
111+
$this->createComponentsArray(),
112+
];
113+
114+
$this->allComponentsTogetherType = TypeCombinator::union( ...$returnTypes );
115+
}
116+
117+
return $this->allComponentsTogetherType;
118+
}
119+
120+
private function createComponentsArray(): Type {
121+
$builder = ConstantArrayTypeBuilder::createEmpty();
122+
123+
if ( null === $this->componentTypesPairedStrings ) {
124+
throw new \PHPStan\ShouldNotHappenException();
125+
}
126+
127+
foreach ( $this->componentTypesPairedStrings as $componentName => $componentValueType ) {
128+
$builder->setOffsetValueType( new ConstantStringType( $componentName ), $componentValueType, true );
129+
}
130+
131+
return $builder->getArray();
132+
}
133+
134+
private function cacheReturnTypes(): void {
135+
if ( null !== $this->componentTypesPairedConstants ) {
136+
return;
137+
}
138+
139+
$stringType = new StringType();
140+
$port = IntegerRangeType::fromInterval( 0, 65535 );
141+
$falseType = new ConstantBooleanType( false );
142+
$nullType = new NullType();
143+
144+
$stringOrFalseOrNull = TypeCombinator::union( $stringType, $falseType, $nullType );
145+
$portOrFalseOrNull = TypeCombinator::union( $port, $falseType, $nullType );
146+
147+
$this->componentTypesPairedConstants = [
148+
PHP_URL_SCHEME => $stringOrFalseOrNull,
149+
PHP_URL_HOST => $stringOrFalseOrNull,
150+
PHP_URL_PORT => $portOrFalseOrNull,
151+
PHP_URL_USER => $stringOrFalseOrNull,
152+
PHP_URL_PASS => $stringOrFalseOrNull,
153+
PHP_URL_PATH => $stringOrFalseOrNull,
154+
PHP_URL_QUERY => $stringOrFalseOrNull,
155+
PHP_URL_FRAGMENT => $stringOrFalseOrNull,
156+
];
157+
158+
$this->componentTypesPairedStrings = [
159+
'scheme' => $stringType,
160+
'host' => $stringType,
161+
'port' => $port,
162+
'user' => $stringType,
163+
'pass' => $stringType,
164+
'path' => $stringType,
165+
'query' => $stringType,
166+
'fragment' => $stringType,
167+
];
168+
}
169+
}

tests/data/parse_url.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/**
4+
* Test data for WpParseUrlFunctionDynamicReturnTypeExtension.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace WP_CLI\Tests\Tests\PHPStan;
10+
11+
use function WP_CLI\Utils\parse_url;
12+
use function PHPStan\Testing\assertType;
13+
14+
/** @var int $integer */
15+
$integer = doFoo();
16+
17+
/** @var string $string */
18+
$string = doFoo();
19+
20+
$value = parse_url( 'http://abc.def' );
21+
assertType( "array{scheme: 'http', host: 'abc.def'}", $value );
22+
23+
$value = parse_url( 'http://def.abc', -1 );
24+
assertType( "array{scheme: 'http', host: 'def.abc'}", $value );
25+
26+
$value = parse_url( 'http://def.abc', $integer );
27+
assertType( 'array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|int<0, 65535>|string|false|null', $value );
28+
29+
$value = parse_url( 'http://def.abc', PHP_URL_FRAGMENT );
30+
assertType( 'null', $value );
31+
32+
$value = parse_url( 'http://def.abc#this-is-fragment', PHP_URL_FRAGMENT );
33+
assertType( "'this-is-fragment'", $value );
34+
35+
$value = parse_url( 'http://def.abc#this-is-fragment', 9999 );
36+
assertType( 'false', $value );
37+
38+
$value = parse_url( $string, 9999 );
39+
assertType( 'false', $value );
40+
41+
$value = parse_url( $string, PHP_URL_PORT );
42+
assertType( 'int<0, 65535>|false|null', $value );
43+
44+
$value = parse_url( $string );
45+
assertType( 'array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $value );
46+
47+
/** @var 'http://def.abc'|'https://example.com' $union */
48+
$union = $union;
49+
assertType( "array{scheme: 'http', host: 'def.abc'}|array{scheme: 'https', host: 'example.com'}", parse_url( $union ) );
50+
51+
/** @var 'http://def.abc#fragment1'|'https://example.com#fragment2' $union */
52+
$union = $union;
53+
assertType( "'fragment1'|'fragment2'", parse_url( $union, PHP_URL_FRAGMENT ) );
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WP_CLI\Tests\Tests\PHPStan;
6+
7+
class TestDynamicReturnTypeExtension extends \PHPStan\Testing\TypeInferenceTestCase {
8+
9+
/**
10+
* @return iterable<mixed>
11+
*/
12+
public function dataFileAsserts(): iterable {
13+
// Path to a file with actual asserts of expected types:
14+
yield from self::gatherAssertTypes( dirname( __DIR__, 2 ) . '/data/parse_url.php' );
15+
}
16+
17+
/**
18+
* @dataProvider dataFileAsserts
19+
* @param array<string> ...$args
20+
*/
21+
public function testFileAsserts( string $assertType, string $file, ...$args ): void {
22+
$this->assertFileAsserts( $assertType, $file, ...$args );
23+
}
24+
25+
public static function getAdditionalConfigFiles(): array {
26+
return [ dirname( __DIR__, 3 ) . '/extension.neon' ];
27+
}
28+
}

0 commit comments

Comments
 (0)