Skip to content

Commit fa41230

Browse files
committed
Add extension for get_flag_value
1 parent 69ab173 commit fa41230

File tree

4 files changed

+179
-0
lines changed

4 files changed

+179
-0
lines changed

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ services:
33
class: WP_CLI\Tests\PHPStan\ParseUrlFunctionDynamicReturnTypeExtension
44
tags:
55
- phpstan.broker.dynamicFunctionReturnTypeExtension
6+
-
7+
class: WP_CLI\Tests\PHPStan\GetFlagValueFunctionDynamicReturnTypeExtension
8+
tags:
9+
- phpstan.broker.dynamicFunctionReturnTypeExtension
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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\Constant\ConstantArrayType;
16+
use PHPStan\Type\Type;
17+
use PHPStan\Type\TypeCombinator;
18+
19+
use function count;
20+
21+
final class GetFlagValueFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension {
22+
23+
public function isFunctionSupported( FunctionReflection $functionReflection ): bool {
24+
return $functionReflection->getName() === 'WP_CLI\Utils\get_flag_value';
25+
}
26+
27+
public function getTypeFromFunctionCall(
28+
FunctionReflection $functionReflection,
29+
FuncCall $functionCall,
30+
Scope $scope
31+
): Type {
32+
$args = $functionCall->getArgs();
33+
34+
// Ensure we have at least two arguments: $assoc_args and $flag
35+
if ( count( $args ) < 2 ) {
36+
// Not enough arguments, fall back to the function's declared return type or mixed
37+
return $functionReflection->getVariants()[0]->getReturnType();
38+
}
39+
40+
$assocArgsType = $scope->getType( $args[0]->value );
41+
$flagArgType = $scope->getType( $args[1]->value );
42+
43+
// Determine the default type
44+
$defaultType = isset( $args[2] ) ? $scope->getType( $args[2]->value ) : new \PHPStan\Type\NullType();
45+
46+
// We can only be precise if $flag is a constant string
47+
if ( ! $flagArgType->isConstantValue()->yes() || ( ! $flagArgType->toInteger() instanceof ConstantIntegerType && ! $flagArgType->toString() instanceof ConstantStringType ) ) {
48+
// If $flag is not a constant string, we cannot know which key to check.
49+
// The return type will be a union of the array's possible value types and the default type.
50+
if ( $assocArgsType instanceof ConstantArrayType ) {
51+
$valueTypes = [];
52+
foreach ( $assocArgsType->getValueTypes() as $valueType ) {
53+
$valueTypes[] = $valueType;
54+
}
55+
if ( count( $valueTypes ) > 0 ) {
56+
return TypeCombinator::union( ...$valueTypes );
57+
}
58+
return $defaultType; // Array is empty or has no predictable value types
59+
} elseif ( $assocArgsType instanceof \PHPStan\Type\ArrayType ) {
60+
return TypeCombinator::union( $assocArgsType->getItemType(), $defaultType );
61+
}
62+
// Fallback if $assocArgsType isn't a well-defined array type
63+
return new MixedType();
64+
}
65+
66+
$flagValue = $flagArgType->getValue();
67+
68+
// If $assoc_args is a constant array, we can check if the key exists
69+
if ( $assocArgsType->isConstantValue()->yes() && $assocArgsType->toArray() instanceof ConstantArrayType ) {
70+
$keyTypes = $assocArgsType->getKeyTypes();
71+
$valueTypes = $assocArgsType->getValueTypes();
72+
$resolvedValueType = null;
73+
74+
foreach ( $keyTypes as $index => $keyType ) {
75+
if ( $keyType->isConstantValue()->yes() && $keyType->toString() instanceof ConstantStringType && $keyType->getValue() === $flagValue ) {
76+
$resolvedValueType = $valueTypes[ $index ];
77+
break;
78+
}
79+
}
80+
81+
if ( null !== $resolvedValueType ) {
82+
// Key definitely exists, return its type
83+
return $resolvedValueType;
84+
} else {
85+
// Key definitely does not exist, return default type
86+
return $defaultType;
87+
}
88+
}
89+
90+
// If $assocArgsType is a general ArrayType, we can't be sure if the specific flag exists.
91+
// The function's logic is: isset( $assoc_args[ $flag ] ) ? $assoc_args[ $flag ] : $default;
92+
// So, it's a union of the potential value type from the array and the default type.
93+
if ( $assocArgsType->isArray()->yes() ) {
94+
// We don't know IF the key $flagValue exists.
95+
// PHPStan's ArrayType has an itemType which represents the type of values in the array.
96+
// This is the best we can do for a generic array.
97+
return TypeCombinator::union( $assocArgsType->getItemType(), $defaultType );
98+
}
99+
100+
// Fallback for other types of $assocArgsType or if we can't determine.
101+
// This should ideally be the union of what the array could contain for that key and the default.
102+
// For simplicity, if not a ConstantArrayType or ArrayType, return mixed or a broad union.
103+
// In a real-world scenario with more complex types, you might query $assocArgsType->getOffsetValueType(new ConstantStringType($flagValue))
104+
// and then union with $defaultType.
105+
$offsetValueType = $assocArgsType->getOffsetValueType( new ConstantStringType( $flagValue ) );
106+
if ( ! $offsetValueType instanceof MixedType || $offsetValueType->isExplicitMixed() ) {
107+
return TypeCombinator::union( $offsetValueType, $defaultType );
108+
}
109+
110+
return new MixedType(); // Default fallback
111+
}
112+
}

tests/data/get_flag_value.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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\get_flag_value;
12+
use function PHPStan\Testing\assertType;
13+
14+
$value = get_flag_value(
15+
[
16+
'foo' => 'bar',
17+
'baz' => 'qux',
18+
],
19+
'foo'
20+
);
21+
assertType( "'bar'", $value );
22+
23+
$value = get_flag_value(
24+
[
25+
'foo' => 'bar',
26+
'baz' => 'qux',
27+
],
28+
'bar'
29+
);
30+
assertType( 'null', $value );
31+
32+
$value = get_flag_value(
33+
[
34+
'foo' => 'bar',
35+
'baz' => 'qux',
36+
],
37+
'bar',
38+
123
39+
);
40+
assertType( '123', $value );
41+
42+
$value = get_flag_value(
43+
[
44+
'foo' => 'bar',
45+
'baz' => true,
46+
],
47+
'baz',
48+
123
49+
);
50+
assertType( 'true', $value );
51+
52+
$assoc_args = [
53+
'foo' => 'bar',
54+
'baz' => true,
55+
];
56+
$key = 'baz';
57+
58+
$value = get_flag_value( $assoc_args, $key, 123 );
59+
assertType( 'true', $value );
60+
61+
$value = get_flag_value( $assoc_args, $key2, 123 );
62+
assertType( "'bar'|true", $value );

tests/tests/PHPStan/TestDynamicReturnTypeExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class TestDynamicReturnTypeExtension extends \PHPStan\Testing\TypeInferenceTestC
1212
public function dataFileAsserts(): iterable {
1313
// Path to a file with actual asserts of expected types:
1414
yield from self::gatherAssertTypes( dirname( __DIR__, 2 ) . '/data/parse_url.php' );
15+
yield from self::gatherAssertTypes( dirname( __DIR__, 2 ) . '/data/get_flag_value.php' );
1516
}
1617

1718
/**

0 commit comments

Comments
 (0)