diff --git a/extension.neon b/extension.neon index 6fbf5b1b..10b46abc 100644 --- a/extension.neon +++ b/extension.neon @@ -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: diff --git a/src/PHPStan/WPCliGetConfigDynamicReturnTypeExtension.php b/src/PHPStan/WPCliGetConfigDynamicReturnTypeExtension.php new file mode 100644 index 00000000..ff3e1bba --- /dev/null +++ b/src/PHPStan/WPCliGetConfigDynamicReturnTypeExtension.php @@ -0,0 +1,138 @@ +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 + */ + 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 ) ); + + 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 ); + } +} diff --git a/tests/data/get_config.php b/tests/data/get_config.php new file mode 100644 index 00000000..3426fea1 --- /dev/null +++ b/tests/data/get_config.php @@ -0,0 +1,37 @@ +, http: string|null, url: string|null, user: string|null, skip-plugins: array|true, skip-themes: array|true, skip-packages: bool, require: array, exec: array, context: string, debug: string|true, prompt: string|false, quiet: bool, apache_modules: array, assume-https: bool, color: bool|string, disabled_commands: array, locale: string, allow-root: bool, alias: string}', WP_CLI::get_config() ); +assertType( 'array{path: string|null, ssh: string|null, ssh-args: array, http: string|null, url: string|null, user: string|null, skip-plugins: array|true, skip-themes: array|true, skip-packages: bool, require: array, exec: array, context: string, debug: string|true, prompt: string|false, quiet: bool, apache_modules: array, assume-https: bool, color: bool|string, disabled_commands: array, locale: string, allow-root: bool, alias: string}', WP_CLI::get_config( null ) ); + +// Specific keys +assertType( 'string|null', WP_CLI::get_config( 'path' ) ); +assertType( 'array', WP_CLI::get_config( 'ssh-args' ) ); +assertType( 'bool', WP_CLI::get_config( 'skip-packages' ) ); +assertType( 'array|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|bool|string|null>|bool|string|null', WP_CLI::get_config( $nullable_key ) ); + +/** @var string $string_key */ +$string_key = 'path'; +assertType( 'array|bool|string|null', WP_CLI::get_config( $string_key ) ); + +// Invalid key +assertType( 'null', WP_CLI::get_config( 'invalid_key' ) ); diff --git a/tests/tests/PHPStan/TestDynamicReturnTypeExtension.php b/tests/tests/PHPStan/TestDynamicReturnTypeExtension.php index 5c4e7467..88d907ac 100644 --- a/tests/tests/PHPStan/TestDynamicReturnTypeExtension.php +++ b/tests/tests/PHPStan/TestDynamicReturnTypeExtension.php @@ -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' ); } /**