Skip to content

Commit 3770e88

Browse files
authored
Merge branch 'main' into add/209-updated
2 parents 1e77437 + ccd1643 commit 3770e88

15 files changed

+787
-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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
services:
2+
-
3+
class: WP_CLI\Tests\PHPStan\ParseUrlFunctionDynamicReturnTypeExtension
4+
tags:
5+
- phpstan.broker.dynamicFunctionReturnTypeExtension
6+
-
7+
class: WP_CLI\Tests\PHPStan\GetFlagValueFunctionDynamicReturnTypeExtension
8+
tags:
9+
- phpstan.broker.dynamicFunctionReturnTypeExtension
10+
-
11+
class: WP_CLI\Tests\PHPStan\WPCliRuncommandDynamicReturnTypeExtension
12+
tags:
13+
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
14+
parameters:
15+
dynamicConstantNames:
16+
- FOO
17+
strictRules:
18+
allRules: false
19+
disallowedLooseComparison: false
20+
booleansInConditions: false
21+
uselessCast: false
22+
requireParentConstructorCall: false
23+
disallowedConstructs: false
24+
overwriteVariablesWithLoop: false
25+
closureUsesThis: false
26+
matchingInheritedMethodNames: false
27+
numericOperandsInArithmeticOperators: false
28+
strictCalls: false
29+
switchConditionsMatchingType: false
30+
noVariableVariables: false
31+
strictArrayFilter: false
32+
33+
# Add the schema from phpstan-strict-rules so it's available without loading the extension
34+
# and the above configuration works.
35+
parametersSchema:
36+
strictRules: structure([
37+
allRules: anyOf(bool(), arrayOf(bool())),
38+
disallowedLooseComparison: anyOf(bool(), arrayOf(bool())),
39+
booleansInConditions: anyOf(bool(), arrayOf(bool()))
40+
uselessCast: anyOf(bool(), arrayOf(bool()))
41+
requireParentConstructorCall: anyOf(bool(), arrayOf(bool()))
42+
disallowedConstructs: anyOf(bool(), arrayOf(bool()))
43+
overwriteVariablesWithLoop: anyOf(bool(), arrayOf(bool()))
44+
closureUsesThis: anyOf(bool(), arrayOf(bool()))
45+
matchingInheritedMethodNames: anyOf(bool(), arrayOf(bool()))
46+
numericOperandsInArithmeticOperators: anyOf(bool(), arrayOf(bool()))
47+
strictCalls: anyOf(bool(), arrayOf(bool()))
48+
switchConditionsMatchingType: anyOf(bool(), arrayOf(bool()))
49+
noVariableVariables: anyOf(bool(), arrayOf(bool()))
50+
strictArrayFilter: anyOf(bool(), arrayOf(bool()))
51+
])

