Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ services:
class: WP_CLI\Tests\PHPStan\WPCliDoHookDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
class: WP_CLI\Tests\PHPStan\WPCliGetConfigDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
class: SzepeViktor\PHPStan\WordPress\HookDocsVisitor
tags:
Expand Down
138 changes: 138 additions & 0 deletions src/PHPStan/WPCliGetConfigDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

declare(strict_types=1);

namespace WP_CLI\Tests\PHPStan;

use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\IntegerType;
use PHPStan\Type\NullType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

use function count;
use function array_values;

final class WPCliGetConfigDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension {

public function getClass(): string {
return 'WP_CLI';
}

public function isStaticMethodSupported( MethodReflection $methodReflection ): bool {
return $methodReflection->getName() === 'get_config';
}

public function getTypeFromStaticMethodCall(
MethodReflection $methodReflection,
StaticCall $methodCall,
Scope $scope
): Type {
$args = $methodCall->getArgs();

if ( count( $args ) === 0 ) {
return $this->getGlobalConfigArrayType();
}

$keyType = $scope->getType( $args[0]->value );

if ( $keyType->isNull()->yes() ) {
return $this->getGlobalConfigArrayType();
}

$constantStrings = $keyType->getConstantStrings();
if ( count( $constantStrings ) > 0 ) {
$types = [];
$configMap = $this->getConfigMap();

foreach ( $constantStrings as $constantString ) {
$key = $constantString->getValue();
if ( isset( $configMap[ $key ] ) ) {
$types[] = $configMap[ $key ];
} else {
$types[] = new NullType();
}
}

if ( count( $types ) > 0 ) {
$returnType = TypeCombinator::union( ...$types );
if ( $keyType->isNull()->maybe() ) {
$returnType = TypeCombinator::union( $returnType, $this->getGlobalConfigArrayType() );
}
return $returnType;
}
}

// Fallback for non-constant string or unknown types
$fallback = TypeCombinator::addNull( $this->getFallbackValueType() );
if ( $keyType->isNull()->maybe() ) {
return TypeCombinator::union( $fallback, $this->getGlobalConfigArrayType() );
}

return $fallback;
}

/**
* @return array<string, Type>
*/
private function getConfigMap(): array {
$stringType = new StringType();
$stringOrNull = TypeCombinator::addNull( $stringType );
$stringList = new ArrayType( new IntegerType(), $stringType );
$boolType = new BooleanType();
$trueOrStringList = TypeCombinator::union( new ConstantBooleanType( true ), $stringList );
$stringOrTrue = TypeCombinator::union( $stringType, new ConstantBooleanType( true ) );
$stringOrFalse = TypeCombinator::union( $stringType, new ConstantBooleanType( false ) );
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return [
'path' => $stringOrNull,
'ssh' => $stringOrNull,
'ssh-args' => $stringList,
'http' => $stringOrNull,
'url' => $stringOrNull,
'user' => $stringOrNull,
'skip-plugins' => $trueOrStringList,
'skip-themes' => $trueOrStringList,
'skip-packages' => $boolType,
'require' => $stringList,
'exec' => $stringList,
'context' => $stringType,
'debug' => $stringOrTrue,
'prompt' => $stringOrFalse,
'quiet' => $boolType,
'apache_modules' => $stringList,
'assume-https' => $boolType,
'color' => TypeCombinator::union( $stringType, $boolType ),
'disabled_commands' => $stringList,
'locale' => $stringType,
'allow-root' => $boolType,
'alias' => $stringType,
];
}

private function getGlobalConfigArrayType(): Type {
$keyTypes = [];
$valueTypes = [];

foreach ( $this->getConfigMap() as $key => $type ) {
$keyTypes[] = new ConstantStringType( $key );
$valueTypes[] = $type;
}

return new ConstantArrayType( $keyTypes, $valueTypes );
}

private function getFallbackValueType(): Type {
$types = array_values( $this->getConfigMap() );
return TypeCombinator::union( ...$types );
}
}
37 changes: 37 additions & 0 deletions tests/data/get_config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/**
* Test data for WPCliGetConfigDynamicReturnTypeExtension.
*/

declare(strict_types=1);

namespace WP_CLI\Tests\Tests\PHPStan;

use WP_CLI;
use function PHPStan\Testing\assertType;

// No arguments
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() );
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 ) );

// Specific keys
assertType( 'string|null', WP_CLI::get_config( 'path' ) );
assertType( 'array<int, string>', WP_CLI::get_config( 'ssh-args' ) );
assertType( 'bool', WP_CLI::get_config( 'skip-packages' ) );
assertType( 'array<int, string>|true', WP_CLI::get_config( 'skip-plugins' ) );
assertType( 'string|false', WP_CLI::get_config( 'prompt' ) );
assertType( 'bool', WP_CLI::get_config( 'quiet' ) );
assertType( 'bool', WP_CLI::get_config( 'assume-https' ) );

// Nullable and non-constant keys
/** @var string|null $nullable_key */
$nullable_key = null;
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 ) );

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

// Invalid key
assertType( 'null', WP_CLI::get_config( 'invalid_key' ) );
1 change: 1 addition & 0 deletions tests/tests/PHPStan/TestDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static function dataFileAsserts(): iterable {
yield from self::gatherAssertTypes( dirname( __DIR__, 2 ) . '/data/get_flag_value.php' );
yield from self::gatherAssertTypes( dirname( __DIR__, 2 ) . '/data/runcommand.php' );
yield from self::gatherAssertTypes( dirname( __DIR__, 2 ) . '/data/do_hook.php' );
yield from self::gatherAssertTypes( dirname( __DIR__, 2 ) . '/data/get_config.php' );
}

/**
Expand Down
Loading