diff --git a/src/PHPStan/GetFlagValueFunctionDynamicReturnTypeExtension.php b/src/PHPStan/GetFlagValueFunctionDynamicReturnTypeExtension.php index 57941e98..f367b1ed 100644 --- a/src/PHPStan/GetFlagValueFunctionDynamicReturnTypeExtension.php +++ b/src/PHPStan/GetFlagValueFunctionDynamicReturnTypeExtension.php @@ -61,22 +61,28 @@ public function getTypeFromFunctionCall( $keyTypes = $assocArgsConstantArray->getKeyTypes(); $valueTypes = $assocArgsConstantArray->getValueTypes(); $resolvedValueType = null; + $isOptionalKey = false; foreach ( $keyTypes as $index => $keyType ) { $keyConstantStrings = $keyType->getConstantStrings(); if ( count( $keyConstantStrings ) === 1 && $keyConstantStrings[0]->getValue() === $flagValue ) { $resolvedValueType = $valueTypes[ $index ]; + $isOptionalKey = $assocArgsConstantArray->isOptionalKey( $index ); break; } } - if ( null !== $resolvedValueType ) { - // Key definitely exists and has a resolved type. - return $resolvedValueType; - } else { + if ( null === $resolvedValueType ) { // Key definitely does not exist in this constant array. return $defaultType; } + + // get_flag_value() uses isset(), so a null value also falls back to the default. + if ( $resolvedValueType->isNull()->no() && ! $isOptionalKey ) { + return $resolvedValueType; + } + + return TypeCombinator::union( TypeCombinator::removeNull( $resolvedValueType ), $defaultType ); } // 4.b. $assoc_args is not a single ConstantArray (but $flagValue is known): diff --git a/tests/data/get_flag_value.php b/tests/data/get_flag_value.php index 9a6b39c2..f2046da9 100644 --- a/tests/data/get_flag_value.php +++ b/tests/data/get_flag_value.php @@ -60,3 +60,14 @@ $value = get_flag_value( $assoc_args, $key2, 123 ); assertType( "123|'bar'|true", $value ); + +/** + * @param array{foo?: string, bar?: 'json'|'xml', baz: bool, qux: string|null} $assoc_args + */ +function test_shape_with_optional_keys( array $assoc_args ): void { + assertType( 'string|null', get_flag_value( $assoc_args, 'foo' ) ); + assertType( "'json'|'xml'|null", get_flag_value( $assoc_args, 'bar' ) ); + assertType( "'json'|'xml'|false", get_flag_value( $assoc_args, 'bar', false ) ); + assertType( 'bool', get_flag_value( $assoc_args, 'baz', 123 ) ); + assertType( '123|string', get_flag_value( $assoc_args, 'qux', 123 ) ); +}