phpcs.xml.dist

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@
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+
74+
<rule ref="Squiz.PHP.CommentedOutCode.Found">
75+
<exclude-pattern>src/PHPStan/*</exclude-pattern>
76+
</rule>
77+
6478
<!-- This is a procedural stand-alone file that is never loaded in a WordPress context,
6579
so this file does not have to comply with WP naming conventions. -->
6680
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">

phpstan.neon.dist

Lines changed: 11 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,13 @@ 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+
strictCalls: true

src/Context/FeatureContext.php

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

3131
/**
3232
* Features context.
33+
*
34+
* @phpstan-ignore class.implementsDeprecatedInterface
3335
*/
3436
class FeatureContext implements SnippetAcceptingContext {
3537

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: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
/**
4+
* Set return type of \WP_CLI\Utils\get_flag_value().
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace WP_CLI\Tests\PHPStan;
10+
11+
use PhpParser\Node\Expr\FuncCall;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Reflection\FunctionReflection;
14+
use PHPStan\Type\Constant\ConstantStringType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\MixedType;
17+
use PHPStan\Type\NullType;
18+
use PHPStan\Type\TypeCombinator;
19+
20+
use function count;
21+
22+
final class GetFlagValueFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension {
23+
24+
public function isFunctionSupported( FunctionReflection $functionReflection ): bool {
25+
return $functionReflection->getName() === 'WP_CLI\Utils\get_flag_value';
26+
}
27+
28+
public function getTypeFromFunctionCall(
29+
FunctionReflection $functionReflection,
30+
FuncCall $functionCall,
31+
Scope $scope
32+
): Type {
33+
$args = $functionCall->getArgs();
34+
35+
if ( count( $args ) < 2 ) {
36+
// Not enough arguments, fall back to the function's declared return type.
37+
return $functionReflection->getVariants()[0]->getReturnType();
38+
}
39+
40+
$assocArgsType = $scope->getType( $args[0]->value );
41+
$flagArgType = $scope->getType( $args[1]->value );
42+
43+
// 2. Determine the default type
44+
$defaultType = isset( $args[2] ) ? $scope->getType( $args[2]->value ) : new NullType();
45+
46+
$flagConstantStrings = $flagArgType->getConstantStrings();
47+
48+
if ( count( $flagConstantStrings ) !== 1 ) {
49+
// Flag name is dynamic or not a string.
50+
// Return type is a union of all possible values in $assoc_args + default type.
51+
return $this->getDynamicFlagFallbackType( $assocArgsType, $defaultType );
52+
}
53+
54+
// 4. Flag is a single constant string.
55+
$flagValue = $flagConstantStrings[0]->getValue();
56+
57+
// 4.a. If $assoc_args is a single ConstantArray:
58+
$assocConstantArrays = $assocArgsType->getConstantArrays();
59+
if ( count( $assocConstantArrays ) === 1 ) {
60+
$assocArgsConstantArray = $assocConstantArrays[0];
61+
$keyTypes = $assocArgsConstantArray->getKeyTypes();
62+
$valueTypes = $assocArgsConstantArray->getValueTypes();
63+
$resolvedValueType = null;
64+
65+
foreach ( $keyTypes as $index => $keyType ) {
66+
$keyConstantStrings = $keyType->getConstantStrings();
67+
if ( count( $keyConstantStrings ) === 1 && $keyConstantStrings[0]->getValue() === $flagValue ) {
68+
$resolvedValueType = $valueTypes[ $index ];
69+
break;
70+
}
71+
}
72+
73+
if ( null !== $resolvedValueType ) {
74+
// Key definitely exists and has a resolved type.
75+
return $resolvedValueType;
76+
} else {
77+
// Key definitely does not exist in this constant array.
78+
return $defaultType;
79+
}
80+
}
81+
82+
// 4.b. $assoc_args is not a single ConstantArray (but $flagValue is known):
83+
// Use getOffsetValueType for other array-like types.
84+
$valueForKeyType = $assocArgsType->getOffsetValueType( new ConstantStringType( $flagValue ) );
85+
86+
// The key might exist, or its presence is unknown.
87+
// The function returns $assoc_args[$flag] if set, otherwise $default.
88+
return TypeCombinator::union( $valueForKeyType, $defaultType );
89+
}
90+
91+
/**
92+
* Handles the case where the flag name is not a single known constant string.
93+
* The return type is a union of all possible values in $assocArgsType and $defaultType.
94+
*/
95+
private function getDynamicFlagFallbackType( Type $assocArgsType, Type $defaultType ): Type {
96+
$possibleValueTypes = [];
97+
98+
$assocConstantArrays = $assocArgsType->getConstantArrays();
99+
if ( count( $assocConstantArrays ) === 1 ) { // It's one specific constant array
100+
$constantArray = $assocConstantArrays[0];
101+
if ( count( $constantArray->getValueTypes() ) > 0 ) {
102+
$possibleValueTypes = $constantArray->getValueTypes();
103+
}
104+
} else {
105+
$possibleValueTypes[] = new MixedType();
106+
}
107+
108+
if ( empty( $possibleValueTypes ) ) {
109+
return $defaultType;
110+
}
111+
112+
return TypeCombinator::union( $defaultType, ...$possibleValueTypes );
113+
}
114+
}

0 commit comments

Comments
 (0)