Skip to content

Commit b02c72d

Browse files
authored
PHPStan: Resolve GlobalConfig dynamically in WPCliGetConfigDynamicReturnTypeExtension (#347)
1 parent fc91f06 commit b02c72d

2 files changed

Lines changed: 53 additions & 71 deletions

File tree

src/PHPStan/WPCliGetConfigDynamicReturnTypeExtension.php

Lines changed: 46 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,37 @@
66

77
use PhpParser\Node\Expr\StaticCall;
88
use PHPStan\Analyser\Scope;
9+
use PHPStan\PhpDoc\TypeNodeResolver;
910
use PHPStan\Reflection\MethodReflection;
10-
use PHPStan\Type\ArrayType;
11-
use PHPStan\Type\BooleanType;
11+
use PHPStan\Reflection\ReflectionProvider;
1212
use PHPStan\Type\Constant\ConstantArrayType;
13-
use PHPStan\Type\Constant\ConstantBooleanType;
14-
use PHPStan\Type\Constant\ConstantStringType;
1513
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
16-
use PHPStan\Type\IntegerType;
14+
use PHPStan\Type\ErrorType;
1715
use PHPStan\Type\NullType;
18-
use PHPStan\Type\StringType;
1916
use PHPStan\Type\Type;
2017
use PHPStan\Type\TypeCombinator;
2118

2219
use function count;
23-
use function array_values;
2420

2521
final class WPCliGetConfigDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension {
2622

23+
/** @var ReflectionProvider */
24+
private $reflectionProvider;
25+
26+
/** @var TypeNodeResolver */
27+
private $typeNodeResolver;
28+
29+
/** @var ConstantArrayType|null */
30+
private $globalConfigType = null;
31+
32+
public function __construct(
33+
ReflectionProvider $reflectionProvider,
34+
TypeNodeResolver $typeNodeResolver
35+
) {
36+
$this->reflectionProvider = $reflectionProvider;
37+
$this->typeNodeResolver = $typeNodeResolver;
38+
}
39+
2740
public function getClass(): string {
2841
return 'WP_CLI';
2942
}
@@ -51,88 +64,57 @@ public function getTypeFromStaticMethodCall(
5164

5265
$constantStrings = $keyType->getConstantStrings();
5366
if ( count( $constantStrings ) > 0 ) {
54-
$types = [];
55-
$configMap = $this->getConfigMap();
67+
$types = [];
68+
$globalConfigType = $this->getGlobalConfigArrayType();
5669

5770
foreach ( $constantStrings as $constantString ) {
58-
$key = $constantString->getValue();
59-
if ( isset( $configMap[ $key ] ) ) {
60-
$types[] = $configMap[ $key ];
61-
} else {
71+
$valueType = $globalConfigType->getOffsetValueType( $constantString );
72+
if ( $valueType instanceof ErrorType ) {
6273
$types[] = new NullType();
74+
} else {
75+
$types[] = $valueType;
6376
}
6477
}
6578

6679
if ( count( $types ) > 0 ) {
6780
$returnType = TypeCombinator::union( ...$types );
6881
if ( $keyType->isNull()->maybe() ) {
69-
$returnType = TypeCombinator::union( $returnType, $this->getGlobalConfigArrayType() );
82+
$returnType = TypeCombinator::union( $returnType, $globalConfigType );
7083
}
7184
return $returnType;
7285
}
7386
}
7487

7588
// Fallback for non-constant string or unknown types
76-
$fallback = TypeCombinator::addNull( $this->getFallbackValueType() );
89+
$fallback = TypeCombinator::addNull( $this->getGlobalConfigArrayType()->getItemType() );
7790
if ( $keyType->isNull()->maybe() ) {
7891
return TypeCombinator::union( $fallback, $this->getGlobalConfigArrayType() );
7992
}
8093

8194
return $fallback;
8295
}
8396

84-
/**
85-
* @return array<string, Type>
86-
*/
87-
private function getConfigMap(): array {
88-
$stringType = new StringType();
89-
$stringOrNull = TypeCombinator::addNull( $stringType );
90-
$stringList = new ArrayType( new IntegerType(), $stringType );
91-
$boolType = new BooleanType();
92-
$trueOrStringList = TypeCombinator::union( new ConstantBooleanType( true ), $stringList );
93-
$stringOrTrue = TypeCombinator::union( $stringType, new ConstantBooleanType( true ) );
94-
$stringOrFalse = TypeCombinator::union( $stringType, new ConstantBooleanType( false ) );
95-
96-
return [
97-
'path' => $stringOrNull,
98-
'ssh' => $stringOrNull,
99-
'ssh-args' => $stringList,
100-
'http' => $stringOrNull,
101-
'url' => $stringOrNull,
102-
'user' => $stringOrNull,
103-
'skip-plugins' => $trueOrStringList,
104-
'skip-themes' => $trueOrStringList,
105-
'skip-packages' => $boolType,
106-
'require' => $stringList,
107-
'exec' => $stringList,
108-
'context' => $stringType,
109-
'debug' => $stringOrTrue,
110-
'prompt' => $stringOrFalse,
111-
'quiet' => $boolType,
112-
'apache_modules' => $stringList,
113-
'assume-https' => $boolType,
114-
'color' => TypeCombinator::union( $stringType, $boolType ),
115-
'disabled_commands' => $stringList,
116-
'locale' => $stringType,
117-
'allow-root' => $boolType,
118-
'alias' => $stringType,
119-
];
120-
}
97+
private function getGlobalConfigArrayType(): ConstantArrayType {
98+
if ( null === $this->globalConfigType ) {
99+
$classReflection = $this->reflectionProvider->getClass( 'WP_CLI' );
100+
$typeAliases = $classReflection->getTypeAliases();
101+
$globalConfig = $typeAliases['GlobalConfig'] ?? null;
121102

122-
private function getGlobalConfigArrayType(): Type {
123-
$keyTypes = [];
124-
$valueTypes = [];
103+
if ( null === $globalConfig ) {
104+
throw new \PHPStan\ShouldNotHappenException( 'GlobalConfig type alias not found on WP_CLI class.' );
105+
}
125106

126-
foreach ( $this->getConfigMap() as $key => $type ) {
127-
$keyTypes[] = new ConstantStringType( $key );
128-
$valueTypes[] = $type;
129-
}
107+
/** @phpstan-ignore phpstanApi.method */
108+
$resolved = $globalConfig->resolve( $this->typeNodeResolver );
109+
$constantArrays = $resolved->getConstantArrays();
130110

131-
return new ConstantArrayType( $keyTypes, $valueTypes );
132-
}
111+
if ( count( $constantArrays ) === 0 ) {
112+
throw new \PHPStan\ShouldNotHappenException( 'GlobalConfig type alias on WP_CLI must resolve to a ConstantArrayType.' );
113+
}
114+
115+
$this->globalConfigType = $constantArrays[0];
116+
}
133117

134-
private function getFallbackValueType(): Type {
135-
$types = array_values( $this->getConfigMap() );
136-
return TypeCombinator::union( ...$types );
118+
return $this->globalConfigType;
137119
}
138120
}

tests/data/get_config.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,26 @@
1212
use function PHPStan\Testing\assertType;
1313

1414
// No arguments
15-
assertType( 'array{path: string|null, ssh: string|null, ssh-args: array<int, string>, http: string|null, url: string|null, user: string|null, skip-plugins: array<int, string>|true, skip-themes: array<int, string>|true, skip-packages: bool, require: array<int, string>, exec: array<int, string>, context: string, debug: string|true, prompt: string|false, quiet: bool, apache_modules: array<int, string>, assume-https: bool, color: bool|string, disabled_commands: array<int, string>, locale: string, allow-root: bool, alias: string}', WP_CLI::get_config() );
16-
assertType( 'array{path: string|null, ssh: string|null, ssh-args: array<int, string>, http: string|null, url: string|null, user: string|null, skip-plugins: array<int, string>|true, skip-themes: array<int, string>|true, skip-packages: bool, require: array<int, string>, exec: array<int, string>, context: string, debug: string|true, prompt: string|false, quiet: bool, apache_modules: array<int, string>, assume-https: bool, color: bool|string, disabled_commands: array<int, string>, locale: string, allow-root: bool, alias: string}', WP_CLI::get_config( null ) );
15+
assertType( 'array{path: string|null, ssh: string|null, ssh-args: array<string>, http: string|null, url: string|null, user: string|null, skip-plugins: array<string>|true, skip-themes: array<string>|true, skip-packages: bool, require: array<string>, exec: array<string>, context: string, debug: string|true, prompt: string|true, quiet: bool, apache_modules: array<string>, assume-https: bool, color: bool|string, disabled_commands: array<string>, locale: string, allow-root: bool, alias: string}', WP_CLI::get_config() );
16+
assertType( 'array{path: string|null, ssh: string|null, ssh-args: array<string>, http: string|null, url: string|null, user: string|null, skip-plugins: array<string>|true, skip-themes: array<string>|true, skip-packages: bool, require: array<string>, exec: array<string>, context: string, debug: string|true, prompt: string|true, quiet: bool, apache_modules: array<string>, assume-https: bool, color: bool|string, disabled_commands: array<string>, locale: string, allow-root: bool, alias: string}', WP_CLI::get_config( null ) );
1717

1818
// Specific keys
1919
assertType( 'string|null', WP_CLI::get_config( 'path' ) );
20-
assertType( 'array<int, string>', WP_CLI::get_config( 'ssh-args' ) );
20+
assertType( 'array<string>', WP_CLI::get_config( 'ssh-args' ) );
2121
assertType( 'bool', WP_CLI::get_config( 'skip-packages' ) );
22-
assertType( 'array<int, string>|true', WP_CLI::get_config( 'skip-plugins' ) );
23-
assertType( 'string|false', WP_CLI::get_config( 'prompt' ) );
22+
assertType( 'array<string>|true', WP_CLI::get_config( 'skip-plugins' ) );
23+
assertType( 'string|true', WP_CLI::get_config( 'prompt' ) );
2424
assertType( 'bool', WP_CLI::get_config( 'quiet' ) );
2525
assertType( 'bool', WP_CLI::get_config( 'assume-https' ) );
2626

2727
// Nullable and non-constant keys
2828
/** @var string|null $nullable_key */
2929
$nullable_key = null;
30-
assertType( 'array<\'alias\'|\'allow-root\'|\'apache_modules\'|\'assume-https\'|\'color\'|\'context\'|\'debug\'|\'disabled_commands\'|\'exec\'|\'http\'|\'locale\'|\'path\'|\'prompt\'|\'quiet\'|\'require\'|\'skip-packages\'|\'skip-plugins\'|\'skip-themes\'|\'ssh\'|\'ssh-args\'|\'url\'|\'user\'|int, array<int, string>|bool|string|null>|bool|string|null', WP_CLI::get_config( $nullable_key ) );
30+
assertType( 'array<array<string>|bool|string|null>|bool|string|null', WP_CLI::get_config( $nullable_key ) );
3131

3232
/** @var string $string_key */
3333
$string_key = 'path';
34-
assertType( 'array<int, string>|bool|string|null', WP_CLI::get_config( $string_key ) );
34+
assertType( 'array<string>|bool|string|null', WP_CLI::get_config( $string_key ) );
3535

3636
// Invalid key
3737
assertType( 'null', WP_CLI::get_config( 'invalid_key' ) );

0 commit comments

Comments
 (0)